plan-verify 0.0.0-stage → 0.1.0

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 (222) hide show
  1. package/.github/workflows/ci.yml +48 -0
  2. package/README.md +1431 -2
  3. package/assets/icon.png +0 -0
  4. package/assets/logo-mark.png +0 -0
  5. package/assets/logo.png +0 -0
  6. package/examples/checkout.ears.md +74 -0
  7. package/examples/order-always.ears.md +3 -0
  8. package/examples/order-awkward.ears.md +8 -0
  9. package/examples/order-bad.json +1 -0
  10. package/examples/order-chain.ears.md +9 -0
  11. package/examples/order-chosen.ears.md +7 -0
  12. package/examples/order-clear.ears.md +8 -0
  13. package/examples/order-combined.ears.md +10 -0
  14. package/examples/order-conflict.ears.md +8 -0
  15. package/examples/order-covered.ears.md +9 -0
  16. package/examples/order-duplicate.ears.md +8 -0
  17. package/examples/order-fail.json +5 -0
  18. package/examples/order-misuse.ears.md +7 -0
  19. package/examples/order-negation.ears.md +7 -0
  20. package/examples/order-pass.json +5 -0
  21. package/examples/order-quantity.ears.md +8 -0
  22. package/examples/order-redundant.ears.md +9 -0
  23. package/examples/order-repair.ears.md +9 -0
  24. package/examples/order-repeat.ears.md +13 -0
  25. package/examples/order-same.ears.md +3 -0
  26. package/examples/order-status.ears.md +7 -0
  27. package/examples/order-unanswered.ears.md +8 -0
  28. package/examples/order-unhandled.ears.md +8 -0
  29. package/examples/order-unmeasured.ears.md +10 -0
  30. package/examples/order-unused.ears.md +13 -0
  31. package/examples/order-vacuous.ears.md +9 -0
  32. package/examples/order-vague.ears.md +7 -0
  33. package/examples/order-wide.ears.md +5 -0
  34. package/examples/order-wording.ears.md +8 -0
  35. package/examples/order.ears.md +7 -0
  36. package/examples/payment-if.ears.md +7 -0
  37. package/examples/plan-to-problems/README.md +54 -0
  38. package/examples/plan-to-problems/expected-problems.md +25 -0
  39. package/examples/plan-to-problems/generate.mjs +186 -0
  40. package/examples/plan-to-problems/generated.ears.md +27 -0
  41. package/examples/plan-to-problems/lint.json +576 -0
  42. package/examples/plan-to-problems/plan.md +5 -0
  43. package/examples/plan-to-problems/planted.txt +5 -0
  44. package/examples/plan-to-problems/problems.md +112 -0
  45. package/examples/status-placed.ears.md +3 -0
  46. package/package.json +25 -4
  47. package/scripts/install-scriptc.sh +16 -0
  48. package/skills/plan-verify/SKILL.md +1515 -0
  49. package/src/check.ts +250 -0
  50. package/src/decide.ts +1324 -0
  51. package/src/ears.ts +1520 -0
  52. package/src/jsonvalue.ts +228 -0
  53. package/src/lint.ts +1245 -0
  54. package/src/main.ts +273 -0
  55. package/src/policy.ts +148 -0
  56. package/src/repair.ts +567 -0
  57. package/src/report.ts +329 -0
  58. package/src/scenarios.ts +149 -0
  59. package/src/smt.ts +97 -0
  60. package/src/solver.ts +868 -0
  61. package/src/spec.ts +307 -0
  62. package/src/term.ts +331 -0
  63. package/src/text.ts +95 -0
  64. package/test/fixtures/malformed.ears.md +3 -0
  65. package/test/fixtures/unknown-phrase.ears.md +5 -0
  66. package/test/golden/check-fail-json.exit +1 -0
  67. package/test/golden/check-fail-json.stderr +0 -0
  68. package/test/golden/check-fail-json.stdout +13 -0
  69. package/test/golden/check-fail.exit +1 -0
  70. package/test/golden/check-fail.stderr +0 -0
  71. package/test/golden/check-fail.stdout +7 -0
  72. package/test/golden/check-pass-json.exit +1 -0
  73. package/test/golden/check-pass-json.stderr +0 -0
  74. package/test/golden/check-pass-json.stdout +6 -0
  75. package/test/golden/check-pass.exit +1 -0
  76. package/test/golden/check-pass.stderr +0 -0
  77. package/test/golden/check-pass.stdout +3 -0
  78. package/test/golden/emit-smt-json-usage.exit +1 -0
  79. package/test/golden/emit-smt-json-usage.stderr +0 -0
  80. package/test/golden/emit-smt-json-usage.stdout +4 -0
  81. package/test/golden/emit-smt-json.exit +1 -0
  82. package/test/golden/emit-smt-json.stderr +0 -0
  83. package/test/golden/emit-smt-json.stdout +5 -0
  84. package/test/golden/emit-smt.exit +1 -0
  85. package/test/golden/emit-smt.stderr +0 -0
  86. package/test/golden/emit-smt.stdout +10 -0
  87. package/test/golden/help.exit +1 -0
  88. package/test/golden/help.stderr +0 -0
  89. package/test/golden/help.stdout +8 -0
  90. package/test/golden/lint-awkward.exit +1 -0
  91. package/test/golden/lint-awkward.stderr +0 -0
  92. package/test/golden/lint-awkward.stdout +29 -0
  93. package/test/golden/lint-chain.exit +1 -0
  94. package/test/golden/lint-chain.stderr +0 -0
  95. package/test/golden/lint-chain.stdout +56 -0
  96. package/test/golden/lint-checkout.exit +1 -0
  97. package/test/golden/lint-checkout.stderr +0 -0
  98. package/test/golden/lint-checkout.stdout +301 -0
  99. package/test/golden/lint-clean.exit +1 -0
  100. package/test/golden/lint-clean.stderr +0 -0
  101. package/test/golden/lint-clean.stdout +9 -0
  102. package/test/golden/lint-clear.exit +1 -0
  103. package/test/golden/lint-clear.stderr +0 -0
  104. package/test/golden/lint-clear.stdout +9 -0
  105. package/test/golden/lint-conflict-json.exit +1 -0
  106. package/test/golden/lint-conflict-json.stderr +0 -0
  107. package/test/golden/lint-conflict-json.stdout +80 -0
  108. package/test/golden/lint-conflict.exit +1 -0
  109. package/test/golden/lint-conflict.stderr +0 -0
  110. package/test/golden/lint-conflict.stdout +41 -0
  111. package/test/golden/lint-duplicate.exit +1 -0
  112. package/test/golden/lint-duplicate.stderr +0 -0
  113. package/test/golden/lint-duplicate.stdout +14 -0
  114. package/test/golden/lint-gap.exit +1 -0
  115. package/test/golden/lint-gap.stderr +0 -0
  116. package/test/golden/lint-gap.stdout +12 -0
  117. package/test/golden/lint-misuse.exit +1 -0
  118. package/test/golden/lint-misuse.stderr +0 -0
  119. package/test/golden/lint-misuse.stdout +18 -0
  120. package/test/golden/lint-negation.exit +1 -0
  121. package/test/golden/lint-negation.stderr +0 -0
  122. package/test/golden/lint-negation.stdout +13 -0
  123. package/test/golden/lint-quantity.exit +1 -0
  124. package/test/golden/lint-quantity.stderr +0 -0
  125. package/test/golden/lint-quantity.stdout +41 -0
  126. package/test/golden/lint-redundant.exit +1 -0
  127. package/test/golden/lint-redundant.stderr +0 -0
  128. package/test/golden/lint-redundant.stdout +14 -0
  129. package/test/golden/lint-repair-json.exit +1 -0
  130. package/test/golden/lint-repair-json.stderr +0 -0
  131. package/test/golden/lint-repair-json.stdout +127 -0
  132. package/test/golden/lint-repair.exit +1 -0
  133. package/test/golden/lint-repair.stderr +0 -0
  134. package/test/golden/lint-repair.stdout +54 -0
  135. package/test/golden/lint-repeat.exit +1 -0
  136. package/test/golden/lint-repeat.stderr +0 -0
  137. package/test/golden/lint-repeat.stdout +48 -0
  138. package/test/golden/lint-status.exit +1 -0
  139. package/test/golden/lint-status.stderr +0 -0
  140. package/test/golden/lint-status.stdout +44 -0
  141. package/test/golden/lint-unanswered.exit +1 -0
  142. package/test/golden/lint-unanswered.stderr +0 -0
  143. package/test/golden/lint-unanswered.stdout +12 -0
  144. package/test/golden/lint-unhandled.exit +1 -0
  145. package/test/golden/lint-unhandled.stderr +0 -0
  146. package/test/golden/lint-unhandled.stdout +12 -0
  147. package/test/golden/lint-unmeasured.exit +1 -0
  148. package/test/golden/lint-unmeasured.stderr +0 -0
  149. package/test/golden/lint-unmeasured.stdout +13 -0
  150. package/test/golden/lint-unused-json.exit +1 -0
  151. package/test/golden/lint-unused-json.stderr +0 -0
  152. package/test/golden/lint-unused-json.stdout +37 -0
  153. package/test/golden/lint-unused.exit +1 -0
  154. package/test/golden/lint-unused.stderr +0 -0
  155. package/test/golden/lint-unused.stdout +25 -0
  156. package/test/golden/lint-vacuous-json.exit +1 -0
  157. package/test/golden/lint-vacuous-json.stderr +0 -0
  158. package/test/golden/lint-vacuous-json.stdout +27 -0
  159. package/test/golden/lint-vacuous.exit +1 -0
  160. package/test/golden/lint-vacuous.stderr +0 -0
  161. package/test/golden/lint-vacuous.stdout +19 -0
  162. package/test/golden/lint-vague.exit +1 -0
  163. package/test/golden/lint-vague.stderr +0 -0
  164. package/test/golden/lint-vague.stdout +21 -0
  165. package/test/golden/lint-wide.exit +1 -0
  166. package/test/golden/lint-wide.stderr +0 -0
  167. package/test/golden/lint-wide.stdout +9 -0
  168. package/test/golden/lint-wording.exit +1 -0
  169. package/test/golden/lint-wording.stderr +0 -0
  170. package/test/golden/lint-wording.stdout +21 -0
  171. package/test/golden/malformed-scenario.exit +1 -0
  172. package/test/golden/malformed-scenario.stderr +1 -0
  173. package/test/golden/malformed-scenario.stdout +0 -0
  174. package/test/golden/malformed-spec.exit +1 -0
  175. package/test/golden/malformed-spec.stderr +1 -0
  176. package/test/golden/malformed-spec.stdout +0 -0
  177. package/test/golden/policy-counterexample.exit +1 -0
  178. package/test/golden/policy-counterexample.stderr +0 -0
  179. package/test/golden/policy-counterexample.stdout +13 -0
  180. package/test/golden/policy-covered.exit +1 -0
  181. package/test/golden/policy-covered.stderr +0 -0
  182. package/test/golden/policy-covered.stdout +19 -0
  183. package/test/golden/policy-holds.exit +1 -0
  184. package/test/golden/policy-holds.stderr +0 -0
  185. package/test/golden/policy-holds.stdout +3 -0
  186. package/test/golden/policy-json.exit +1 -0
  187. package/test/golden/policy-json.stderr +0 -0
  188. package/test/golden/policy-json.stdout +34 -0
  189. package/test/golden/policy-status.exit +1 -0
  190. package/test/golden/policy-status.stderr +0 -0
  191. package/test/golden/policy-status.stdout +16 -0
  192. package/test/golden/policy-vacuous.exit +1 -0
  193. package/test/golden/policy-vacuous.stderr +0 -0
  194. package/test/golden/policy-vacuous.stdout +3 -0
  195. package/test/golden/policy-wide.exit +1 -0
  196. package/test/golden/policy-wide.stderr +0 -0
  197. package/test/golden/policy-wide.stdout +15 -0
  198. package/test/golden/policy.exit +1 -0
  199. package/test/golden/policy.stderr +0 -0
  200. package/test/golden/policy.stdout +19 -0
  201. package/test/golden/scenarios-json.exit +1 -0
  202. package/test/golden/scenarios-json.stderr +0 -0
  203. package/test/golden/scenarios-json.stdout +75 -0
  204. package/test/golden/scenarios-status.exit +1 -0
  205. package/test/golden/scenarios-status.stderr +0 -0
  206. package/test/golden/scenarios-status.stdout +24 -0
  207. package/test/golden/scenarios.exit +1 -0
  208. package/test/golden/scenarios.stderr +0 -0
  209. package/test/golden/scenarios.stdout +26 -0
  210. package/test/golden/unit.exit +1 -0
  211. package/test/golden/unit.stderr +0 -0
  212. package/test/golden/unit.stdout +2 -0
  213. package/test/golden/unknown-phrase.exit +1 -0
  214. package/test/golden/unknown-phrase.stderr +1 -0
  215. package/test/golden/unknown-phrase.stdout +0 -0
  216. package/test/golden/usage.exit +1 -0
  217. package/test/golden/usage.stderr +9 -0
  218. package/test/golden/usage.stdout +0 -0
  219. package/test/run-all.ts +499 -0
  220. package/test/soundness.ts +375 -0
  221. package/test/unit.ts +1352 -0
  222. package/tsconfig.json +15 -0
package/README.md CHANGED
@@ -1,3 +1,1432 @@
1
- # Temporary Holding Version
1
+ <p align="center"><img src="assets/logo.png" alt="plan-verify" width="480"></p>
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # plan-verify
4
+
5
+ Formerly ears-verify.
6
+
7
+ Catch contradictions in your requirements before anyone builds them.
8
+
9
+ plan-verify is for product managers, and for the AI agents who write requirements with them. You talk through what the product should do. The agent turns that into [EARS](https://alistairmavin.com/ears/) requirements. Each name in the schema is bound to a phrase, and the requirement is written with those phrases, so a line reads the way you would say it. plan-verify then looks for requirements that cannot all be true, and explains the clash in plain language: which requirements disagree, in what situation, a concrete example, and a fix.
10
+
11
+ It will not choose which requirement wins. That question goes back to you.
12
+
13
+ The same TypeScript runs on Node 24 and compiles to a native binary with [ScriptC](https://scriptc.dev) 0.4.1. A requirement is checked by our own solver. The logic is booleans, integers in a declared range, and enums.
14
+
15
+ ## Try it
16
+
17
+ You need Node 24 and clang. `npm install` installs ScriptC 0.4.1.
18
+
19
+ ```bash
20
+ npm install
21
+ npm run build
22
+ ./build/plan-verify lint examples/order-conflict.ears.md
23
+ ```
24
+
25
+ `node --experimental-strip-types src/main.ts` runs the same program and prints the same bytes. The tests fail if the two ever drift.
26
+
27
+ <!-- verify stdout 1
28
+ build/plan-verify
29
+ lint
30
+ examples/order-conflict.ears.md
31
+ -->
32
+ ```text
33
+ spec: examples/order-conflict.ears.md
34
+ conflicts: 1
35
+ gaps: 0
36
+ misuse: 0
37
+ wording: 0
38
+ redundant: 0
39
+ duplicate: 0
40
+ vacuous: 0
41
+ unused: 0
42
+
43
+ conflict {R2, R3}
44
+ R2 and R3 cannot both hold.
45
+
46
+ R2 line 7
47
+ When an order is submitted, the Order System shall fulfill the order.
48
+
49
+ R3 line 8
50
+ When an order is submitted, the Order System shall not fulfill the order.
51
+
52
+ They clash when an order was submitted.
53
+ R2 requires that the order was fulfilled.
54
+ R3 requires that the order was not fulfilled.
55
+
56
+ Example scenario:
57
+ {
58
+ "order_submitted": true,
59
+ "order_fulfilled": true
60
+ }
61
+ This scenario keeps R2 and breaks R3.
62
+
63
+ Suggested fix: ask which requirement should win. To keep R2, remove R3. To keep R3, remove R2.
64
+
65
+ Candidate repairs follow. This report does not choose one.
66
+
67
+ repair drop R2
68
+ One repair is to drop R2 and keep R3 as written.
69
+ When an order is submitted, the Order System shall not fulfill the order.
70
+
71
+ repair drop R3
72
+ One repair is to drop R3 and keep R2 as written.
73
+ When an order is submitted, the Order System shall fulfill the order.
74
+ ```
75
+
76
+ R2 says the order was fulfilled. R3 says the order was not fulfilled. Both apply when the customer submits an order, so no behavior can satisfy both. The example keeps R2 only because R3 is the last requirement in the set. It is a witness, and it is not a recommendation. Ask which requirement should win, then delete or rewrite the other. The candidate repairs under the clash are the same question in sentence form: drop one requirement, or, when a single extra condition can separate them, the rewritten line. None of those repairs is chosen.
77
+
78
+ ## A conversation
79
+
80
+ A product manager says: "When no order has been submitted, do not fulfill it. When an order is submitted, fulfill it. And when an order is submitted, do not fulfill it."
81
+
82
+ The agent writes that as EARS. Each declared name is bound to a phrase, and the requirements use those phrases:
83
+
84
+ ```ears
85
+ event order_submitted: Bool "The customer submits an order." phrase "an order is submitted"
86
+ output order_fulfilled: Bool "The order is fulfilled." phrase "fulfill the order" outcome "the order was fulfilled" / "the order was not fulfilled"
87
+
88
+ R1 When an order is not submitted, the Order System shall not fulfill the order.
89
+ R2 When an order is submitted, the Order System shall fulfill the order.
90
+ R3 When an order is submitted, the Order System shall not fulfill the order.
91
+ ```
92
+
93
+ That is `examples/order-conflict.ears.md`. Lint names the clash. R2 and R3 both apply when an order was submitted, and the example keeps R2 and breaks R3.
94
+
95
+ <!-- verify stdout 1
96
+ build/plan-verify
97
+ lint
98
+ examples/order-conflict.ears.md
99
+ -->
100
+ ```text
101
+ spec: examples/order-conflict.ears.md
102
+ conflicts: 1
103
+ gaps: 0
104
+ misuse: 0
105
+ wording: 0
106
+ redundant: 0
107
+ duplicate: 0
108
+ vacuous: 0
109
+ unused: 0
110
+
111
+ conflict {R2, R3}
112
+ R2 and R3 cannot both hold.
113
+
114
+ R2 line 7
115
+ When an order is submitted, the Order System shall fulfill the order.
116
+
117
+ R3 line 8
118
+ When an order is submitted, the Order System shall not fulfill the order.
119
+
120
+ They clash when an order was submitted.
121
+ R2 requires that the order was fulfilled.
122
+ R3 requires that the order was not fulfilled.
123
+
124
+ Example scenario:
125
+ {
126
+ "order_submitted": true,
127
+ "order_fulfilled": true
128
+ }
129
+ This scenario keeps R2 and breaks R3.
130
+
131
+ Suggested fix: ask which requirement should win. To keep R2, remove R3. To keep R3, remove R2.
132
+
133
+ Candidate repairs follow. This report does not choose one.
134
+
135
+ repair drop R2
136
+ One repair is to drop R2 and keep R3 as written.
137
+ When an order is submitted, the Order System shall not fulfill the order.
138
+
139
+ repair drop R3
140
+ One repair is to drop R3 and keep R2 as written.
141
+ When an order is submitted, the Order System shall fulfill the order.
142
+ ```
143
+
144
+ The product manager chooses to keep fulfillment of a submitted order and to remove the refusal. The agent deletes R3. `examples/order-chosen.ears.md` is what remains:
145
+
146
+ ```ears
147
+ R1 When an order is not submitted, the Order System shall not fulfill the order.
148
+ R2 When an order is submitted, the Order System shall fulfill the order.
149
+ ```
150
+
151
+ Lint again, and the spec is clean:
152
+
153
+ <!-- verify stdout 0
154
+ build/plan-verify
155
+ lint
156
+ examples/order-chosen.ears.md
157
+ -->
158
+ ```text
159
+ spec: examples/order-chosen.ears.md
160
+ conflicts: 0
161
+ gaps: 0
162
+ misuse: 0
163
+ wording: 0
164
+ redundant: 0
165
+ duplicate: 0
166
+ vacuous: 0
167
+ unused: 0
168
+ ```
169
+
170
+ ## A condition that separates the clash
171
+
172
+ `examples/order-repair.ears.md` lends a copy that is in the building and refuses a copy that is out on loan. Those two states can both be true, so the requirements clash. Lint still asks which requirement should win. It also shows candidate repairs it has checked: drop either requirement, or add one existing state to one condition. Adding "and the copy is not out on loan" to R1 leaves R2 unchanged, and the patched spec no longer has that conflict. Adding "and the copy is not in the building" to R2 is the other strengthening. The report does not pick one.
173
+
174
+ <!-- verify stdout 1
175
+ build/plan-verify
176
+ lint
177
+ examples/order-repair.ears.md
178
+ -->
179
+ ```text
180
+ spec: examples/order-repair.ears.md
181
+ conflicts: 1
182
+ gaps: 1
183
+ misuse: 0
184
+ wording: 0
185
+ redundant: 0
186
+ duplicate: 0
187
+ vacuous: 0
188
+ unused: 0
189
+
190
+ conflict {R1, R2}
191
+ R1 and R2 cannot both hold.
192
+
193
+ R1 line 8
194
+ While the copy is in the building, when a member asks to take a copy home, the Desk shall allow the loan.
195
+
196
+ R2 line 9
197
+ While the copy is out on loan, when a member asks to take a copy home, the Desk shall not allow the loan.
198
+
199
+ They clash when the copy was in the building, the copy was out on loan, and a member asks to take a copy home.
200
+ R1 requires that the loan was allowed.
201
+ R2 requires that the loan was not allowed.
202
+
203
+ Example scenario:
204
+ {
205
+ "copy_in_building": true,
206
+ "copy_on_loan": true,
207
+ "borrow_requested": true,
208
+ "loan_allowed": true
209
+ }
210
+ This scenario keeps R1 and breaks R2.
211
+
212
+ Suggested fix: ask which requirement should win. To keep R1, remove R2. To keep R2, remove R1.
213
+
214
+ Candidate repairs follow. This report does not choose one.
215
+
216
+ repair drop R1
217
+ One repair is to drop R1 and keep R2 as written.
218
+ While the copy is out on loan, when a member asks to take a copy home, the Desk shall not allow the loan.
219
+
220
+ repair drop R2
221
+ One repair is to drop R2 and keep R1 as written.
222
+ While the copy is in the building, when a member asks to take a copy home, the Desk shall allow the loan.
223
+
224
+ repair strengthen R1
225
+ One repair is to add "and the copy is not out on loan" to the condition of R1. R2 stays as written.
226
+ While the copy is in the building, when a member asks to take a copy home and the copy is not out on loan, the Desk shall allow the loan.
227
+
228
+ repair strengthen R2
229
+ One repair is to add "and the copy is not in the building" to the condition of R2. R1 stays as written.
230
+ While the copy is out on loan, when a member asks to take a copy home and the copy is not in the building, the Desk shall not allow the loan.
231
+
232
+ gap loan_allowed
233
+ Nothing says whether the loan was allowed when the copy was in the building, the copy was out on loan, and not a member asks to take a copy home. Suggested fix: add a requirement for this situation, and ask what the system shall do. Do not choose the response.
234
+ ```
235
+
236
+ ## Check one scenario
237
+
238
+ `examples/order.ears.md` is a single rule: when an order is submitted and inventory is available, the order is fulfilled. A scenario is a JSON object with one value for every declared name.
239
+
240
+ <!-- verify stdout 1
241
+ build/plan-verify
242
+ check
243
+ examples/order.ears.md
244
+ --input
245
+ examples/order-fail.json
246
+ -->
247
+ ```text
248
+ spec: examples/order.ears.md
249
+ scenario: examples/order-fail.json
250
+ violations: 1
251
+
252
+ R1 line 7
253
+ WHEN an order is submitted AND inventory is available, THE Order System SHALL fulfill the order.
254
+ An order was submitted and inventory was available, but the order was not fulfilled.
255
+ ```
256
+
257
+ <!-- verify stdout 0
258
+ build/plan-verify
259
+ check
260
+ examples/order.ears.md
261
+ --input
262
+ examples/order-pass.json
263
+ -->
264
+ ```text
265
+ spec: examples/order.ears.md
266
+ scenario: examples/order-pass.json
267
+ violations: 0
268
+ ```
269
+
270
+ `check` answers a different question from `lint`. The passing scenario fits R1. Lint still finds a hole, because nothing says what fulfillment should be when the order was submitted and the inventory is not available:
271
+
272
+ <!-- verify stdout 1
273
+ build/plan-verify
274
+ lint
275
+ examples/order.ears.md
276
+ -->
277
+ ```text
278
+ spec: examples/order.ears.md
279
+ conflicts: 0
280
+ gaps: 1
281
+ misuse: 0
282
+ wording: 0
283
+ redundant: 0
284
+ duplicate: 0
285
+ vacuous: 0
286
+ unused: 0
287
+
288
+ gap order_fulfilled
289
+ Nothing says whether the order was fulfilled when an order was submitted and inventory was not available. Suggested fix: add a requirement for this situation, and ask what the system shall do. Do not choose the response.
290
+ ```
291
+
292
+ `examples/order-covered.ears.md` gives every situation a response. Lint is quiet:
293
+
294
+ <!-- verify stdout 0
295
+ build/plan-verify
296
+ lint
297
+ examples/order-covered.ears.md
298
+ -->
299
+ ```text
300
+ spec: examples/order-covered.ears.md
301
+ conflicts: 0
302
+ gaps: 0
303
+ misuse: 0
304
+ wording: 0
305
+ redundant: 0
306
+ duplicate: 0
307
+ vacuous: 0
308
+ unused: 0
309
+ ```
310
+
311
+ ## See the logic
312
+
313
+ `emit-smt` prints each requirement as one named implication. The comment above the assertion points back at the line and the sentence.
314
+
315
+ <!-- verify stdout 0
316
+ build/plan-verify
317
+ emit-smt
318
+ examples/order.ears.md
319
+ -->
320
+ ```text
321
+ ; spec: examples/order.ears.md
322
+ (set-logic ALL)
323
+ ; event order_submitted: Bool "The customer submits an order." phrase "an order is submitted"
324
+ (declare-fun order_submitted () Bool)
325
+ ; state inventory_available: Bool "Enough stock is on hand to fill the order." phrase "inventory is available"
326
+ (declare-fun inventory_available () Bool)
327
+ ; output order_fulfilled: Bool "The order is fulfilled." phrase "fulfill the order" outcome "the order was fulfilled" / "the order was not fulfilled"
328
+ (declare-fun order_fulfilled () Bool)
329
+ ; R1_L7 line 7: WHEN an order is submitted AND inventory is available, THE Order System SHALL fulfill the order.
330
+ (assert (! (=> (and order_submitted inventory_available) order_fulfilled) :named R1_L7))
331
+ ```
332
+
333
+ ## Commands
334
+
335
+ | Command | Question it answers | Status |
336
+ | --- | --- | --- |
337
+ | `check [--json] <spec> --input <scenario>` | Does this scenario fit the requirements? | Implemented |
338
+ | `lint [--json] <spec>` | Do any requirements clash, leave a hole, never trigger, leave schema unused, repeat each other, or use a word a test cannot pin down? | Implemented |
339
+ | `report <spec>` | What are the logical problems, in plain language? | Implemented |
340
+ | `scenarios [--json] <spec>` | What accepted and rejected examples show each requirement's trigger? | Implemented |
341
+ | `emit-smt [--json] <spec>` | What is the SMT-LIB for these requirements? | Implemented |
342
+ | `policy [--json] <spec> [--property <file>]` | Which scenarios do the requirements allow and forbid? Does any allowed scenario break a property? | Implemented |
343
+ | `help` | What commands are available? | Implemented |
344
+
345
+ <!-- verify stdout 0
346
+ build/plan-verify
347
+ help
348
+ -->
349
+ ```text
350
+ usage:
351
+ plan-verify check [--json] <spec> --input <scenario>
352
+ plan-verify emit-smt [--json] <spec>
353
+ plan-verify lint [--json] <spec>
354
+ plan-verify report <spec>
355
+ plan-verify scenarios [--json] <spec>
356
+ plan-verify policy [--json] <spec> [--property <file>]
357
+ plan-verify help
358
+ ```
359
+
360
+ With no property, `policy` prints one scenario the requirements allow and one they forbid. Both are the first assignment the search finds, false before true. A requirement whose condition is false counts as satisfied, so the allowed scenario is not a recommendation.
361
+
362
+ <!-- verify stdout 0
363
+ build/plan-verify
364
+ policy
365
+ examples/order.ears.md
366
+ -->
367
+ ```text
368
+ spec: examples/order.ears.md
369
+ accepted:
370
+ {
371
+ "order_submitted": false,
372
+ "inventory_available": false,
373
+ "order_fulfilled": false
374
+ }
375
+ This scenario satisfies every requirement.
376
+ rejected:
377
+ {
378
+ "order_submitted": true,
379
+ "inventory_available": true,
380
+ "order_fulfilled": false
381
+ }
382
+ This scenario violates a requirement.
383
+
384
+ R1 line 7
385
+ WHEN an order is submitted AND inventory is available, THE Order System SHALL fulfill the order.
386
+ An order was submitted and inventory was available, but the order was not fulfilled.
387
+ ```
388
+
389
+ Exit status is `0` when the command is clean, `1` when a scenario violates a requirement, lint finds something, or a property has a counterexample, and `2` when the spec, the scenario, the property, or the arguments are wrong. `--json` is for the agent. The text report is for the product manager.
390
+
391
+ ## JSON
392
+
393
+ `--json` prints one object on stdout and leaves stderr empty. The keys below do not change. Wrong arguments, and a file that cannot be read or parsed, are exit 2:
394
+
395
+ <!-- verify stdout 2
396
+ build/plan-verify
397
+ emit-smt
398
+ --json
399
+ -->
400
+ ```text
401
+ {
402
+ "ok": false,
403
+ "error": "usage: plan-verify emit-smt [--json] <spec>"
404
+ }
405
+ ```
406
+
407
+ `check` is `ok`, `spec`, `scenario`, and `violations`. Each violation is `id`, `line`, `sentence`, and `because`. `ok` is true when `violations` is empty.
408
+
409
+ <!-- verify stdout 1
410
+ build/plan-verify
411
+ check
412
+ --json
413
+ examples/order.ears.md
414
+ --input
415
+ examples/order-fail.json
416
+ -->
417
+ ```text
418
+ {
419
+ "ok": false,
420
+ "spec": "examples/order.ears.md",
421
+ "scenario": "examples/order-fail.json",
422
+ "violations": [
423
+ {
424
+ "id": "R1",
425
+ "line": 7,
426
+ "sentence": "WHEN an order is submitted AND inventory is available, THE Order System SHALL fulfill the order.",
427
+ "because": "An order was submitted and inventory was available, but the order was not fulfilled."
428
+ }
429
+ ]
430
+ }
431
+ ```
432
+
433
+ `lint` is `ok`, `spec`, `conflicts`, `gaps`, `misuse`, `wording`, `redundant`, `duplicate`, `vacuous`, and `unused`. A conflict is `ids`, `minimal`, `everySituation`, `summary`, `clash`, `requirements` (`id`, `line`, `sentence`), `situation` and `example` (`name`, `value`), `requires` (`id`, `text`), `keeps`, `breaks`, and `fix`. A gap is `output`, `situation`, and `text`. Misuse is `id`, `line`, `symbols`, `sentence`, and `text`. Wording is `line`, `word`, `kind`, `text`, and `context`. `kind` is `vague`, `solution`, `form`, or `negation`. A redundant requirement is `id`, `line`, `sentence`, `text`, and `fix`. An exact duplicate is `id`, `line`, `sameAs`, `sentence`, `text`, and `fix`. A vacuous requirement has the same fields as a redundant requirement. An unused entry is `name`, `kind` (`state`, `input`, `output`, or `variant`), `text`, and `fix`.
434
+
435
+ <!-- verify stdout 1
436
+ build/plan-verify
437
+ lint
438
+ --json
439
+ examples/order-conflict.ears.md
440
+ -->
441
+ ```text
442
+ {
443
+ "ok": false,
444
+ "spec": "examples/order-conflict.ears.md",
445
+ "conflicts": [
446
+ {
447
+ "ids": [
448
+ "R2",
449
+ "R3"
450
+ ],
451
+ "minimal": true,
452
+ "everySituation": false,
453
+ "summary": "R2 and R3 cannot both hold.",
454
+ "clash": "They clash when an order was submitted.",
455
+ "requirements": [
456
+ {
457
+ "id": "R2",
458
+ "line": 7,
459
+ "sentence": "When an order is submitted, the Order System shall fulfill the order."
460
+ },
461
+ {
462
+ "id": "R3",
463
+ "line": 8,
464
+ "sentence": "When an order is submitted, the Order System shall not fulfill the order."
465
+ }
466
+ ],
467
+ "situation": [
468
+ {
469
+ "name": "order_submitted",
470
+ "value": "true"
471
+ }
472
+ ],
473
+ "requires": [
474
+ {
475
+ "id": "R2",
476
+ "text": "that the order was fulfilled"
477
+ },
478
+ {
479
+ "id": "R3",
480
+ "text": "that the order was not fulfilled"
481
+ }
482
+ ],
483
+ "example": [
484
+ {
485
+ "name": "order_submitted",
486
+ "value": "true"
487
+ },
488
+ {
489
+ "name": "order_fulfilled",
490
+ "value": "true"
491
+ }
492
+ ],
493
+ "keeps": [
494
+ "R2"
495
+ ],
496
+ "breaks": "R3",
497
+ "fix": "Suggested fix: ask which requirement should win. To keep R2, remove R3. To keep R3, remove R2.",
498
+ "repairs": [
499
+ {
500
+ "kind": "drop",
501
+ "id": "R2",
502
+ "text": "One repair is to drop R2 and keep R3 as written.",
503
+ "sentence": "When an order is submitted, the Order System shall not fulfill the order."
504
+ },
505
+ {
506
+ "kind": "drop",
507
+ "id": "R3",
508
+ "text": "One repair is to drop R3 and keep R2 as written.",
509
+ "sentence": "When an order is submitted, the Order System shall fulfill the order."
510
+ }
511
+ ]
512
+ }
513
+ ],
514
+ "gaps": [],
515
+ "misuse": [],
516
+ "wording": [],
517
+ "redundant": [],
518
+ "duplicate": [],
519
+ "vacuous": [],
520
+ "unused": []
521
+ }
522
+ ```
523
+
524
+ `policy` is `ok`, `spec`, `property`, `holds`, `note`, `acceptedFound`, `accepted`, `rejectedFound`, `rejected`, `counterexampleFound`, `counterexample`, and `breaks`. A scenario is a list of `name` and `value`. The list is empty when the matching `Found` flag is false. A break is `id`, `line`, `sentence`, and `because`.
525
+
526
+ <!-- verify stdout 1
527
+ build/plan-verify
528
+ policy
529
+ --json
530
+ examples/order.ears.md
531
+ --property
532
+ examples/order-always.ears.md
533
+ -->
534
+ ```text
535
+ {
536
+ "ok": false,
537
+ "spec": "examples/order.ears.md",
538
+ "property": "examples/order-always.ears.md",
539
+ "holds": false,
540
+ "note": "",
541
+ "acceptedFound": false,
542
+ "accepted": [],
543
+ "rejectedFound": false,
544
+ "rejected": [],
545
+ "counterexampleFound": true,
546
+ "counterexample": [
547
+ {
548
+ "name": "order_submitted",
549
+ "value": "false"
550
+ },
551
+ {
552
+ "name": "inventory_available",
553
+ "value": "false"
554
+ },
555
+ {
556
+ "name": "order_fulfilled",
557
+ "value": "false"
558
+ }
559
+ ],
560
+ "breaks": [
561
+ {
562
+ "id": "P1",
563
+ "line": 3,
564
+ "sentence": "The Order System shall fulfill the order.",
565
+ "because": "The order was not fulfilled."
566
+ }
567
+ ]
568
+ }
569
+ ```
570
+
571
+ `emit-smt` is `ok`, `spec`, and `smt`. `smt` is the same SMT-LIB text as the plain command, with a newline between lines and none after the last line.
572
+
573
+ <!-- verify stdout 0
574
+ build/plan-verify
575
+ emit-smt
576
+ --json
577
+ examples/order.ears.md
578
+ -->
579
+ ```text
580
+ {
581
+ "ok": true,
582
+ "spec": "examples/order.ears.md",
583
+ "smt": "; spec: examples/order.ears.md\n(set-logic ALL)\n; event order_submitted: Bool \"The customer submits an order.\" phrase \"an order is submitted\"\n(declare-fun order_submitted () Bool)\n; state inventory_available: Bool \"Enough stock is on hand to fill the order.\" phrase \"inventory is available\"\n(declare-fun inventory_available () Bool)\n; output order_fulfilled: Bool \"The order is fulfilled.\" phrase \"fulfill the order\" outcome \"the order was fulfilled\" / \"the order was not fulfilled\"\n(declare-fun order_fulfilled () Bool)\n; R1_L7 line 7: WHEN an order is submitted AND inventory is available, THE Order System SHALL fulfill the order.\n(assert (! (=> (and order_submitted inventory_available) order_fulfilled) :named R1_L7))"
584
+ }
585
+ ```
586
+
587
+ ## What EARS is
588
+
589
+ An EARS line is one testable response, with the condition in front of it:
590
+
591
+ ```text
592
+ While <state>, when <trigger>, if <condition>, where <feature>, then the <system> shall <response>.
593
+ ```
594
+
595
+ Each piece is optional. `then` belongs in the line exactly when `if` is there. The names come from declarations at the top of the spec, so the same idea cannot be spelled two ways:
596
+
597
+ ```text
598
+ event order_submitted: Bool "The customer submits an order." phrase "an order is submitted"
599
+ state inventory_available: Bool "Enough stock is on hand to fill the order." phrase "inventory is available"
600
+ input quantity: Int 1..10 "How many units the customer asked for." phrase "the quantity"
601
+ output order_fulfilled: Bool "The order is fulfilled." phrase "fulfill the order" outcome "the order was fulfilled" / "the order was not fulfilled"
602
+ unwanted event payment_declined: Bool "The payment was declined." phrase "payment fails"
603
+ ```
604
+
605
+ The phrase is the only wording a requirement may use for that name. `not fulfill the order` is the phrase with `not` in front. When the phrase contains `is`, write `the customer is not cancelling`. `the quantity is at most 10` still parses, and lint flags `shall the quantity is 10`. Bind the response with `as`, for example `as "accept {op} {value} units"`, and write `shall accept at most 10 units`. `says "{value} units were accepted"` is the sentence a report uses. A Bool can add `action "start the subscription"` next to `phrase "the subscription is started"`. Matching is exact and case-insensitive: the words have to be the phrase you declared, and the longest phrase wins. A sentence that says `an order was submitted` is not close enough to count. The error names the line and the nearest phrases.
606
+
607
+ A Bool can also say how a report states the result, after the phrase: `outcome "the order was fulfilled" / "the order was not fulfilled"`. One string is enough. The false form inserts `not` after the first `was`, `were`, or `is`, or prefixes `not` when none of those words is there. Two strings separated by `/` are used as written. `outcome` is only for Bool, and `not` is allowed in that wording. When it is left off, a phrase that contains `is` is reported with `was` or `was not`. The violation line for the order above is `An order was submitted and inventory was available, but the order was not fulfilled.`
608
+
609
+ <!-- verify stderr 2
610
+ build/plan-verify
611
+ lint
612
+ test/fixtures/unknown-phrase.ears.md
613
+ -->
614
+ ```text
615
+ error: test/fixtures/unknown-phrase.ears.md:5: unknown phrase "an order was submitted". Closest: "an order is submitted", "inventory is available", "fulfill the order".
616
+ ```
617
+
618
+ Scenarios and the SMT-LIB still use the declared names (`order_submitted`, `quantity`). The sentences you read, and the explanations under them, use the phrases and the outcome wording.
619
+
620
+ Events and states are booleans. Inputs and outputs are booleans, integers with an inclusive range, or enums. An unwanted event, such as a payment being declined, is written with `if` ... `then`. Writing it with `when` is a lint finding, because `when` reads like the normal trigger for the response.
621
+
622
+ A line with no condition is ubiquitous: the response is always required. `While` is a state that holds. `When` is the trigger. `If` ... `then` is the unwanted or exceptional case. `Where` is a feature or a variant, such as a region.
623
+
624
+ ## How a contradiction is found
625
+
626
+ Each requirement becomes an implication: when its condition is true, its response must be true. `check` plugs in the scenario you wrote. `lint` asks whether some situation has no response that satisfies the set.
627
+
628
+ Booleans are decided by DPLL over the clauses of those implications. Integers, and enum values numbered in the order you declared them, are linear arithmetic: bounds tighten until a comparison is forced, and the search branches where a bound or a comparison can change. A model is the lexicographically least assignment. The search tries `false` before `true`, enum values in declaration order, and integers from the low end of the range to the high end. The example scenario is that first assignment. Run it twice and you get the same bytes.
629
+
630
+ A set such as `{R2, R3}` is reported when some situation has no allowed response, even if other situations are fine. The set is minimal: drop any one requirement and the rest can be satisfied. With 12 requirements or fewer, every such set is listed, smallest first. With more than 12, each set is shrunk by deleting requirements from a failing situation, and situations are tried in that same order. Two sets that share a requirement and fail in exactly the same situations can collapse to one of them. If a driver domain is wider than 64 values, or the driver combinations exceed 8192, each set is removed before the next search, so a later set that needs a requirement from an earlier one is not reported.
631
+
632
+ An integer has to declare `min..max`, inside `-1000000..1000000`. The procedure decides the range. It does not list every value, so a wide range is still checked:
633
+
634
+ <!-- verify stdout 0
635
+ build/plan-verify
636
+ lint
637
+ examples/order-wide.ears.md
638
+ -->
639
+ ```text
640
+ spec: examples/order-wide.ears.md
641
+ conflicts: 0
642
+ gaps: 0
643
+ misuse: 0
644
+ wording: 0
645
+ redundant: 0
646
+ duplicate: 0
647
+ vacuous: 0
648
+ unused: 0
649
+ ```
650
+
651
+ A comparison that ties two wide integers together is still decided, by splitting the range. A boolean combination of those comparisons can take longer than a single bound.
652
+
653
+ There are no strings in the logic. Descriptions and phrases stay next to the names. Vague words (`as needed`, `quickly`, `appropriate`, `user-friendly`, `fast`, `slow`, `large`, `small`, `easy`, `robust`, `flexible`, `efficient`) and solution words (`database`, `api`, `algorithm`, `microservice`, `server`, `class`, `function`) are caught in descriptions and in requirement sentences. A word that sits inside a declared phrase or action is already bound to a schema item, so that use is not a wording finding.
654
+
655
+ Lint also reports an event no requirement handles, an error condition with no response, an unwanted event written with `when`, and vague words. An integer cap can clash with a larger accepted quantity, and an enum can be pinned to two variants at once. The fix still asks which requirement should win. A condition that can never be true, a state or input no requirement mentions, an enum variant no requirement mentions, and an output no response sets are reported on their own. An output no requirement mentions at all stays a gap. An event nobody handles stays a gap. A free input that some requirement already mentions is not a finding: the environment is allowed to choose it.
656
+
657
+ <!-- verify stdout 1
658
+ build/plan-verify
659
+ lint
660
+ examples/order-unhandled.ears.md
661
+ -->
662
+ ```text
663
+ spec: examples/order-unhandled.ears.md
664
+ conflicts: 0
665
+ gaps: 1
666
+ misuse: 0
667
+ wording: 0
668
+ redundant: 0
669
+ duplicate: 0
670
+ vacuous: 0
671
+ unused: 0
672
+
673
+ gap order_cancelled
674
+ No requirement handles "an order is cancelled". Suggested fix: add a requirement that says what the system shall do, or remove the event.
675
+ ```
676
+
677
+ <!-- verify stdout 1
678
+ build/plan-verify
679
+ lint
680
+ examples/order-unanswered.ears.md
681
+ -->
682
+ ```text
683
+ spec: examples/order-unanswered.ears.md
684
+ conflicts: 0
685
+ gaps: 1
686
+ misuse: 0
687
+ wording: 0
688
+ redundant: 0
689
+ duplicate: 0
690
+ vacuous: 0
691
+ unused: 0
692
+
693
+ gap payment_declined
694
+ No requirement states a response when "payment fails". Suggested fix: add an if ... then requirement, and ask what the system shall do. Do not choose the response.
695
+ ```
696
+
697
+ <!-- verify stdout 1
698
+ build/plan-verify
699
+ lint
700
+ examples/order-misuse.ears.md
701
+ -->
702
+ ```text
703
+ spec: examples/order-misuse.ears.md
704
+ conflicts: 0
705
+ gaps: 0
706
+ misuse: 1
707
+ wording: 0
708
+ redundant: 1
709
+ duplicate: 0
710
+ vacuous: 0
711
+ unused: 0
712
+
713
+ misuse R4 line 6
714
+ "payment fails" is an unwanted event. Write if ... then, not when.
715
+ When payment fails, the Order System shall reject the order.
716
+
717
+ redundant R4 line 6
718
+ R4 adds nothing. Whenever the other requirements hold, R4 holds too.
719
+ When payment fails, the Order System shall reject the order.
720
+ Suggested fix: remove R4, or ask whether it was meant to say something the others do not already require. Do not delete it until the PM agrees.
721
+ ```
722
+
723
+ <!-- verify stdout 1
724
+ build/plan-verify
725
+ lint
726
+ examples/order-vague.ears.md
727
+ -->
728
+ ```text
729
+ spec: examples/order-vague.ears.md
730
+ conflicts: 0
731
+ gaps: 0
732
+ misuse: 0
733
+ wording: 3
734
+ redundant: 0
735
+ duplicate: 0
736
+ vacuous: 0
737
+ unused: 0
738
+
739
+ wording line 4
740
+ "fast" is too vague to test. Replace it with something a scenario can check.
741
+ output order_fulfilled: Bool "The checkout is fast, appropriate, and user-friendly." phrase "fulfill the order" outcome "the order was fulfilled" / "the order was not fulfilled"
742
+
743
+ wording line 4
744
+ "appropriate" is too vague to test. Replace it with something a scenario can check.
745
+ output order_fulfilled: Bool "The checkout is fast, appropriate, and user-friendly." phrase "fulfill the order" outcome "the order was fulfilled" / "the order was not fulfilled"
746
+
747
+ wording line 4
748
+ "user-friendly" is too vague to test. Replace it with something a scenario can check.
749
+ output order_fulfilled: Bool "The checkout is fast, appropriate, and user-friendly." phrase "fulfill the order" outcome "the order was fulfilled" / "the order was not fulfilled"
750
+ ```
751
+
752
+ ## A word that is not bound
753
+
754
+ `examples/order-unmeasured.ears.md` says "as needed" in a description. That phrase is not a schema item. The requirements also say "respond quickly", and that phrase is declared, so "quickly" is bound and is not a finding.
755
+
756
+ <!-- verify stdout 1
757
+ build/plan-verify
758
+ lint
759
+ examples/order-unmeasured.ears.md
760
+ -->
761
+ ```text
762
+ spec: examples/order-unmeasured.ears.md
763
+ conflicts: 0
764
+ gaps: 0
765
+ misuse: 0
766
+ wording: 1
767
+ redundant: 0
768
+ duplicate: 0
769
+ vacuous: 0
770
+ unused: 0
771
+
772
+ wording line 4
773
+ "as needed" is too vague to test. Replace it with something a scenario can check.
774
+ output order_fulfilled: Bool "Handle it as needed." phrase "fulfill the order" outcome "the order was fulfilled" / "the order was not fulfilled"
775
+ ```
776
+
777
+ <!-- verify stdout 1
778
+ build/plan-verify
779
+ lint
780
+ examples/order-quantity.ears.md
781
+ -->
782
+ ```text
783
+ spec: examples/order-quantity.ears.md
784
+ conflicts: 1
785
+ gaps: 0
786
+ misuse: 0
787
+ wording: 0
788
+ redundant: 0
789
+ duplicate: 0
790
+ vacuous: 0
791
+ unused: 0
792
+
793
+ conflict {R4, R5}
794
+ R4 and R5 cannot both hold.
795
+
796
+ R4 line 7
797
+ When an order is submitted, the Order System shall accept at most 10 units.
798
+
799
+ R5 line 8
800
+ When an order is submitted, the Order System shall accept 12 units.
801
+
802
+ They clash when an order was submitted.
803
+ R4 requires that at most 10 units were accepted.
804
+ R5 requires that 12 units were accepted.
805
+
806
+ Example scenario:
807
+ {
808
+ "order_submitted": true,
809
+ "quantity": 0
810
+ }
811
+ This scenario keeps R4 and breaks R5.
812
+
813
+ Suggested fix: ask which requirement should win. To keep R4, remove R5. To keep R5, remove R4.
814
+
815
+ Candidate repairs follow. This report does not choose one.
816
+
817
+ repair drop R4
818
+ One repair is to drop R4 and keep R5 as written.
819
+ When an order is submitted, the Order System shall accept 12 units.
820
+
821
+ repair drop R5
822
+ One repair is to drop R5 and keep R4 as written.
823
+ When an order is submitted, the Order System shall accept at most 10 units.
824
+ ```
825
+
826
+ R4 says a submitted order accepts at most 10 units. R5 says that same order accepts exactly 12. The example keeps R4 with a quantity of 0 only because that is the first value that satisfies R4. It is a witness, and it is not a recommendation.
827
+
828
+ `examples/order-status.ears.md` requires the status to be shipped and cancelled in every situation. `placed` is declared on the enum and no requirement mentions it.
829
+
830
+ <!-- verify stdout 1
831
+ build/plan-verify
832
+ lint
833
+ examples/order-status.ears.md
834
+ -->
835
+ ```text
836
+ spec: examples/order-status.ears.md
837
+ conflicts: 1
838
+ gaps: 0
839
+ misuse: 0
840
+ wording: 0
841
+ redundant: 0
842
+ duplicate: 0
843
+ vacuous: 0
844
+ unused: 1
845
+
846
+ conflict {R6, R7}
847
+ R6 and R7 cannot both hold.
848
+
849
+ R6 line 6
850
+ The Order System shall mark the order shipped.
851
+
852
+ R7 line 7
853
+ The Order System shall mark the order cancelled.
854
+
855
+ They clash in every situation.
856
+ R6 requires that the order was shipped.
857
+ R7 requires that the order was cancelled.
858
+
859
+ Example scenario:
860
+ {
861
+ "status": "shipped"
862
+ }
863
+ This scenario keeps R6 and breaks R7.
864
+
865
+ Suggested fix: ask which requirement should win. To keep R6, remove R7. To keep R7, remove R6.
866
+
867
+ Candidate repairs follow. This report does not choose one.
868
+
869
+ repair drop R6
870
+ One repair is to drop R6 and keep R7 as written.
871
+ The Order System shall mark the order cancelled.
872
+
873
+ repair drop R7
874
+ One repair is to drop R7 and keep R6 as written.
875
+ The Order System shall mark the order shipped.
876
+
877
+ unused Status.placed
878
+ Status.placed is never mentioned.
879
+ Suggested fix: use placed in a requirement, or remove it from Status.
880
+ ```
881
+
882
+ ## From a plan to the problems
883
+
884
+ [examples/plan-to-problems/README.md](examples/plan-to-problems/README.md) walks the whole path. A plain product plan goes in. A plain list of logical problems comes out. The live step that drafts the spec calls a model. The report step does not. The offline test uses the spec already in the folder and does not need a key.
885
+
886
+ ## Where this comes from
887
+
888
+ The shape of the analysis follows Kiro's requirements analysis: separate behavior from design, and look for what the requirements refuse to allow.
889
+
890
+ - [Deep spec analysis](https://kiro.dev/blog/deep-spec-analysis/)
891
+ - [Analyze requirements](https://kiro.dev/docs/specs/analyze-requirements/)
892
+
893
+ ## For the agent in the conversation
894
+
895
+ The skill at [`skills/plan-verify/SKILL.md`](skills/plan-verify/SKILL.md) tells an agent how to sit with a product manager. It lists the grammar the parser accepts, shows real `check`, `lint`, `report`, `scenarios`, `emit-smt`, `policy`, and `help` output, including the sentences a product manager would read, and says to ask which requirement should win.
896
+
897
+ ## Questions
898
+
899
+ **Why our own solver?** The decision procedure is in this repository: DPLL for the boolean structure, and bounds for linear integer arithmetic. There is no Z3, cvc5, or yices to install, and the Node run and the native binary are tested to print the same bytes.
900
+
901
+ **Why ScriptC?** One TypeScript program is the product. ScriptC 0.4.1 compiles that program to a native binary, and `scriptc coverage` stays fully static. Node 24 runs the same files for day-to-day edits.
902
+
903
+ **Why do integers need a range?** The arithmetic is bounded. A declared range, inside `-1000000..1000000`, keeps every integer finite. The solver does not walk that range one value at a time.
904
+
905
+ **Will it pick a winner?** It will show who clashes, the situation, and an example that breaks one requirement in the set. The fix says to ask which requirement should win.
906
+
907
+ ## Policy
908
+
909
+ A property is a separate file of requirement lines. It uses the spec's names and phrases, and it does not declare symbols. `P1` is the usual id.
910
+
911
+ `examples/order-always.ears.md` hopes the order is fulfilled in every scenario. The requirements do not say that. The counterexample is a scenario they allow where the order is not fulfilled:
912
+
913
+ <!-- verify stdout 1
914
+ build/plan-verify
915
+ policy
916
+ examples/order.ears.md
917
+ --property
918
+ examples/order-always.ears.md
919
+ -->
920
+ ```text
921
+ spec: examples/order.ears.md
922
+ property: examples/order-always.ears.md
923
+ counterexample:
924
+ {
925
+ "order_submitted": false,
926
+ "inventory_available": false,
927
+ "order_fulfilled": false
928
+ }
929
+ This scenario satisfies every requirement and violates the property.
930
+
931
+ P1 line 3
932
+ The Order System shall fulfill the order.
933
+ The order was not fulfilled.
934
+ ```
935
+
936
+ `examples/order-same.ears.md` repeats R1. Nothing the requirements allow can break it:
937
+
938
+ <!-- verify stdout 0
939
+ build/plan-verify
940
+ policy
941
+ examples/order.ears.md
942
+ --property
943
+ examples/order-same.ears.md
944
+ -->
945
+ ```text
946
+ spec: examples/order.ears.md
947
+ property: examples/order-same.ears.md
948
+ No scenario satisfies every requirement and violates the property.
949
+ ```
950
+
951
+ When the requirements themselves cannot all hold, there is no scenario to test the property against. `examples/order-status.ears.md` is that spec: the status would have to be shipped and cancelled.
952
+
953
+ <!-- verify stdout 0
954
+ build/plan-verify
955
+ policy
956
+ examples/order-status.ears.md
957
+ --property
958
+ examples/status-placed.ears.md
959
+ -->
960
+ ```text
961
+ spec: examples/order-status.ears.md
962
+ property: examples/status-placed.ears.md
963
+ No scenario satisfies every requirement, so none violates the property.
964
+ ```
965
+
966
+ ## Three requirements, no pair
967
+
968
+ `examples/order-chain.ears.md` is unsatisfiable only as a set. A submitted order must be accepted, an accepted order must be shipped, and a submitted order must not be shipped. Each pair can still hold. The example keeps the first two and breaks the third. It is a witness, and it is not a recommendation. Ask which requirement should change.
969
+
970
+ <!-- verify stdout 1
971
+ build/plan-verify
972
+ lint
973
+ examples/order-chain.ears.md
974
+ -->
975
+ ```text
976
+ spec: examples/order-chain.ears.md
977
+ conflicts: 1
978
+ gaps: 0
979
+ misuse: 0
980
+ wording: 0
981
+ redundant: 0
982
+ duplicate: 0
983
+ vacuous: 0
984
+ unused: 0
985
+
986
+ conflict {R1, R2, R3}
987
+ R1, R2, and R3 cannot all hold.
988
+
989
+ R1 line 7
990
+ When an order is submitted, the Order System shall accept the order.
991
+
992
+ R2 line 8
993
+ When the order is accepted, the Order System shall ship the order.
994
+
995
+ R3 line 9
996
+ When an order is submitted, the Order System shall not ship the order.
997
+
998
+ They clash when an order was submitted.
999
+ R1 requires that the order was accepted.
1000
+ R3 requires that the order was not shipped.
1001
+
1002
+ Example scenario:
1003
+ {
1004
+ "order_submitted": true,
1005
+ "order_accepted": true,
1006
+ "order_shipped": true
1007
+ }
1008
+ This scenario keeps R1 and R2 and breaks R3.
1009
+
1010
+ Suggested fix: ask which requirement should win. Removing any one of R1, R2, and R3 leaves the rest consistent.
1011
+
1012
+ Candidate repairs follow. This report does not choose one.
1013
+
1014
+ repair drop R1
1015
+ One repair is to drop R1 and keep R2 and R3 as written.
1016
+ When the order is accepted, the Order System shall ship the order.
1017
+ When an order is submitted, the Order System shall not ship the order.
1018
+
1019
+ repair drop R2
1020
+ One repair is to drop R2 and keep R1 and R3 as written.
1021
+ When an order is submitted, the Order System shall accept the order.
1022
+ When an order is submitted, the Order System shall not ship the order.
1023
+
1024
+ repair drop R3
1025
+ One repair is to drop R3 and keep R1 and R2 as written.
1026
+ When an order is submitted, the Order System shall accept the order.
1027
+ When the order is accepted, the Order System shall ship the order.
1028
+
1029
+ repair strengthen R2
1030
+ One repair is to add "and an order is not submitted" to the condition of R2. R1 and R3 stay as written.
1031
+ When the order is accepted and an order is not submitted, the Order System shall ship the order.
1032
+ ```
1033
+
1034
+ ## A requirement that adds nothing
1035
+
1036
+ `examples/order-redundant.ears.md` says a submitted order is fulfilled, and also that a submitted order with inventory is fulfilled. The second one is already required by the first. Lint names it and asks. It does not delete it. A conflict does not hide a repeat that sits outside that conflict. Lint checks redundancy on the requirements that are not part of any conflict.
1037
+
1038
+ <!-- verify stdout 1
1039
+ build/plan-verify
1040
+ lint
1041
+ examples/order-redundant.ears.md
1042
+ -->
1043
+ ```text
1044
+ spec: examples/order-redundant.ears.md
1045
+ conflicts: 0
1046
+ gaps: 0
1047
+ misuse: 0
1048
+ wording: 0
1049
+ redundant: 1
1050
+ duplicate: 0
1051
+ vacuous: 0
1052
+ unused: 0
1053
+
1054
+ redundant R2 line 8
1055
+ R2 adds nothing. Whenever the other requirements hold, R2 holds too.
1056
+ When an order is submitted and inventory is available, the Order System shall fulfill the order.
1057
+ Suggested fix: remove R2, or ask whether it was meant to say something the others do not already require. Do not delete it until the PM agrees.
1058
+ ```
1059
+
1060
+ ## A repeat next to a conflict
1061
+
1062
+ `examples/order-repeat.ears.md` conflicts on whether a submitted order is fulfilled. Separately, closing a submitted order already requires closing it when inventory is available. `R5` is not part of the conflict, so lint still names it.
1063
+
1064
+ <!-- verify stdout 1
1065
+ build/plan-verify
1066
+ lint
1067
+ examples/order-repeat.ears.md
1068
+ -->
1069
+ ```text
1070
+ spec: examples/order-repeat.ears.md
1071
+ conflicts: 1
1072
+ gaps: 0
1073
+ misuse: 0
1074
+ wording: 0
1075
+ redundant: 1
1076
+ duplicate: 0
1077
+ vacuous: 0
1078
+ unused: 0
1079
+
1080
+ conflict {R1, R2}
1081
+ R1 and R2 cannot both hold.
1082
+
1083
+ R1 line 8
1084
+ When an order is submitted, the Order System shall fulfill the order.
1085
+
1086
+ R2 line 9
1087
+ When an order is submitted, the Order System shall not fulfill the order.
1088
+
1089
+ They clash when an order was submitted and inventory was not available.
1090
+ R1 requires that the order was fulfilled.
1091
+ R2 requires that the order was not fulfilled.
1092
+
1093
+ Example scenario:
1094
+ {
1095
+ "order_submitted": true,
1096
+ "inventory_available": false,
1097
+ "order_fulfilled": true,
1098
+ "order_closed": false
1099
+ }
1100
+ This scenario keeps R1 and breaks R2.
1101
+
1102
+ Suggested fix: ask which requirement should win. To keep R1, remove R2. To keep R2, remove R1.
1103
+
1104
+ Candidate repairs follow. This report does not choose one.
1105
+
1106
+ repair drop R1
1107
+ One repair is to drop R1 and keep R2 as written.
1108
+ When an order is submitted, the Order System shall not fulfill the order.
1109
+
1110
+ repair drop R2
1111
+ One repair is to drop R2 and keep R1 as written.
1112
+ When an order is submitted, the Order System shall fulfill the order.
1113
+
1114
+ redundant R5 line 12
1115
+ R5 adds nothing. Whenever the requirements outside a conflict hold, R5 holds too.
1116
+ When an order is submitted and inventory is available, the Order System shall close the order.
1117
+ Suggested fix: remove R5, or ask whether it was meant to say something the others do not already require. Do not delete it until the PM agrees.
1118
+ ```
1119
+
1120
+ ## The same condition and the same response
1121
+
1122
+ `examples/order-duplicate.ears.md` states the fulfilled-order rule twice. That is a duplicate, not a redundant requirement: the two lines are the same condition and the same response. Lint names the later line and the earlier id. It does not call it redundant.
1123
+
1124
+ <!-- verify stdout 1
1125
+ build/plan-verify
1126
+ lint
1127
+ examples/order-duplicate.ears.md
1128
+ -->
1129
+ ```text
1130
+ spec: examples/order-duplicate.ears.md
1131
+ conflicts: 0
1132
+ gaps: 0
1133
+ misuse: 0
1134
+ wording: 0
1135
+ redundant: 0
1136
+ duplicate: 1
1137
+ vacuous: 0
1138
+ unused: 0
1139
+
1140
+ duplicate R2 line 7
1141
+ R2 duplicates R1. The condition and the response are the same.
1142
+ When an order is submitted, the Order System shall fulfill the order.
1143
+ Suggested fix: remove R2, or ask whether it was meant to differ from R1. Do not delete it until the PM agrees.
1144
+ ```
1145
+
1146
+ <!-- verify stdout 1
1147
+ build/plan-verify
1148
+ report
1149
+ examples/order-duplicate.ears.md
1150
+ -->
1151
+ ```text
1152
+ # Logical problems
1153
+
1154
+ Spec: examples/order-duplicate.ears.md
1155
+
1156
+ These are the logical problems in the requirements. For a contradiction, which requirement should win? This report does not choose.
1157
+
1158
+ ## 1. R2 duplicates R1
1159
+
1160
+ R2 line 7
1161
+ "When an order is submitted, the Order System shall fulfill the order."
1162
+
1163
+ R2 duplicates R1. The condition and the response are the same.
1164
+
1165
+ R1 line 6
1166
+ "When an order is submitted, the Order System shall fulfill the order."
1167
+
1168
+ Suggested fix: remove R2, or ask whether it was meant to differ from R1. Do not delete it until the PM agrees.
1169
+ ```
1170
+
1171
+ ## A trigger that starts with not
1172
+
1173
+ `examples/order-negation.ears.md` says `When not a member asks`. The trigger is a bare negation. Lint asks for a positive condition, such as `When a non-member asks`, or a positive event. `is not` stays the normal way to negate a phrase that contains `is`.
1174
+
1175
+ <!-- verify stdout 1
1176
+ build/plan-verify
1177
+ lint
1178
+ examples/order-negation.ears.md
1179
+ -->
1180
+ ```text
1181
+ spec: examples/order-negation.ears.md
1182
+ conflicts: 0
1183
+ gaps: 0
1184
+ misuse: 0
1185
+ wording: 1
1186
+ redundant: 0
1187
+ duplicate: 0
1188
+ vacuous: 0
1189
+ unused: 0
1190
+
1191
+ wording line 6
1192
+ The trigger is a bare negation. Write a positive condition, such as "When a non-member asks", or declare a positive event.
1193
+ When not a member asks to take a copy home, the Desk shall not allow the loan.
1194
+ ```
1195
+
1196
+ ## A condition that can never be true
1197
+
1198
+ `examples/order-vacuous.ears.md` has a condition that contradicts itself, and a quantity comparison outside the declared range. The response is never demanded. Lint names each one as vacuous. It does not also call that requirement redundant or a duplicate, because a condition that can never be true is implied by anything. The vacuous line says to rewrite the condition. It does not pick a response.
1199
+
1200
+ <!-- verify stdout 1
1201
+ build/plan-verify
1202
+ lint
1203
+ examples/order-vacuous.ears.md
1204
+ -->
1205
+ ```text
1206
+ spec: examples/order-vacuous.ears.md
1207
+ conflicts: 0
1208
+ gaps: 0
1209
+ misuse: 0
1210
+ wording: 0
1211
+ redundant: 0
1212
+ duplicate: 0
1213
+ vacuous: 2
1214
+ unused: 0
1215
+
1216
+ vacuous R1 line 7
1217
+ R1 can never trigger. Its condition is impossible for every allowed value.
1218
+ When the cart is open and the cart is not open, the Checkout shall accept the order.
1219
+ Suggested fix: rewrite the condition in R1 so a real situation can make it true, or remove the requirement. Do not invent a response.
1220
+
1221
+ vacuous R2 line 8
1222
+ R2 can never trigger. Its condition is impossible for every allowed value.
1223
+ When the quantity is at least 9, the Checkout shall accept the order.
1224
+ Suggested fix: rewrite the condition in R2 so a real situation can make it true, or remove the requirement. Do not invent a response.
1225
+ ```
1226
+
1227
+ ## Schema the requirements never use
1228
+
1229
+ `examples/order-unused.ears.md` declares a state and an input that no sentence mentions, an enum variant that no sentence mentions, and an output that requirements only read. An output that no requirement mentions at all is still a gap, not a second finding. An event that nobody handles stays a gap.
1230
+
1231
+ <!-- verify stdout 1
1232
+ build/plan-verify
1233
+ lint
1234
+ examples/order-unused.ears.md
1235
+ -->
1236
+ ```text
1237
+ spec: examples/order-unused.ears.md
1238
+ conflicts: 0
1239
+ gaps: 0
1240
+ misuse: 0
1241
+ wording: 0
1242
+ redundant: 0
1243
+ duplicate: 0
1244
+ vacuous: 0
1245
+ unused: 4
1246
+
1247
+ unused loyalty
1248
+ loyalty ("the customer is loyal") is a state and no requirement mentions it.
1249
+ Suggested fix: use loyalty in a requirement, or remove it from the schema.
1250
+
1251
+ unused region
1252
+ region ("the region") is an input and no requirement mentions it.
1253
+ Suggested fix: use region in a requirement, or remove it from the schema.
1254
+
1255
+ unused receipt_sent
1256
+ Nothing sets receipt_sent ("a receipt is sent"). Requirements mention it, but no response says what it shall be.
1257
+ Suggested fix: add a shall requirement that sets receipt_sent, or declare it as an input if the environment provides it. Do not choose the value.
1258
+
1259
+ unused Lane.refund
1260
+ Lane.refund is never mentioned.
1261
+ Suggested fix: use refund in a requirement, or remove it from Lane.
1262
+ ```
1263
+
1264
+ <!-- verify stdout 1
1265
+ build/plan-verify
1266
+ lint
1267
+ --json
1268
+ examples/order-unused.ears.md
1269
+ -->
1270
+ ```text
1271
+ {
1272
+ "ok": false,
1273
+ "spec": "examples/order-unused.ears.md",
1274
+ "conflicts": [],
1275
+ "gaps": [],
1276
+ "misuse": [],
1277
+ "wording": [],
1278
+ "redundant": [],
1279
+ "duplicate": [],
1280
+ "vacuous": [],
1281
+ "unused": [
1282
+ {
1283
+ "name": "loyalty",
1284
+ "kind": "state",
1285
+ "text": "loyalty (\"the customer is loyal\") is a state and no requirement mentions it.",
1286
+ "fix": "Suggested fix: use loyalty in a requirement, or remove it from the schema."
1287
+ },
1288
+ {
1289
+ "name": "region",
1290
+ "kind": "input",
1291
+ "text": "region (\"the region\") is an input and no requirement mentions it.",
1292
+ "fix": "Suggested fix: use region in a requirement, or remove it from the schema."
1293
+ },
1294
+ {
1295
+ "name": "receipt_sent",
1296
+ "kind": "output",
1297
+ "text": "Nothing sets receipt_sent (\"a receipt is sent\"). Requirements mention it, but no response says what it shall be.",
1298
+ "fix": "Suggested fix: add a shall requirement that sets receipt_sent, or declare it as an input if the environment provides it. Do not choose the value."
1299
+ },
1300
+ {
1301
+ "name": "Lane.refund",
1302
+ "kind": "variant",
1303
+ "text": "Lane.refund is never mentioned.",
1304
+ "fix": "Suggested fix: use refund in a requirement, or remove it from Lane."
1305
+ }
1306
+ ]
1307
+ }
1308
+ ```
1309
+
1310
+ ## Scenarios
1311
+
1312
+ `scenarios` lists a small set of examples so a product manager can check that a requirement means what they think. For each requirement it looks for an accepted scenario with the trigger true, an accepted scenario with the trigger false, and a rejected scenario with the trigger true. Each one is the first assignment the search finds, false before true. A requirement with no trigger says so, instead of inventing a trigger-false case. The same assignment is printed once.
1313
+
1314
+ <!-- verify stdout 0
1315
+ build/plan-verify
1316
+ scenarios
1317
+ examples/order.ears.md
1318
+ -->
1319
+ ```text
1320
+ spec: examples/order.ears.md
1321
+ scenarios: 3
1322
+
1323
+ R1 accepted, trigger true
1324
+ {
1325
+ "order_submitted": true,
1326
+ "inventory_available": true,
1327
+ "order_fulfilled": true
1328
+ }
1329
+ An order was submitted and inventory was available, and the order was fulfilled.
1330
+
1331
+ R1 accepted, trigger false
1332
+ {
1333
+ "order_submitted": false,
1334
+ "inventory_available": false,
1335
+ "order_fulfilled": false
1336
+ }
1337
+ An order was not submitted and inventory was not available, and the order was not fulfilled.
1338
+
1339
+ R1 rejected, trigger true
1340
+ {
1341
+ "order_submitted": true,
1342
+ "inventory_available": true,
1343
+ "order_fulfilled": false
1344
+ }
1345
+ An order was submitted and inventory was available, but the order was not fulfilled.
1346
+ ```
1347
+
1348
+ `--json` is `ok`, `spec`, and `scenarios`. Each row is `id`, `line`, `kind` (`accepted` or `rejected`), `trigger` (`true` or `false`), `found`, `sameAs`, `scenario` (`name` and `value`), and `text`. `scenario` is empty when `found` is false. `sameAs` names an earlier row when the assignment was already listed.
1349
+
1350
+ <!-- verify stdout 0
1351
+ build/plan-verify
1352
+ scenarios
1353
+ --json
1354
+ examples/order.ears.md
1355
+ -->
1356
+ ```text
1357
+ {
1358
+ "ok": true,
1359
+ "spec": "examples/order.ears.md",
1360
+ "scenarios": [
1361
+ {
1362
+ "id": "R1",
1363
+ "line": 7,
1364
+ "kind": "accepted",
1365
+ "trigger": "true",
1366
+ "found": true,
1367
+ "sameAs": "",
1368
+ "scenario": [
1369
+ {
1370
+ "name": "order_submitted",
1371
+ "value": "true"
1372
+ },
1373
+ {
1374
+ "name": "inventory_available",
1375
+ "value": "true"
1376
+ },
1377
+ {
1378
+ "name": "order_fulfilled",
1379
+ "value": "true"
1380
+ }
1381
+ ],
1382
+ "text": "An order was submitted and inventory was available, and the order was fulfilled."
1383
+ },
1384
+ {
1385
+ "id": "R1",
1386
+ "line": 7,
1387
+ "kind": "accepted",
1388
+ "trigger": "false",
1389
+ "found": true,
1390
+ "sameAs": "",
1391
+ "scenario": [
1392
+ {
1393
+ "name": "order_submitted",
1394
+ "value": "false"
1395
+ },
1396
+ {
1397
+ "name": "inventory_available",
1398
+ "value": "false"
1399
+ },
1400
+ {
1401
+ "name": "order_fulfilled",
1402
+ "value": "false"
1403
+ }
1404
+ ],
1405
+ "text": "An order was not submitted and inventory was not available, and the order was not fulfilled."
1406
+ },
1407
+ {
1408
+ "id": "R1",
1409
+ "line": 7,
1410
+ "kind": "rejected",
1411
+ "trigger": "true",
1412
+ "found": true,
1413
+ "sameAs": "",
1414
+ "scenario": [
1415
+ {
1416
+ "name": "order_submitted",
1417
+ "value": "true"
1418
+ },
1419
+ {
1420
+ "name": "inventory_available",
1421
+ "value": "true"
1422
+ },
1423
+ {
1424
+ "name": "order_fulfilled",
1425
+ "value": "false"
1426
+ }
1427
+ ],
1428
+ "text": "An order was submitted and inventory was available, but the order was not fulfilled."
1429
+ }
1430
+ ]
1431
+ }
1432
+ ```