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/repair.ts ADDED
@@ -0,0 +1,567 @@
1
+ /**
2
+ * Candidate repairs for one minimal conflict.
3
+ * A repair drops one requirement in the set, or adds one driver literal
4
+ * (a Bool, an enum equality, or an Int bound) to one requirement's condition.
5
+ * Nothing here chooses a winner. A suggestion is kept only when the patched
6
+ * spec no longer has this conflict and has no conflict it did not already have.
7
+ */
8
+
9
+ import { parseProperty } from "./ears.ts";
10
+ import { firstUnsatDrivers, satisfiable } from "./decide.ts";
11
+ import { isDriver, findEnum, type Requirement, type Spec, type SymbolDecl, type Term } from "./spec.ts";
12
+ import { conflictingSets } from "./solver.ts";
13
+ import { englishList, formatInt } from "./text.ts";
14
+ import { termsEqual } from "./term.ts";
15
+
16
+ export interface Repair {
17
+ kind: string;
18
+ id: string;
19
+ text: string;
20
+ sentence: string;
21
+ }
22
+
23
+ interface Attempt {
24
+ literal: Term;
25
+ words: string;
26
+ requirement: Requirement;
27
+ }
28
+
29
+ export function conflictKey(spec: Spec, indexes: number[]): string {
30
+ let key = "";
31
+ for (let i = 0; i < indexes.length; i++) {
32
+ if (i > 0) key += ",";
33
+ key += spec.requirements[indexes[i]].id;
34
+ }
35
+ return key;
36
+ }
37
+
38
+ export function findRepairs(spec: Spec, indexes: number[], originalKeys: string[]): Repair[] {
39
+ const found: Repair[] = [];
40
+ if (indexes.length === 0) return found;
41
+ const target = conflictKey(spec, indexes);
42
+ const ids: string[] = [];
43
+ for (let i = 0; i < indexes.length; i++) ids.push(spec.requirements[indexes[i]].id);
44
+ for (let i = 0; i < indexes.length; i++) {
45
+ const dropped = dropRepair(spec, indexes, i, ids, originalKeys, target);
46
+ if (dropped !== null) found.push(dropped);
47
+ }
48
+ for (let i = 0; i < indexes.length; i++) {
49
+ const strengthened = strengthenRepairs(spec, indexes, i, ids, originalKeys, target);
50
+ for (let s = 0; s < strengthened.length; s++) found.push(strengthened[s]);
51
+ }
52
+ return found;
53
+ }
54
+
55
+ function dropRepair(
56
+ spec: Spec,
57
+ indexes: number[],
58
+ pos: number,
59
+ ids: string[],
60
+ originalKeys: string[],
61
+ target: string,
62
+ ): Repair | null {
63
+ const at = indexes[pos];
64
+ if (!verified(spec, indexes, at, null, true, originalKeys, target)) return null;
65
+ const keptIds: string[] = [];
66
+ const keptSentences: string[] = [];
67
+ for (let i = 0; i < indexes.length; i++) {
68
+ if (i === pos) continue;
69
+ keptIds.push(ids[i]);
70
+ keptSentences.push(spec.requirements[indexes[i]].sentence);
71
+ }
72
+ let sentence = "";
73
+ for (let i = 0; i < keptSentences.length; i++) {
74
+ if (i > 0) sentence += "\n";
75
+ sentence += keptSentences[i];
76
+ }
77
+ return { kind: "drop", id: ids[pos], text: dropText(ids[pos], keptIds), sentence: sentence };
78
+ }
79
+
80
+ function dropText(id: string, kept: string[]): string {
81
+ if (kept.length === 0) return "One repair is to drop " + id + ".";
82
+ if (kept.length === 1) return "One repair is to drop " + id + " and keep " + kept[0] + " as written.";
83
+ return "One repair is to drop " + id + " and keep " + englishList(kept) + " as written.";
84
+ }
85
+
86
+ function strengthenRepairs(
87
+ spec: Spec,
88
+ indexes: number[],
89
+ pos: number,
90
+ ids: string[],
91
+ originalKeys: string[],
92
+ target: string,
93
+ ): Repair[] {
94
+ const at = indexes[pos];
95
+ const req = spec.requirements[at];
96
+ const raw: Attempt[] = [];
97
+ for (let s = 0; s < spec.symbols.length; s++) {
98
+ const sym = spec.symbols[s];
99
+ if (!isDriver(sym)) continue;
100
+ if (sym.sort.kind === "bool") {
101
+ consider(spec, indexes, at, req, notTerm(symTerm(spec, s)), boolWords(sym, false), raw);
102
+ consider(spec, indexes, at, req, symTerm(spec, s), boolWords(sym, true), raw);
103
+ continue;
104
+ }
105
+ if (sym.sort.kind === "enum") {
106
+ const decl = findEnum(spec, sym.sort.enumName);
107
+ if (decl === null) continue;
108
+ for (let v = 0; v < decl.variants.length; v++) {
109
+ const literal: Term = {
110
+ kind: "eq",
111
+ left: symTerm(spec, s),
112
+ right: { kind: "enum", enumName: decl.name, variant: decl.variants[v], index: v },
113
+ };
114
+ consider(spec, indexes, at, req, literal, enumWords(sym, decl.variants[v]), raw);
115
+ }
116
+ continue;
117
+ }
118
+ const constants = intConstants(spec, sym.sort.min, sym.sort.max);
119
+ const ops = ["lt", "gt", "le", "ge", "eq"];
120
+ for (let c = 0; c < constants.length; c++) {
121
+ for (let o = 0; o < ops.length; o++) {
122
+ if (!usefulBound(ops[o], constants[c], sym.sort.min, sym.sort.max)) continue;
123
+ const literal = boundTerm(spec, s, ops[o], constants[c]);
124
+ consider(spec, indexes, at, req, literal, boundWords(sym, ops[o], constants[c]), raw);
125
+ }
126
+ }
127
+ }
128
+ const keep = pareto(spec, req.antecedent, raw);
129
+ const found: Repair[] = [];
130
+ for (let i = 0; i < raw.length; i++) {
131
+ if (!keep[i]) continue;
132
+ if (!verified(spec, indexes, at, raw[i].requirement, false, originalKeys, target)) continue;
133
+ found.push({
134
+ kind: "strengthen",
135
+ id: req.id,
136
+ text: strengthenText(req.id, raw[i].words, ids),
137
+ sentence: raw[i].requirement.sentence,
138
+ });
139
+ }
140
+ return found;
141
+ }
142
+
143
+ function consider(
144
+ spec: Spec,
145
+ indexes: number[],
146
+ at: number,
147
+ req: Requirement,
148
+ literal: Term,
149
+ words: string,
150
+ raw: Attempt[],
151
+ ): void {
152
+ const sentence = rewriteSentence(req.sentence, words);
153
+ const parsed = parseProperty(req.id + " " + sentence, spec);
154
+ if (!parsed.ok || parsed.requirements.length !== 1) return;
155
+ const got = parsed.requirements[0];
156
+ if (!termsEqual(got.consequent, req.consequent)) return;
157
+ if (got.antecedent === null) return;
158
+ if (!equivalent(spec, got.antecedent, andTerm(req.antecedent, literal))) return;
159
+ if (!conditionPossible(spec, got.antecedent)) return;
160
+ const holds: Requirement[] = [];
161
+ for (let i = 0; i < indexes.length; i++) {
162
+ if (indexes[i] === at) holds.push(withLine(got, req));
163
+ else holds.push(spec.requirements[indexes[i]]);
164
+ }
165
+ if (firstUnsatDrivers(spec, holds) !== null) return;
166
+ raw.push({ literal: literal, words: words, requirement: withLine(got, req) });
167
+ }
168
+
169
+ function withLine(parsed: Requirement, original: Requirement): Requirement {
170
+ return {
171
+ id: original.id,
172
+ line: original.line,
173
+ sentence: parsed.sentence,
174
+ system: parsed.system,
175
+ antecedent: parsed.antecedent,
176
+ consequent: parsed.consequent,
177
+ hasWhen: parsed.hasWhen,
178
+ hasIf: parsed.hasIf,
179
+ whenTerm: parsed.whenTerm,
180
+ };
181
+ }
182
+
183
+ function pareto(spec: Spec, antecedent: Term | null, raw: Attempt[]): boolean[] {
184
+ const keep: boolean[] = [];
185
+ for (let i = 0; i < raw.length; i++) keep.push(true);
186
+ for (let i = 0; i < raw.length; i++) {
187
+ for (let j = 0; j < raw.length; j++) {
188
+ if (i === j || !keep[i]) continue;
189
+ const iSub = excludesSubset(spec, antecedent, raw[i].literal, raw[j].literal);
190
+ const jSub = excludesSubset(spec, antecedent, raw[j].literal, raw[i].literal);
191
+ if (iSub && jSub && i > j) keep[i] = false;
192
+ if (jSub && !iSub) keep[i] = false;
193
+ }
194
+ }
195
+ return keep;
196
+ }
197
+
198
+ function excludesSubset(spec: Spec, antecedent: Term | null, smaller: Term, larger: Term): boolean {
199
+ return !conditionPossible(spec, andTerm(antecedent, andTerm(notTerm(smaller), larger)));
200
+ }
201
+
202
+ function verified(
203
+ spec: Spec,
204
+ indexes: number[],
205
+ at: number,
206
+ replacement: Requirement | null,
207
+ drop: boolean,
208
+ originalKeys: string[],
209
+ target: string,
210
+ ): boolean {
211
+ const requirements: Requirement[] = [];
212
+ for (let i = 0; i < spec.requirements.length; i++) {
213
+ if (drop && i === at) continue;
214
+ if (!drop && i === at && replacement !== null) requirements.push(replacement);
215
+ else requirements.push(spec.requirements[i]);
216
+ }
217
+ const patched: Spec = { enums: spec.enums, symbols: spec.symbols, requirements: requirements };
218
+ const targetReqs: Requirement[] = [];
219
+ for (let i = 0; i < indexes.length; i++) {
220
+ const idx = indexes[i];
221
+ if (drop && idx === at) continue;
222
+ if (!drop && idx === at && replacement !== null) targetReqs.push(replacement);
223
+ else targetReqs.push(spec.requirements[idx]);
224
+ }
225
+ if (targetReqs.length > 0 && firstUnsatDrivers(patched, targetReqs) !== null) return false;
226
+ const keys = patchedKeys(patched);
227
+ for (let i = 0; i < keys.length; i++) {
228
+ if (keys[i] === target) return false;
229
+ let known = false;
230
+ for (let j = 0; j < originalKeys.length; j++) {
231
+ if (originalKeys[j] === keys[i]) known = true;
232
+ }
233
+ if (!known && !alreadyUnsat(spec, keys[i])) return false;
234
+ }
235
+ return true;
236
+ }
237
+
238
+ function alreadyUnsat(spec: Spec, key: string): boolean {
239
+ const ids: string[] = [];
240
+ let start = 0;
241
+ for (let i = 0; i <= key.length; i++) {
242
+ if (i === key.length || key.charCodeAt(i) === 44) {
243
+ if (i > start) ids.push(key.slice(start, i));
244
+ start = i + 1;
245
+ }
246
+ }
247
+ const holds: Requirement[] = [];
248
+ for (let i = 0; i < ids.length; i++) {
249
+ let found: Requirement | null = null;
250
+ for (let r = 0; r < spec.requirements.length; r++) {
251
+ if (spec.requirements[r].id === ids[i]) found = spec.requirements[r];
252
+ }
253
+ if (found === null) return false;
254
+ holds.push(found);
255
+ }
256
+ if (holds.length === 0) return false;
257
+ return firstUnsatDrivers(spec, holds) !== null;
258
+ }
259
+
260
+ function patchedKeys(spec: Spec): string[] {
261
+ if (spec.requirements.length > 12) {
262
+ const groups = conflictingSets(spec);
263
+ const keys: string[] = [];
264
+ for (let i = 0; i < groups.length; i++) keys.push(conflictKey(spec, groups[i].indexes));
265
+ return keys;
266
+ }
267
+ const keys: string[] = [];
268
+ const pool: number[] = [];
269
+ for (let i = 0; i < spec.requirements.length; i++) pool.push(i);
270
+ collectCores(spec, pool, keys, new UnsatCache());
271
+ return keys;
272
+ }
273
+
274
+ class UnsatCache {
275
+ keys: string[];
276
+ unsat: boolean[];
277
+ pools: string[];
278
+
279
+ constructor() {
280
+ this.keys = [];
281
+ this.unsat = [];
282
+ this.pools = [];
283
+ }
284
+ }
285
+
286
+ function collectCores(spec: Spec, pool: number[], keys: string[], cache: UnsatCache): void {
287
+ if (pool.length === 0) return;
288
+ const poolKey = indexKey(pool);
289
+ for (let i = 0; i < cache.pools.length; i++) {
290
+ if (cache.pools[i] === poolKey) return;
291
+ }
292
+ cache.pools.push(poolKey);
293
+ if (!indexesUnsat(spec, pool, cache)) return;
294
+ const core = shrinkCore(spec, pool, cache);
295
+ if (core.length === 0) return;
296
+ const key = conflictKey(spec, core);
297
+ let seen = false;
298
+ for (let i = 0; i < keys.length; i++) {
299
+ if (keys[i] === key) seen = true;
300
+ }
301
+ if (!seen) keys.push(key);
302
+ for (let c = 0; c < core.length; c++) {
303
+ const next: number[] = [];
304
+ for (let i = 0; i < pool.length; i++) {
305
+ if (pool[i] !== core[c]) next.push(pool[i]);
306
+ }
307
+ collectCores(spec, next, keys, cache);
308
+ }
309
+ }
310
+
311
+ function shrinkCore(spec: Spec, indexes: number[], cache: UnsatCache): number[] {
312
+ let current: number[] = [];
313
+ for (let i = 0; i < indexes.length; i++) current.push(indexes[i]);
314
+ let i = 0;
315
+ while (i < current.length) {
316
+ const trial: number[] = [];
317
+ for (let j = 0; j < current.length; j++) {
318
+ if (j !== i) trial.push(current[j]);
319
+ }
320
+ if (trial.length > 0 && indexesUnsat(spec, trial, cache)) current = trial;
321
+ else i++;
322
+ }
323
+ return current;
324
+ }
325
+
326
+ function indexKey(indexes: number[]): string {
327
+ let key = "";
328
+ for (let i = 0; i < indexes.length; i++) {
329
+ if (i > 0) key += ",";
330
+ key += String(indexes[i]);
331
+ }
332
+ return key;
333
+ }
334
+
335
+ function indexesUnsat(spec: Spec, indexes: number[], cache: UnsatCache): boolean {
336
+ const key = indexKey(indexes);
337
+ for (let i = 0; i < cache.keys.length; i++) {
338
+ if (cache.keys[i] === key) return cache.unsat[i];
339
+ }
340
+ const holds: Requirement[] = [];
341
+ for (let i = 0; i < indexes.length; i++) holds.push(spec.requirements[indexes[i]]);
342
+ const unsat = firstUnsatDrivers(spec, holds) !== null;
343
+ cache.keys.push(key);
344
+ cache.unsat.push(unsat);
345
+ return unsat;
346
+ }
347
+
348
+ function strengthenText(id: string, words: string, ids: string[]): string {
349
+ return "One repair is to add \"and " + words + "\" to the condition of " + id + "." + stays(id, ids);
350
+ }
351
+
352
+ function stays(id: string, ids: string[]): string {
353
+ const rest: string[] = [];
354
+ for (let i = 0; i < ids.length; i++) {
355
+ if (ids[i] !== id) rest.push(ids[i]);
356
+ }
357
+ if (rest.length === 0) return "";
358
+ if (rest.length === 1) return " " + rest[0] + " stays as written.";
359
+ return " " + englishList(rest) + " stay as written.";
360
+ }
361
+
362
+ function rewriteSentence(sentence: string, extra: string): string {
363
+ let body = sentence;
364
+ if (body.length > 0 && body.charCodeAt(body.length - 1) === 46) body = body.slice(0, body.length - 1);
365
+ const parts = splitTopCommas(body);
366
+ if (parts === null || parts.length <= 1) {
367
+ let base = body.trim();
368
+ if (base.length >= 4 && base.slice(0, 4) === "The ") base = "the " + base.slice(4);
369
+ return "When " + extra + ", " + base + ".";
370
+ }
371
+ const idx = parts.length - 2;
372
+ parts[idx] = extendClause(parts[idx], extra);
373
+ let out = parts[0];
374
+ for (let i = 1; i < parts.length; i++) out += ", " + parts[i];
375
+ return out + ".";
376
+ }
377
+
378
+ function extendClause(clause: string, extra: string): string {
379
+ const trimmed = clause.trim();
380
+ let space = -1;
381
+ for (let i = 0; i < trimmed.length; i++) {
382
+ if (trimmed.charCodeAt(i) === 32) {
383
+ space = i;
384
+ break;
385
+ }
386
+ }
387
+ if (space < 0) return trimmed + " and " + extra;
388
+ const keyword = trimmed.slice(0, space);
389
+ const rest = trimmed.slice(space + 1).trim();
390
+ if (hasTopOr(rest)) return keyword + " (" + rest + ") and " + extra;
391
+ return keyword + " " + rest + " and " + extra;
392
+ }
393
+
394
+ function hasTopOr(text: string): boolean {
395
+ let depth = 0;
396
+ let start = 0;
397
+ for (let i = 0; i <= text.length; i++) {
398
+ const code = i < text.length ? text.charCodeAt(i) : 32;
399
+ if (code === 40) depth++;
400
+ else if (code === 41 && depth > 0) depth--;
401
+ if (depth === 0 && (code === 32 || i === text.length)) {
402
+ if (text.slice(start, i) === "or") return true;
403
+ start = i + 1;
404
+ }
405
+ }
406
+ return false;
407
+ }
408
+
409
+ function splitTopCommas(text: string): string[] | null {
410
+ const parts: string[] = [];
411
+ let start = 0;
412
+ let depth = 0;
413
+ for (let i = 0; i < text.length; i++) {
414
+ const code = text.charCodeAt(i);
415
+ if (code === 40) depth++;
416
+ else if (code === 41) {
417
+ depth--;
418
+ if (depth < 0) return null;
419
+ } else if (code === 44 && depth === 0) {
420
+ parts.push(text.slice(start, i).trim());
421
+ start = i + 1;
422
+ }
423
+ }
424
+ if (depth !== 0) return null;
425
+ parts.push(text.slice(start).trim());
426
+ return parts;
427
+ }
428
+
429
+ function symTerm(spec: Spec, index: number): Term {
430
+ const sym = spec.symbols[index];
431
+ return { kind: "sym", name: sym.name, index: index, phrase: sym.phrase };
432
+ }
433
+
434
+ function notTerm(term: Term): Term {
435
+ return { kind: "not", term: term };
436
+ }
437
+
438
+ function andTerm(left: Term | null, right: Term): Term {
439
+ if (left === null) return right;
440
+ return { kind: "and", left: left, right: right };
441
+ }
442
+
443
+ function boundTerm(spec: Spec, index: number, rel: string, value: number): Term {
444
+ const right: Term = { kind: "int", value: value };
445
+ const left = symTerm(spec, index);
446
+ if (rel === "lt") return { kind: "lt", left: left, right: right };
447
+ if (rel === "gt") return { kind: "gt", left: left, right: right };
448
+ if (rel === "le") return { kind: "le", left: left, right: right };
449
+ if (rel === "ge") return { kind: "ge", left: left, right: right };
450
+ return { kind: "eq", left: left, right: right };
451
+ }
452
+
453
+ function boolWords(sym: SymbolDecl, isTrue: boolean): string {
454
+ const phrase = sym.phrase.length > 0 ? sym.phrase : sym.name;
455
+ if (isTrue) return phrase;
456
+ return negateCondition(phrase);
457
+ }
458
+
459
+ function negateCondition(phrase: string): string {
460
+ const token = " is ";
461
+ const at = phrase.indexOf(token);
462
+ if (at >= 0) return phrase.slice(0, at) + " is not " + phrase.slice(at + token.length);
463
+ return "not " + phrase;
464
+ }
465
+
466
+ function enumWords(sym: SymbolDecl, variant: string): string {
467
+ const name = sym.phrase.length > 0 ? sym.phrase : sym.name;
468
+ return name + " is " + variant;
469
+ }
470
+
471
+ function boundWords(sym: SymbolDecl, rel: string, value: number): string {
472
+ const name = sym.phrase.length > 0 ? sym.phrase : sym.name;
473
+ return name + " is " + relationWords(rel, formatInt(value));
474
+ }
475
+
476
+ function relationWords(rel: string, shown: string): string {
477
+ if (rel === "lt") return "less than " + shown;
478
+ if (rel === "gt") return "greater than " + shown;
479
+ if (rel === "le") return "at most " + shown;
480
+ if (rel === "ge") return "at least " + shown;
481
+ return shown;
482
+ }
483
+
484
+ function usefulBound(rel: string, value: number, min: number, max: number): boolean {
485
+ if (rel === "eq") return value >= min && value <= max;
486
+ if (rel === "le") return value >= min && value < max;
487
+ if (rel === "ge") return value > min && value <= max;
488
+ if (rel === "lt") return value > min && value <= max;
489
+ if (rel === "gt") return value >= min && value < max;
490
+ return false;
491
+ }
492
+
493
+ function intConstants(spec: Spec, min: number, max: number): number[] {
494
+ const found: number[] = [];
495
+ pushUnique(found, min);
496
+ pushUnique(found, max);
497
+ for (let r = 0; r < spec.requirements.length; r++) {
498
+ const req = spec.requirements[r];
499
+ if (req.antecedent !== null) collectInts(req.antecedent, found);
500
+ collectInts(req.consequent, found);
501
+ }
502
+ for (let i = 0; i < found.length; i++) {
503
+ for (let j = i + 1; j < found.length; j++) {
504
+ if (found[j] < found[i]) {
505
+ const tmp = found[i];
506
+ found[i] = found[j];
507
+ found[j] = tmp;
508
+ }
509
+ }
510
+ }
511
+ return found;
512
+ }
513
+
514
+ function collectInts(term: Term, found: number[]): void {
515
+ if (term.kind === "int") {
516
+ pushUnique(found, term.value);
517
+ return;
518
+ }
519
+ if (term.kind === "not") {
520
+ collectInts(term.term, found);
521
+ return;
522
+ }
523
+ if (
524
+ term.kind === "and" ||
525
+ term.kind === "or" ||
526
+ term.kind === "eq" ||
527
+ term.kind === "neq" ||
528
+ term.kind === "lt" ||
529
+ term.kind === "le" ||
530
+ term.kind === "gt" ||
531
+ term.kind === "ge" ||
532
+ term.kind === "add" ||
533
+ term.kind === "sub"
534
+ ) {
535
+ collectInts(term.left, found);
536
+ collectInts(term.right, found);
537
+ }
538
+ }
539
+
540
+ function pushUnique(found: number[], value: number): void {
541
+ for (let i = 0; i < found.length; i++) {
542
+ if (found[i] === value) return;
543
+ }
544
+ found.push(value);
545
+ }
546
+
547
+ function equivalent(spec: Spec, left: Term, right: Term): boolean {
548
+ if (termsEqual(left, right)) return true;
549
+ return !conditionPossible(spec, andTerm(left, notTerm(right))) && !conditionPossible(spec, andTerm(right, notTerm(left)));
550
+ }
551
+
552
+ function conditionPossible(spec: Spec, term: Term): boolean {
553
+ const probe: Requirement = {
554
+ id: "probe",
555
+ line: 0,
556
+ sentence: "",
557
+ system: "",
558
+ antecedent: null,
559
+ consequent: term,
560
+ hasWhen: false,
561
+ hasIf: false,
562
+ whenTerm: null,
563
+ };
564
+ const holds: Requirement[] = [];
565
+ holds.push(probe);
566
+ return satisfiable(spec, holds);
567
+ }