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.
- package/.github/workflows/ci.yml +48 -0
- package/README.md +1431 -2
- package/assets/icon.png +0 -0
- package/assets/logo-mark.png +0 -0
- package/assets/logo.png +0 -0
- package/examples/checkout.ears.md +74 -0
- package/examples/order-always.ears.md +3 -0
- package/examples/order-awkward.ears.md +8 -0
- package/examples/order-bad.json +1 -0
- package/examples/order-chain.ears.md +9 -0
- package/examples/order-chosen.ears.md +7 -0
- package/examples/order-clear.ears.md +8 -0
- package/examples/order-combined.ears.md +10 -0
- package/examples/order-conflict.ears.md +8 -0
- package/examples/order-covered.ears.md +9 -0
- package/examples/order-duplicate.ears.md +8 -0
- package/examples/order-fail.json +5 -0
- package/examples/order-misuse.ears.md +7 -0
- package/examples/order-negation.ears.md +7 -0
- package/examples/order-pass.json +5 -0
- package/examples/order-quantity.ears.md +8 -0
- package/examples/order-redundant.ears.md +9 -0
- package/examples/order-repair.ears.md +9 -0
- package/examples/order-repeat.ears.md +13 -0
- package/examples/order-same.ears.md +3 -0
- package/examples/order-status.ears.md +7 -0
- package/examples/order-unanswered.ears.md +8 -0
- package/examples/order-unhandled.ears.md +8 -0
- package/examples/order-unmeasured.ears.md +10 -0
- package/examples/order-unused.ears.md +13 -0
- package/examples/order-vacuous.ears.md +9 -0
- package/examples/order-vague.ears.md +7 -0
- package/examples/order-wide.ears.md +5 -0
- package/examples/order-wording.ears.md +8 -0
- package/examples/order.ears.md +7 -0
- package/examples/payment-if.ears.md +7 -0
- package/examples/plan-to-problems/README.md +54 -0
- package/examples/plan-to-problems/expected-problems.md +25 -0
- package/examples/plan-to-problems/generate.mjs +186 -0
- package/examples/plan-to-problems/generated.ears.md +27 -0
- package/examples/plan-to-problems/lint.json +576 -0
- package/examples/plan-to-problems/plan.md +5 -0
- package/examples/plan-to-problems/planted.txt +5 -0
- package/examples/plan-to-problems/problems.md +112 -0
- package/examples/status-placed.ears.md +3 -0
- package/package.json +25 -4
- package/scripts/install-scriptc.sh +16 -0
- package/skills/plan-verify/SKILL.md +1515 -0
- package/src/check.ts +250 -0
- package/src/decide.ts +1324 -0
- package/src/ears.ts +1520 -0
- package/src/jsonvalue.ts +228 -0
- package/src/lint.ts +1245 -0
- package/src/main.ts +273 -0
- package/src/policy.ts +148 -0
- package/src/repair.ts +567 -0
- package/src/report.ts +329 -0
- package/src/scenarios.ts +149 -0
- package/src/smt.ts +97 -0
- package/src/solver.ts +868 -0
- package/src/spec.ts +307 -0
- package/src/term.ts +331 -0
- package/src/text.ts +95 -0
- package/test/fixtures/malformed.ears.md +3 -0
- package/test/fixtures/unknown-phrase.ears.md +5 -0
- package/test/golden/check-fail-json.exit +1 -0
- package/test/golden/check-fail-json.stderr +0 -0
- package/test/golden/check-fail-json.stdout +13 -0
- package/test/golden/check-fail.exit +1 -0
- package/test/golden/check-fail.stderr +0 -0
- package/test/golden/check-fail.stdout +7 -0
- package/test/golden/check-pass-json.exit +1 -0
- package/test/golden/check-pass-json.stderr +0 -0
- package/test/golden/check-pass-json.stdout +6 -0
- package/test/golden/check-pass.exit +1 -0
- package/test/golden/check-pass.stderr +0 -0
- package/test/golden/check-pass.stdout +3 -0
- package/test/golden/emit-smt-json-usage.exit +1 -0
- package/test/golden/emit-smt-json-usage.stderr +0 -0
- package/test/golden/emit-smt-json-usage.stdout +4 -0
- package/test/golden/emit-smt-json.exit +1 -0
- package/test/golden/emit-smt-json.stderr +0 -0
- package/test/golden/emit-smt-json.stdout +5 -0
- package/test/golden/emit-smt.exit +1 -0
- package/test/golden/emit-smt.stderr +0 -0
- package/test/golden/emit-smt.stdout +10 -0
- package/test/golden/help.exit +1 -0
- package/test/golden/help.stderr +0 -0
- package/test/golden/help.stdout +8 -0
- package/test/golden/lint-awkward.exit +1 -0
- package/test/golden/lint-awkward.stderr +0 -0
- package/test/golden/lint-awkward.stdout +29 -0
- package/test/golden/lint-chain.exit +1 -0
- package/test/golden/lint-chain.stderr +0 -0
- package/test/golden/lint-chain.stdout +56 -0
- package/test/golden/lint-checkout.exit +1 -0
- package/test/golden/lint-checkout.stderr +0 -0
- package/test/golden/lint-checkout.stdout +301 -0
- package/test/golden/lint-clean.exit +1 -0
- package/test/golden/lint-clean.stderr +0 -0
- package/test/golden/lint-clean.stdout +9 -0
- package/test/golden/lint-clear.exit +1 -0
- package/test/golden/lint-clear.stderr +0 -0
- package/test/golden/lint-clear.stdout +9 -0
- package/test/golden/lint-conflict-json.exit +1 -0
- package/test/golden/lint-conflict-json.stderr +0 -0
- package/test/golden/lint-conflict-json.stdout +80 -0
- package/test/golden/lint-conflict.exit +1 -0
- package/test/golden/lint-conflict.stderr +0 -0
- package/test/golden/lint-conflict.stdout +41 -0
- package/test/golden/lint-duplicate.exit +1 -0
- package/test/golden/lint-duplicate.stderr +0 -0
- package/test/golden/lint-duplicate.stdout +14 -0
- package/test/golden/lint-gap.exit +1 -0
- package/test/golden/lint-gap.stderr +0 -0
- package/test/golden/lint-gap.stdout +12 -0
- package/test/golden/lint-misuse.exit +1 -0
- package/test/golden/lint-misuse.stderr +0 -0
- package/test/golden/lint-misuse.stdout +18 -0
- package/test/golden/lint-negation.exit +1 -0
- package/test/golden/lint-negation.stderr +0 -0
- package/test/golden/lint-negation.stdout +13 -0
- package/test/golden/lint-quantity.exit +1 -0
- package/test/golden/lint-quantity.stderr +0 -0
- package/test/golden/lint-quantity.stdout +41 -0
- package/test/golden/lint-redundant.exit +1 -0
- package/test/golden/lint-redundant.stderr +0 -0
- package/test/golden/lint-redundant.stdout +14 -0
- package/test/golden/lint-repair-json.exit +1 -0
- package/test/golden/lint-repair-json.stderr +0 -0
- package/test/golden/lint-repair-json.stdout +127 -0
- package/test/golden/lint-repair.exit +1 -0
- package/test/golden/lint-repair.stderr +0 -0
- package/test/golden/lint-repair.stdout +54 -0
- package/test/golden/lint-repeat.exit +1 -0
- package/test/golden/lint-repeat.stderr +0 -0
- package/test/golden/lint-repeat.stdout +48 -0
- package/test/golden/lint-status.exit +1 -0
- package/test/golden/lint-status.stderr +0 -0
- package/test/golden/lint-status.stdout +44 -0
- package/test/golden/lint-unanswered.exit +1 -0
- package/test/golden/lint-unanswered.stderr +0 -0
- package/test/golden/lint-unanswered.stdout +12 -0
- package/test/golden/lint-unhandled.exit +1 -0
- package/test/golden/lint-unhandled.stderr +0 -0
- package/test/golden/lint-unhandled.stdout +12 -0
- package/test/golden/lint-unmeasured.exit +1 -0
- package/test/golden/lint-unmeasured.stderr +0 -0
- package/test/golden/lint-unmeasured.stdout +13 -0
- package/test/golden/lint-unused-json.exit +1 -0
- package/test/golden/lint-unused-json.stderr +0 -0
- package/test/golden/lint-unused-json.stdout +37 -0
- package/test/golden/lint-unused.exit +1 -0
- package/test/golden/lint-unused.stderr +0 -0
- package/test/golden/lint-unused.stdout +25 -0
- package/test/golden/lint-vacuous-json.exit +1 -0
- package/test/golden/lint-vacuous-json.stderr +0 -0
- package/test/golden/lint-vacuous-json.stdout +27 -0
- package/test/golden/lint-vacuous.exit +1 -0
- package/test/golden/lint-vacuous.stderr +0 -0
- package/test/golden/lint-vacuous.stdout +19 -0
- package/test/golden/lint-vague.exit +1 -0
- package/test/golden/lint-vague.stderr +0 -0
- package/test/golden/lint-vague.stdout +21 -0
- package/test/golden/lint-wide.exit +1 -0
- package/test/golden/lint-wide.stderr +0 -0
- package/test/golden/lint-wide.stdout +9 -0
- package/test/golden/lint-wording.exit +1 -0
- package/test/golden/lint-wording.stderr +0 -0
- package/test/golden/lint-wording.stdout +21 -0
- package/test/golden/malformed-scenario.exit +1 -0
- package/test/golden/malformed-scenario.stderr +1 -0
- package/test/golden/malformed-scenario.stdout +0 -0
- package/test/golden/malformed-spec.exit +1 -0
- package/test/golden/malformed-spec.stderr +1 -0
- package/test/golden/malformed-spec.stdout +0 -0
- package/test/golden/policy-counterexample.exit +1 -0
- package/test/golden/policy-counterexample.stderr +0 -0
- package/test/golden/policy-counterexample.stdout +13 -0
- package/test/golden/policy-covered.exit +1 -0
- package/test/golden/policy-covered.stderr +0 -0
- package/test/golden/policy-covered.stdout +19 -0
- package/test/golden/policy-holds.exit +1 -0
- package/test/golden/policy-holds.stderr +0 -0
- package/test/golden/policy-holds.stdout +3 -0
- package/test/golden/policy-json.exit +1 -0
- package/test/golden/policy-json.stderr +0 -0
- package/test/golden/policy-json.stdout +34 -0
- package/test/golden/policy-status.exit +1 -0
- package/test/golden/policy-status.stderr +0 -0
- package/test/golden/policy-status.stdout +16 -0
- package/test/golden/policy-vacuous.exit +1 -0
- package/test/golden/policy-vacuous.stderr +0 -0
- package/test/golden/policy-vacuous.stdout +3 -0
- package/test/golden/policy-wide.exit +1 -0
- package/test/golden/policy-wide.stderr +0 -0
- package/test/golden/policy-wide.stdout +15 -0
- package/test/golden/policy.exit +1 -0
- package/test/golden/policy.stderr +0 -0
- package/test/golden/policy.stdout +19 -0
- package/test/golden/scenarios-json.exit +1 -0
- package/test/golden/scenarios-json.stderr +0 -0
- package/test/golden/scenarios-json.stdout +75 -0
- package/test/golden/scenarios-status.exit +1 -0
- package/test/golden/scenarios-status.stderr +0 -0
- package/test/golden/scenarios-status.stdout +24 -0
- package/test/golden/scenarios.exit +1 -0
- package/test/golden/scenarios.stderr +0 -0
- package/test/golden/scenarios.stdout +26 -0
- package/test/golden/unit.exit +1 -0
- package/test/golden/unit.stderr +0 -0
- package/test/golden/unit.stdout +2 -0
- package/test/golden/unknown-phrase.exit +1 -0
- package/test/golden/unknown-phrase.stderr +1 -0
- package/test/golden/unknown-phrase.stdout +0 -0
- package/test/golden/usage.exit +1 -0
- package/test/golden/usage.stderr +9 -0
- package/test/golden/usage.stdout +0 -0
- package/test/run-all.ts +499 -0
- package/test/soundness.ts +375 -0
- package/test/unit.ts +1352 -0
- package/tsconfig.json +15 -0
package/src/jsonvalue.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small JSON reader for flat scenario objects.
|
|
3
|
+
* Values are booleans, integers, or strings. Nested values are rejected.
|
|
4
|
+
* This stays in the static ScriptC tier, which does not accept JSON.parse.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type JsonValue =
|
|
8
|
+
| { kind: "bool"; value: boolean }
|
|
9
|
+
| { kind: "int"; value: number }
|
|
10
|
+
| { kind: "string"; value: string };
|
|
11
|
+
|
|
12
|
+
export interface JsonObject {
|
|
13
|
+
keys: string[];
|
|
14
|
+
values: JsonValue[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type JsonResult = { ok: true; object: JsonObject } | { ok: false; line: number; message: string };
|
|
18
|
+
|
|
19
|
+
class Cur {
|
|
20
|
+
text: string;
|
|
21
|
+
i: number;
|
|
22
|
+
line: number;
|
|
23
|
+
|
|
24
|
+
constructor(text: string) {
|
|
25
|
+
this.text = text;
|
|
26
|
+
this.i = 0;
|
|
27
|
+
this.line = 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parseScenarioJson(text: string): JsonResult {
|
|
32
|
+
const source = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
33
|
+
const cur = new Cur(source);
|
|
34
|
+
skip(cur);
|
|
35
|
+
if (cur.i >= cur.text.length) return fail(cur, "expected an object");
|
|
36
|
+
if (cur.text.charCodeAt(cur.i) !== 123) return fail(cur, "expected an object");
|
|
37
|
+
cur.i++;
|
|
38
|
+
const keys: string[] = [];
|
|
39
|
+
const values: JsonValue[] = [];
|
|
40
|
+
skip(cur);
|
|
41
|
+
if (cur.i < cur.text.length && cur.text.charCodeAt(cur.i) === 125) {
|
|
42
|
+
cur.i++;
|
|
43
|
+
skip(cur);
|
|
44
|
+
if (cur.i < cur.text.length) return fail(cur, "unexpected trailing text");
|
|
45
|
+
return { ok: true, object: { keys: keys, values: values } };
|
|
46
|
+
}
|
|
47
|
+
while (cur.i < cur.text.length) {
|
|
48
|
+
skip(cur);
|
|
49
|
+
const key = parseString(cur);
|
|
50
|
+
if (key === null) return fail(cur, "expected a string key");
|
|
51
|
+
for (let i = 0; i < keys.length; i++) {
|
|
52
|
+
if (keys[i] === key) return fail(cur, "duplicate key " + quote(key));
|
|
53
|
+
}
|
|
54
|
+
skip(cur);
|
|
55
|
+
if (cur.i >= cur.text.length || cur.text.charCodeAt(cur.i) !== 58) return fail(cur, "expected ':'");
|
|
56
|
+
cur.i++;
|
|
57
|
+
skip(cur);
|
|
58
|
+
const value = parseValue(cur);
|
|
59
|
+
if (typeof value === "string") return fail(cur, value);
|
|
60
|
+
keys.push(key);
|
|
61
|
+
values.push(value);
|
|
62
|
+
skip(cur);
|
|
63
|
+
if (cur.i >= cur.text.length) return fail(cur, "expected '}'");
|
|
64
|
+
const mark = cur.text.charCodeAt(cur.i);
|
|
65
|
+
if (mark === 125) {
|
|
66
|
+
cur.i++;
|
|
67
|
+
skip(cur);
|
|
68
|
+
if (cur.i < cur.text.length) return fail(cur, "unexpected trailing text");
|
|
69
|
+
return { ok: true, object: { keys: keys, values: values } };
|
|
70
|
+
}
|
|
71
|
+
if (mark !== 44) return fail(cur, "expected ',' or '}'");
|
|
72
|
+
cur.i++;
|
|
73
|
+
skip(cur);
|
|
74
|
+
if (cur.i < cur.text.length && cur.text.charCodeAt(cur.i) === 125) return fail(cur, "trailing comma");
|
|
75
|
+
}
|
|
76
|
+
return fail(cur, "expected '}'");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseValue(cur: Cur): JsonValue | string {
|
|
80
|
+
if (cur.i >= cur.text.length) return "expected a value";
|
|
81
|
+
const code = cur.text.charCodeAt(cur.i);
|
|
82
|
+
if (code === 116 && cur.text.slice(cur.i, cur.i + 4) === "true") {
|
|
83
|
+
const after = cur.i + 4;
|
|
84
|
+
if (after < cur.text.length && isIdent(cur.text.charCodeAt(after))) return "expected a value";
|
|
85
|
+
cur.i = after;
|
|
86
|
+
return { kind: "bool", value: true };
|
|
87
|
+
}
|
|
88
|
+
if (code === 102 && cur.text.slice(cur.i, cur.i + 5) === "false") {
|
|
89
|
+
const after = cur.i + 5;
|
|
90
|
+
if (after < cur.text.length && isIdent(cur.text.charCodeAt(after))) return "expected a value";
|
|
91
|
+
cur.i = after;
|
|
92
|
+
return { kind: "bool", value: false };
|
|
93
|
+
}
|
|
94
|
+
if (code === 110 && cur.text.slice(cur.i, cur.i + 4) === "null") return "null is not allowed";
|
|
95
|
+
if (code === 34) {
|
|
96
|
+
const text = parseString(cur);
|
|
97
|
+
if (text === null) return "unterminated string";
|
|
98
|
+
return { kind: "string", value: text };
|
|
99
|
+
}
|
|
100
|
+
if (code === 45 || isDigit(code)) {
|
|
101
|
+
const parsed = parseIntLiteral(cur);
|
|
102
|
+
if (parsed === null) return "expected an integer";
|
|
103
|
+
return { kind: "int", value: parsed };
|
|
104
|
+
}
|
|
105
|
+
if (code === 123 || code === 91) return "expected a boolean, integer, or string";
|
|
106
|
+
return "expected a value";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseIntLiteral(cur: Cur): number | null {
|
|
110
|
+
const start = cur.i;
|
|
111
|
+
let negative = false;
|
|
112
|
+
if (cur.text.charCodeAt(cur.i) === 45) {
|
|
113
|
+
negative = true;
|
|
114
|
+
cur.i++;
|
|
115
|
+
if (cur.i >= cur.text.length || !isDigit(cur.text.charCodeAt(cur.i))) {
|
|
116
|
+
cur.i = start;
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (cur.text.charCodeAt(cur.i) === 48) {
|
|
121
|
+
cur.i++;
|
|
122
|
+
if (cur.i < cur.text.length && isDigit(cur.text.charCodeAt(cur.i))) {
|
|
123
|
+
cur.i = start;
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
if (!isDigit(cur.text.charCodeAt(cur.i))) {
|
|
128
|
+
cur.i = start;
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
while (cur.i < cur.text.length && isDigit(cur.text.charCodeAt(cur.i))) cur.i++;
|
|
132
|
+
}
|
|
133
|
+
if (cur.i < cur.text.length) {
|
|
134
|
+
const code = cur.text.charCodeAt(cur.i);
|
|
135
|
+
if (code === 46 || code === 101 || code === 69) {
|
|
136
|
+
cur.i = start;
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const digits = cur.text.slice(negative ? start + 1 : start, cur.i);
|
|
141
|
+
if (digits.length > 9) {
|
|
142
|
+
cur.i = start;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
let value = 0;
|
|
146
|
+
for (let i = 0; i < digits.length; i++) value = value * 10 + (digits.charCodeAt(i) - 48);
|
|
147
|
+
return negative ? -value : value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseString(cur: Cur): string | null {
|
|
151
|
+
if (cur.i >= cur.text.length || cur.text.charCodeAt(cur.i) !== 34) return null;
|
|
152
|
+
cur.i++;
|
|
153
|
+
let out = "";
|
|
154
|
+
while (cur.i < cur.text.length) {
|
|
155
|
+
const code = cur.text.charCodeAt(cur.i);
|
|
156
|
+
if (code === 34) {
|
|
157
|
+
cur.i++;
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
if (code === 10 || code === 13) return null;
|
|
161
|
+
if (code === 92) {
|
|
162
|
+
cur.i++;
|
|
163
|
+
if (cur.i >= cur.text.length) return null;
|
|
164
|
+
const esc = cur.text.charCodeAt(cur.i);
|
|
165
|
+
if (esc === 34 || esc === 92 || esc === 47) out += cur.text.slice(cur.i, cur.i + 1);
|
|
166
|
+
else if (esc === 110) out += "\n";
|
|
167
|
+
else if (esc === 114) out += "\r";
|
|
168
|
+
else if (esc === 116) out += "\t";
|
|
169
|
+
else if (esc === 117) {
|
|
170
|
+
if (cur.i + 4 >= cur.text.length) return null;
|
|
171
|
+
let hex = 0;
|
|
172
|
+
for (let h = 1; h <= 4; h++) {
|
|
173
|
+
const digit = hexValue(cur.text.charCodeAt(cur.i + h));
|
|
174
|
+
if (digit < 0) return null;
|
|
175
|
+
hex = hex * 16 + digit;
|
|
176
|
+
}
|
|
177
|
+
out += String.fromCharCode(hex);
|
|
178
|
+
cur.i += 4;
|
|
179
|
+
} else {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
cur.i++;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
out += cur.text.slice(cur.i, cur.i + 1);
|
|
186
|
+
cur.i++;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function skip(cur: Cur): void {
|
|
192
|
+
while (cur.i < cur.text.length) {
|
|
193
|
+
const code = cur.text.charCodeAt(cur.i);
|
|
194
|
+
if (code === 10) {
|
|
195
|
+
cur.line++;
|
|
196
|
+
cur.i++;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (code === 32 || code === 9 || code === 13) {
|
|
200
|
+
cur.i++;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function fail(cur: Cur, message: string): JsonResult {
|
|
208
|
+
return { ok: false, line: cur.line, message: message };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function quote(value: string): string {
|
|
212
|
+
return "\"" + value + "\"";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isDigit(code: number): boolean {
|
|
216
|
+
return code >= 48 && code <= 57;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function isIdent(code: number): boolean {
|
|
220
|
+
return isDigit(code) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code === 95;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function hexValue(code: number): number {
|
|
224
|
+
if (code >= 48 && code <= 57) return code - 48;
|
|
225
|
+
if (code >= 97 && code <= 102) return code - 87;
|
|
226
|
+
if (code >= 65 && code <= 70) return code - 55;
|
|
227
|
+
return -1;
|
|
228
|
+
}
|