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/main.ts ADDED
@@ -0,0 +1,273 @@
1
+ /**
2
+ * plan-verify command line.
3
+ * User arguments start at process.argv index 2 under both Node and ScriptC.
4
+ */
5
+
6
+ import { readFileSync } from "node:fs";
7
+ import { bindScenario, evaluate, formatCheckJson, formatCheckText, formatErrorJson } from "./check.ts";
8
+ import { parseProperty, parseSpec } from "./ears.ts";
9
+ import { parseScenarioJson } from "./jsonvalue.ts";
10
+ import { formatLintJson, formatLintText, lintSpec } from "./lint.ts";
11
+ import { buildProblems } from "./report.ts";
12
+ import { formatScenariosJson, formatScenariosText, listScenarios } from "./scenarios.ts";
13
+ import { analyzePolicy, formatPolicyJson, formatPolicyText } from "./policy.ts";
14
+ import { formatEmitJson, printSmt } from "./smt.ts";
15
+ import { spaceSize } from "./solver.ts";
16
+ import { displayQuote } from "./text.ts";
17
+ import type { Requirement } from "./spec.ts";
18
+
19
+ const EXIT_OK = 0;
20
+ const EXIT_VIOLATIONS = 1;
21
+ const EXIT_USAGE = 2;
22
+
23
+ function main(): void {
24
+ const args: string[] = [];
25
+ for (let i = 2; i < process.argv.length; i++) args.push(process.argv[i]);
26
+ if (args.length === 0) {
27
+ console.error("error: missing command");
28
+ usage(true);
29
+ process.exit(EXIT_USAGE);
30
+ }
31
+ const command = args[0];
32
+ if (command === "help" || command === "--help" || command === "-h") {
33
+ usage(false);
34
+ process.exit(EXIT_OK);
35
+ }
36
+ if (command === "policy") {
37
+ commandPolicy(args.slice(1));
38
+ return;
39
+ }
40
+ if (command === "check") {
41
+ commandCheck(args.slice(1));
42
+ return;
43
+ }
44
+ if (command === "emit-smt") {
45
+ commandEmit(args.slice(1));
46
+ return;
47
+ }
48
+ if (command === "lint") {
49
+ commandLint(args.slice(1));
50
+ return;
51
+ }
52
+ if (command === "report") {
53
+ commandReport(args.slice(1));
54
+ return;
55
+ }
56
+ if (command === "scenarios") {
57
+ commandScenarios(args.slice(1));
58
+ return;
59
+ }
60
+ console.error("error: unknown command " + displayQuote(command));
61
+ usage(true);
62
+ process.exit(EXIT_USAGE);
63
+ }
64
+
65
+ interface Flags {
66
+ json: boolean;
67
+ input: string;
68
+ property: string;
69
+ positionals: string[];
70
+ error: string;
71
+ }
72
+
73
+ function parseFlags(args: string[], allowInput: boolean, allowProperty: boolean): Flags {
74
+ const flags: Flags = { json: false, input: "", property: "", positionals: [], error: "" };
75
+ for (let i = 0; i < args.length; i++) {
76
+ const arg = args[i];
77
+ if (arg === "--json") flags.json = true;
78
+ else if (arg === "--input") {
79
+ if (!allowInput) {
80
+ flags.error = "unknown option --input";
81
+ return flags;
82
+ }
83
+ if (flags.input.length > 0) {
84
+ flags.error = "repeated option --input";
85
+ return flags;
86
+ }
87
+ if (i + 1 >= args.length) {
88
+ flags.error = "missing value for --input";
89
+ return flags;
90
+ }
91
+ i++;
92
+ flags.input = args[i];
93
+ } else if (arg === "--property") {
94
+ if (!allowProperty) {
95
+ flags.error = "unknown option --property";
96
+ return flags;
97
+ }
98
+ if (flags.property.length > 0) {
99
+ flags.error = "repeated option --property";
100
+ return flags;
101
+ }
102
+ if (i + 1 >= args.length) {
103
+ flags.error = "missing value for --property";
104
+ return flags;
105
+ }
106
+ i++;
107
+ flags.property = args[i];
108
+ } else if (arg.startsWith("--")) {
109
+ flags.error = "unknown option " + arg;
110
+ return flags;
111
+ } else flags.positionals.push(arg);
112
+ }
113
+ return flags;
114
+ }
115
+
116
+ function commandCheck(args: string[]): void {
117
+ const flags = parseFlags(args, true, false);
118
+ if (flags.error.length > 0) fail(flags.json, flags.error);
119
+ if (flags.positionals.length !== 1 || flags.input.length === 0) {
120
+ fail(flags.json, "usage: plan-verify check [--json] <spec> --input <scenario>");
121
+ }
122
+ const specPath = flags.positionals[0];
123
+ const scenarioPath = flags.input;
124
+ const specText = readText(specPath);
125
+ if (specText === null) fail(flags.json, "cannot read " + specPath);
126
+ const spec = parseSpec(specText);
127
+ if (!spec.ok) fail(flags.json, specPath + ":" + String(spec.line) + ": " + spec.message);
128
+ const scenarioText = readText(scenarioPath);
129
+ if (scenarioText === null) fail(flags.json, "cannot read " + scenarioPath);
130
+ const json = parseScenarioJson(scenarioText);
131
+ if (!json.ok) fail(flags.json, scenarioPath + ":" + String(json.line) + ": " + json.message);
132
+ const bound = bindScenario(spec.spec, json.object);
133
+ if (!bound.ok) fail(flags.json, "scenario " + scenarioPath + ": " + bound.message);
134
+ const violations = evaluate(spec.spec, bound.env);
135
+ if (flags.json) console.log(formatCheckJson(specPath, scenarioPath, violations));
136
+ else console.log(formatCheckText(specPath, scenarioPath, violations));
137
+ if (violations.length === 0) process.exit(EXIT_OK);
138
+ process.exit(EXIT_VIOLATIONS);
139
+ }
140
+
141
+ function commandEmit(args: string[]): void {
142
+ const flags = parseFlags(args, false, false);
143
+ if (flags.error.length > 0) fail(flags.json, flags.error);
144
+ if (flags.positionals.length !== 1) fail(flags.json, "usage: plan-verify emit-smt [--json] <spec>");
145
+ const specPath = flags.positionals[0];
146
+ const specText = readText(specPath);
147
+ if (specText === null) fail(flags.json, "cannot read " + specPath);
148
+ const spec = parseSpec(specText);
149
+ if (!spec.ok) fail(flags.json, specPath + ":" + String(spec.line) + ": " + spec.message);
150
+ const smt = printSmt(specPath, spec.spec);
151
+ if (flags.json) console.log(formatEmitJson(specPath, smt));
152
+ else console.log(smt);
153
+ process.exit(EXIT_OK);
154
+ }
155
+
156
+ function commandLint(args: string[]): void {
157
+ const flags = parseFlags(args, false, false);
158
+ if (flags.error.length > 0) fail(flags.json, flags.error);
159
+ if (flags.input.length > 0 || flags.positionals.length !== 1) fail(flags.json, "usage: plan-verify lint [--json] <spec>");
160
+ const specPath = flags.positionals[0];
161
+ const specText = readText(specPath);
162
+ if (specText === null) fail(flags.json, "cannot read " + specPath);
163
+ const spec = parseSpec(specText);
164
+ if (!spec.ok) fail(flags.json, specPath + ":" + String(spec.line) + ": " + spec.message);
165
+ const outcome = lintSpec(spec.spec);
166
+ if (outcome.kind === "error") fail(flags.json, outcome.message);
167
+ const findings =
168
+ outcome.report.conflicts.length +
169
+ outcome.report.gaps.length +
170
+ outcome.report.misuse.length +
171
+ outcome.report.wording.length +
172
+ outcome.report.redundant.length +
173
+ outcome.report.duplicate.length +
174
+ outcome.report.vacuous.length +
175
+ outcome.report.unused.length;
176
+ if (flags.json) console.log(formatLintJson(specPath, outcome.report));
177
+ else console.log(formatLintText(specPath, outcome.report));
178
+ if (findings === 0) process.exit(EXIT_OK);
179
+ process.exit(EXIT_VIOLATIONS);
180
+ }
181
+
182
+ function commandReport(args: string[]): void {
183
+ const flags = parseFlags(args, false, false);
184
+ if (flags.error.length > 0) fail(flags.json, flags.error);
185
+ if (flags.json) fail(true, "usage: plan-verify report <spec>");
186
+ if (flags.positionals.length !== 1) fail(false, "usage: plan-verify report <spec>");
187
+ const specPath = flags.positionals[0];
188
+ const specText = readText(specPath);
189
+ if (specText === null) fail(false, "cannot read " + specPath);
190
+ const spec = parseSpec(specText);
191
+ if (!spec.ok) fail(false, specPath + ":" + String(spec.line) + ": " + spec.message);
192
+ const outcome = lintSpec(spec.spec);
193
+ if (outcome.kind === "error") fail(false, outcome.message);
194
+ const doc = buildProblems(specPath, spec.spec, outcome.report);
195
+ console.log(doc.text);
196
+ if (doc.count === 0) process.exit(EXIT_OK);
197
+ process.exit(EXIT_VIOLATIONS);
198
+ }
199
+
200
+ function commandScenarios(args: string[]): void {
201
+ const flags = parseFlags(args, false, false);
202
+ if (flags.error.length > 0) fail(flags.json, flags.error);
203
+ if (flags.positionals.length !== 1) fail(flags.json, "usage: plan-verify scenarios [--json] <spec>");
204
+ const specPath = flags.positionals[0];
205
+ const specText = readText(specPath);
206
+ if (specText === null) fail(flags.json, "cannot read " + specPath);
207
+ const spec = parseSpec(specText);
208
+ if (!spec.ok) fail(flags.json, specPath + ":" + String(spec.line) + ": " + spec.message);
209
+ if (spaceSize(spec.spec) < 0) {
210
+ fail(flags.json, "a symbol has an empty domain. Declare at least one allowed value.");
211
+ }
212
+ const rows = listScenarios(spec.spec);
213
+ if (flags.json) console.log(formatScenariosJson(specPath, rows));
214
+ else console.log(formatScenariosText(specPath, rows));
215
+ process.exit(EXIT_OK);
216
+ }
217
+
218
+ function commandPolicy(args: string[]): void {
219
+ const flags = parseFlags(args, false, true);
220
+ if (flags.error.length > 0) fail(flags.json, flags.error);
221
+ if (flags.positionals.length !== 1) fail(flags.json, "usage: plan-verify policy [--json] <spec> [--property <file>]");
222
+ const specPath = flags.positionals[0];
223
+ const specText = readText(specPath);
224
+ if (specText === null) fail(flags.json, "cannot read " + specPath);
225
+ const parsed = parseSpec(specText);
226
+ if (!parsed.ok) fail(flags.json, specPath + ":" + String(parsed.line) + ": " + parsed.message);
227
+ if (spaceSize(parsed.spec) < 0) {
228
+ fail(flags.json, "a symbol has an empty domain. Declare at least one allowed value.");
229
+ }
230
+ let propertyReqs: Requirement[] = [];
231
+ if (flags.property.length > 0) {
232
+ const propertyText = readText(flags.property);
233
+ if (propertyText === null) fail(flags.json, "cannot read " + flags.property);
234
+ const property = parseProperty(propertyText, parsed.spec);
235
+ if (!property.ok) fail(flags.json, flags.property + ":" + String(property.line) + ": " + property.message);
236
+ propertyReqs = property.requirements;
237
+ }
238
+ const report = analyzePolicy(parsed.spec, flags.property, propertyReqs);
239
+ if (flags.json) console.log(formatPolicyJson(specPath, parsed.spec, report));
240
+ else console.log(formatPolicyText(specPath, parsed.spec, report));
241
+ if (report.holds) process.exit(EXIT_OK);
242
+ process.exit(EXIT_VIOLATIONS);
243
+ }
244
+
245
+ function readText(path: string): string | null {
246
+ try {
247
+ return readFileSync(path, "utf8");
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
252
+
253
+ function fail(json: boolean, message: string): never {
254
+ if (json) console.log(formatErrorJson(message));
255
+ else console.error("error: " + message);
256
+ process.exit(EXIT_USAGE);
257
+ }
258
+
259
+ function usage(toStderr: boolean): void {
260
+ const text =
261
+ "usage:\n" +
262
+ " plan-verify check [--json] <spec> --input <scenario>\n" +
263
+ " plan-verify emit-smt [--json] <spec>\n" +
264
+ " plan-verify lint [--json] <spec>\n" +
265
+ " plan-verify report <spec>\n" +
266
+ " plan-verify scenarios [--json] <spec>\n" +
267
+ " plan-verify policy [--json] <spec> [--property <file>]\n" +
268
+ " plan-verify help";
269
+ if (toStderr) console.error(text);
270
+ else console.log(text);
271
+ }
272
+
273
+ main();
package/src/policy.ts ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Policy search.
3
+ * With no property, report one scenario the requirements allow and one they
4
+ * forbid. With a property, search for a scenario that satisfies every
5
+ * requirement and violates the property.
6
+ * The first hit is the smallest assignment: false before true, enums in
7
+ * declaration order, integers from low to high.
8
+ */
9
+
10
+ import { evaluateRequirements, type Violation } from "./check.ts";
11
+ import { displayCode, type Requirement, type Spec } from "./spec.ts";
12
+ import { firstScenario } from "./solver.ts";
13
+ import { Binding, formatScenario } from "./term.ts";
14
+ import { formatInt } from "./text.ts";
15
+
16
+ export interface PolicyReport {
17
+ propertyPath: string;
18
+ holds: boolean;
19
+ note: string;
20
+ accepted: Binding | null;
21
+ rejected: Binding | null;
22
+ counterexample: Binding | null;
23
+ breaks: Violation[];
24
+ }
25
+
26
+ export function analyzePolicy(spec: Spec, propertyPath: string, propertyReqs: Requirement[]): PolicyReport {
27
+ if (propertyPath.length === 0) {
28
+ const accepted = firstScenario(spec, 0, propertyReqs);
29
+ const rejected = firstScenario(spec, 1, propertyReqs);
30
+ const breaks = rejected === null ? [] : evaluateRequirements(spec, spec.requirements, rejected);
31
+ return {
32
+ propertyPath: "",
33
+ holds: true,
34
+ note: "",
35
+ accepted: accepted,
36
+ rejected: rejected,
37
+ counterexample: null,
38
+ breaks: breaks,
39
+ };
40
+ }
41
+ const counterexample = firstScenario(spec, 2, propertyReqs);
42
+ if (counterexample !== null) {
43
+ return {
44
+ propertyPath: propertyPath,
45
+ holds: false,
46
+ note: "",
47
+ accepted: null,
48
+ rejected: null,
49
+ counterexample: counterexample,
50
+ breaks: evaluateRequirements(spec, propertyReqs, counterexample),
51
+ };
52
+ }
53
+ const accepted = firstScenario(spec, 0, propertyReqs);
54
+ const note =
55
+ accepted === null
56
+ ? "No scenario satisfies every requirement, so none violates the property."
57
+ : "No scenario satisfies every requirement and violates the property.";
58
+ return {
59
+ propertyPath: propertyPath,
60
+ holds: true,
61
+ note: note,
62
+ accepted: null,
63
+ rejected: null,
64
+ counterexample: null,
65
+ breaks: [],
66
+ };
67
+ }
68
+
69
+ export function formatPolicyText(specPath: string, spec: Spec, report: PolicyReport): string {
70
+ const lines: string[] = [];
71
+ lines.push("spec: " + specPath);
72
+ if (report.propertyPath.length > 0) lines.push("property: " + report.propertyPath);
73
+ if (report.propertyPath.length === 0) {
74
+ lines.push("accepted:");
75
+ if (report.accepted === null) lines.push("No scenario satisfies every requirement.");
76
+ else {
77
+ lines.push(formatScenario(spec, report.accepted));
78
+ lines.push("This scenario satisfies every requirement.");
79
+ }
80
+ lines.push("rejected:");
81
+ if (report.rejected === null) lines.push("No scenario violates a requirement.");
82
+ else {
83
+ lines.push(formatScenario(spec, report.rejected));
84
+ lines.push("This scenario violates a requirement.");
85
+ appendBreaks(lines, report.breaks);
86
+ }
87
+ return lines.join("\n");
88
+ }
89
+ if (report.counterexample !== null) {
90
+ lines.push("counterexample:");
91
+ lines.push(formatScenario(spec, report.counterexample));
92
+ lines.push("This scenario satisfies every requirement and violates the property.");
93
+ appendBreaks(lines, report.breaks);
94
+ return lines.join("\n");
95
+ }
96
+ lines.push(report.note);
97
+ return lines.join("\n");
98
+ }
99
+
100
+ function appendBreaks(lines: string[], breaks: Violation[]): void {
101
+ for (let i = 0; i < breaks.length; i++) {
102
+ const item = breaks[i];
103
+ lines.push("");
104
+ lines.push(item.id + " line " + formatInt(item.line));
105
+ lines.push(item.sentence);
106
+ lines.push(item.because);
107
+ }
108
+ }
109
+
110
+ interface NameValue {
111
+ name: string;
112
+ value: string;
113
+ }
114
+
115
+ export function formatPolicyJson(specPath: string, spec: Spec, report: PolicyReport): string {
116
+ const breaks: { id: string; line: number; sentence: string; because: string }[] = [];
117
+ for (let i = 0; i < report.breaks.length; i++) {
118
+ const item = report.breaks[i];
119
+ breaks.push({ id: item.id, line: item.line, sentence: item.sentence, because: item.because });
120
+ }
121
+ return JSON.stringify(
122
+ {
123
+ ok: report.holds,
124
+ spec: specPath,
125
+ property: report.propertyPath,
126
+ holds: report.holds,
127
+ note: report.note,
128
+ acceptedFound: report.accepted !== null,
129
+ accepted: pairs(spec, report.accepted),
130
+ rejectedFound: report.rejected !== null,
131
+ rejected: pairs(spec, report.rejected),
132
+ counterexampleFound: report.counterexample !== null,
133
+ counterexample: pairs(spec, report.counterexample),
134
+ breaks: breaks,
135
+ },
136
+ null,
137
+ 2,
138
+ );
139
+ }
140
+
141
+ function pairs(spec: Spec, env: Binding | null): NameValue[] {
142
+ const out: NameValue[] = [];
143
+ if (env === null) return out;
144
+ for (let i = 0; i < spec.symbols.length; i++) {
145
+ out.push({ name: spec.symbols[i].name, value: displayCode(spec, spec.symbols[i], env.code[i]) });
146
+ }
147
+ return out;
148
+ }