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/src/check.ts ADDED
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Evaluate one fully assigned scenario. This does not search.
3
+ * A requirement fails when its condition holds and its response does not.
4
+ */
5
+
6
+ import type { JsonObject, JsonValue } from "./jsonvalue.ts";
7
+ import { describeAssignment, displayCode, literalShown, reportBool, speakBound, type Requirement, type Spec, type Term } from "./spec.ts";
8
+ import { Binding, evalAssertion, evalBool, formatTerm, mentionedIndexes, T_FALSE, T_TRUE, T_UNKNOWN } from "./term.ts";
9
+ import { capitalize, displayQuote, englishList, formatInt, pastVoice } from "./text.ts";
10
+
11
+ export interface Violation {
12
+ id: string;
13
+ line: number;
14
+ sentence: string;
15
+ because: string;
16
+ }
17
+
18
+ export type BindResult = { ok: true; env: Binding } | { ok: false; message: string };
19
+
20
+ export function bindScenario(spec: Spec, object: JsonObject): BindResult {
21
+ const env = new Binding(spec.symbols.length);
22
+ const used: boolean[] = [];
23
+ for (let i = 0; i < object.keys.length; i++) used.push(false);
24
+ for (let s = 0; s < spec.symbols.length; s++) {
25
+ const sym = spec.symbols[s];
26
+ let found = -1;
27
+ for (let k = 0; k < object.keys.length; k++) {
28
+ if (object.keys[k] === sym.name) found = k;
29
+ }
30
+ if (found < 0) return { ok: false, message: "missing " + sym.name };
31
+ used[found] = true;
32
+ const value = object.values[found];
33
+ const bound = bindValue(spec, s, value);
34
+ if (!bound.ok) return bound;
35
+ env.known[s] = true;
36
+ env.code[s] = bound.code;
37
+ }
38
+ for (let k = 0; k < object.keys.length; k++) {
39
+ if (!used[k]) return { ok: false, message: "unexpected " + object.keys[k] };
40
+ }
41
+ return { ok: true, env: env };
42
+ }
43
+
44
+ function bindValue(spec: Spec, index: number, value: JsonValue): { ok: true; code: number } | { ok: false; message: string } {
45
+ const sym = spec.symbols[index];
46
+ if (sym.sort.kind === "bool") {
47
+ if (value.kind !== "bool") return { ok: false, message: sym.name + " must be true or false" };
48
+ return { ok: true, code: value.value ? 1 : 0 };
49
+ }
50
+ if (sym.sort.kind === "int") {
51
+ if (value.kind !== "int") {
52
+ return { ok: false, message: sym.name + " must be an integer from " + formatInt(sym.sort.min) + " to " + formatInt(sym.sort.max) };
53
+ }
54
+ if (value.value < sym.sort.min || value.value > sym.sort.max) {
55
+ return {
56
+ ok: false,
57
+ message: sym.name + " is " + formatInt(value.value) + ", outside " + formatInt(sym.sort.min) + ".." + formatInt(sym.sort.max),
58
+ };
59
+ }
60
+ return { ok: true, code: value.value };
61
+ }
62
+ const decl = spec.enums;
63
+ let variants: string[] = [];
64
+ for (let i = 0; i < decl.length; i++) {
65
+ if (decl[i].name === sym.sort.enumName) variants = decl[i].variants;
66
+ }
67
+ let expected = "";
68
+ for (let i = 0; i < variants.length; i++) {
69
+ if (i > 0) expected += " or ";
70
+ expected += variants[i];
71
+ }
72
+ if (value.kind !== "string") return { ok: false, message: sym.name + " must be " + expected };
73
+ for (let i = 0; i < variants.length; i++) {
74
+ if (variants[i] === value.value) return { ok: true, code: i };
75
+ }
76
+ return { ok: false, message: sym.name + " is " + displayQuote(value.value) + ", expected " + expected };
77
+ }
78
+
79
+ export function evaluate(spec: Spec, env: Binding): Violation[] {
80
+ return evaluateRequirements(spec, spec.requirements, env);
81
+ }
82
+
83
+ export function evaluateRequirements(spec: Spec, requirements: Requirement[], env: Binding): Violation[] {
84
+ const violations: Violation[] = [];
85
+ for (let i = 0; i < requirements.length; i++) {
86
+ const req = requirements[i];
87
+ const result = evalAssertion(req.antecedent, req.consequent, spec, env);
88
+ if (result === T_FALSE) {
89
+ violations.push({
90
+ id: req.id,
91
+ line: req.line,
92
+ sentence: req.sentence,
93
+ because: because(spec, env, req.antecedent, req.consequent),
94
+ });
95
+ } else if (result === T_UNKNOWN) {
96
+ violations.push({
97
+ id: req.id,
98
+ line: req.line,
99
+ sentence: req.sentence,
100
+ because: "could not decide this requirement from the scenario.",
101
+ });
102
+ }
103
+ }
104
+ return violations;
105
+ }
106
+
107
+ export function explainHold(spec: Spec, env: Binding, antecedent: Term | null, consequent: Term): string {
108
+ return narrate(spec, env, antecedent, consequent, "and");
109
+ }
110
+
111
+ function because(spec: Spec, env: Binding, antecedent: Term | null, consequent: Term): string {
112
+ return narrate(spec, env, antecedent, consequent, "but");
113
+ }
114
+
115
+ function narrate(spec: Spec, env: Binding, antecedent: Term | null, consequent: Term, join: string): string {
116
+ const indexes = mentionedIndexes(antecedent, consequent);
117
+ if (indexes.length === 0) return "the response is false.";
118
+ if (!usesVoice(spec, indexes)) {
119
+ const parts: string[] = [];
120
+ for (let i = 0; i < indexes.length; i++) {
121
+ const sym = spec.symbols[indexes[i]];
122
+ const code = env.code[indexes[i]];
123
+ parts.push(describeAssignment(sym, displayCode(spec, sym, code), code === 1));
124
+ }
125
+ return englishList(parts) + ".";
126
+ }
127
+ const held: string[] = [];
128
+ if (antecedent !== null) collectHeld(spec, env, antecedent, held);
129
+ const actual = describeActual(spec, env, consequent);
130
+ if (held.length === 0) return capitalize(actual) + ".";
131
+ return capitalize(englishList(held)) + ", " + join + " " + actual + ".";
132
+ }
133
+
134
+ function usesVoice(spec: Spec, indexes: number[]): boolean {
135
+ for (let i = 0; i < indexes.length; i++) {
136
+ const sym = spec.symbols[indexes[i]];
137
+ if (sym.phrase.length > 0 || sym.outcomeYes.length > 0) return true;
138
+ }
139
+ return false;
140
+ }
141
+
142
+ function collectHeld(spec: Spec, env: Binding, term: Term, out: string[]): void {
143
+ if (term.kind === "and") {
144
+ collectHeld(spec, env, term.left, out);
145
+ collectHeld(spec, env, term.right, out);
146
+ return;
147
+ }
148
+ if (term.kind === "or") {
149
+ const leftTrue = evalBool(term.left, spec, env) === T_TRUE;
150
+ const rightTrue = evalBool(term.right, spec, env) === T_TRUE;
151
+ if (leftTrue) collectHeld(spec, env, term.left, out);
152
+ if (rightTrue) collectHeld(spec, env, term.right, out);
153
+ if (!leftTrue && !rightTrue) out.push(spokenOrPast(spec, term));
154
+ return;
155
+ }
156
+ if (term.kind === "sym") {
157
+ const sym = spec.symbols[term.index];
158
+ if (sym.sort.kind === "bool") {
159
+ out.push(reportBool(sym, env.code[term.index] === 1));
160
+ return;
161
+ }
162
+ }
163
+ if (term.kind === "not") {
164
+ const inner = term.term;
165
+ if (inner.kind === "sym") {
166
+ const sym = spec.symbols[inner.index];
167
+ if (sym.sort.kind === "bool") {
168
+ out.push(reportBool(sym, env.code[inner.index] === 1));
169
+ return;
170
+ }
171
+ }
172
+ }
173
+ out.push(spokenOrPast(spec, term));
174
+ }
175
+
176
+ function spokenOrPast(spec: Spec, term: Term): string {
177
+ const spoken = spokenComparison(spec, term);
178
+ if (spoken.length > 0) return spoken;
179
+ return pastVoice(formatTerm(term));
180
+ }
181
+
182
+ function spokenComparison(spec: Spec, term: Term): string {
183
+ if (
184
+ term.kind !== "eq" &&
185
+ term.kind !== "neq" &&
186
+ term.kind !== "lt" &&
187
+ term.kind !== "le" &&
188
+ term.kind !== "gt" &&
189
+ term.kind !== "ge"
190
+ ) {
191
+ return "";
192
+ }
193
+ if (term.left.kind !== "sym") return "";
194
+ const sym = spec.symbols[term.left.index];
195
+ if (sym.sort.kind === "bool") return "";
196
+ const shown = literalShown(term.right);
197
+ if (shown.length > 0) return speakBound(sym, shown, term.kind, false);
198
+ return speakBound(sym, formatTerm(term.right), term.kind, true);
199
+ }
200
+
201
+ function describeActual(spec: Spec, env: Binding, term: Term): string {
202
+ if (term.kind === "sym") {
203
+ const sym = spec.symbols[term.index];
204
+ if (sym.sort.kind === "bool") return reportBool(sym, env.code[term.index] === 1);
205
+ }
206
+ if (term.kind === "not") {
207
+ const inner = term.term;
208
+ if (inner.kind === "sym") {
209
+ const sym = spec.symbols[inner.index];
210
+ if (sym.sort.kind === "bool") return reportBool(sym, env.code[inner.index] === 1);
211
+ }
212
+ }
213
+ const indexes = mentionedIndexes(null, term);
214
+ if (indexes.length === 0) return pastVoice(formatTerm(term));
215
+ const parts: string[] = [];
216
+ for (let i = 0; i < indexes.length; i++) {
217
+ const sym = spec.symbols[indexes[i]];
218
+ const code = env.code[indexes[i]];
219
+ parts.push(describeAssignment(sym, displayCode(spec, sym, code), code === 1));
220
+ }
221
+ return englishList(parts);
222
+ }
223
+
224
+ export function formatCheckText(specPath: string, scenarioPath: string, violations: Violation[]): string {
225
+ const lines: string[] = [];
226
+ lines.push("spec: " + specPath);
227
+ lines.push("scenario: " + scenarioPath);
228
+ lines.push("violations: " + formatInt(violations.length));
229
+ for (let i = 0; i < violations.length; i++) {
230
+ const item = violations[i];
231
+ lines.push("");
232
+ lines.push(item.id + " line " + formatInt(item.line));
233
+ lines.push(item.sentence);
234
+ lines.push(item.because);
235
+ }
236
+ return lines.join("\n");
237
+ }
238
+
239
+ export function formatCheckJson(specPath: string, scenarioPath: string, violations: Violation[]): string {
240
+ const items: { id: string; line: number; sentence: string; because: string }[] = [];
241
+ for (let i = 0; i < violations.length; i++) {
242
+ const item = violations[i];
243
+ items.push({ id: item.id, line: item.line, sentence: item.sentence, because: item.because });
244
+ }
245
+ return JSON.stringify({ ok: violations.length === 0, spec: specPath, scenario: scenarioPath, violations: items }, null, 2);
246
+ }
247
+
248
+ export function formatErrorJson(message: string): string {
249
+ return JSON.stringify({ ok: false, error: message }, null, 2);
250
+ }