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/test/unit.ts
ADDED
|
@@ -0,0 +1,1352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser, scenario check, SMT printer, and lint behavior.
|
|
3
|
+
* The native build of this file must print the same bytes as Node.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { bindScenario, evaluate } from "../src/check.ts";
|
|
8
|
+
import { parseProperty, parseSpec } from "../src/ears.ts";
|
|
9
|
+
import { analyzePolicy } from "../src/policy.ts";
|
|
10
|
+
import { parseScenarioJson } from "../src/jsonvalue.ts";
|
|
11
|
+
import { lintSpec } from "../src/lint.ts";
|
|
12
|
+
import { buildProblems } from "../src/report.ts";
|
|
13
|
+
import type { Requirement, Spec } from "../src/spec.ts";
|
|
14
|
+
import { listScenarios } from "../src/scenarios.ts";
|
|
15
|
+
import { printSmt } from "../src/smt.ts";
|
|
16
|
+
import { spaceSize } from "../src/solver.ts";
|
|
17
|
+
import { formatTerm } from "../src/term.ts";
|
|
18
|
+
import { runSoundness } from "./soundness.ts";
|
|
19
|
+
|
|
20
|
+
let passed = 0;
|
|
21
|
+
let failed = 0;
|
|
22
|
+
|
|
23
|
+
function applySentence(spec: Spec, id: string, sentence: string): Spec | null {
|
|
24
|
+
const parsed = parseProperty(id + " " + sentence, spec);
|
|
25
|
+
if (!parsed.ok || parsed.requirements.length !== 1) return null;
|
|
26
|
+
const requirements: Requirement[] = [];
|
|
27
|
+
for (let i = 0; i < spec.requirements.length; i++) {
|
|
28
|
+
if (spec.requirements[i].id === id) requirements.push(parsed.requirements[0]);
|
|
29
|
+
else requirements.push(spec.requirements[i]);
|
|
30
|
+
}
|
|
31
|
+
return { enums: spec.enums, symbols: spec.symbols, requirements: requirements };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasArg(flag: string): boolean {
|
|
35
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
36
|
+
if (process.argv[i] === flag) return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (hasArg("--soundness-only")) {
|
|
42
|
+
const started = Date.now();
|
|
43
|
+
const specs = runSoundness();
|
|
44
|
+
console.log("soundness specs: " + String(specs));
|
|
45
|
+
console.log("soundness ms: " + String(Date.now() - started));
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function check(name: string, cond: boolean): void {
|
|
50
|
+
if (cond) {
|
|
51
|
+
passed++;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
failed++;
|
|
55
|
+
console.error("FAIL " + name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mustSpec(text: string): Spec {
|
|
59
|
+
const parsed = parseSpec(text);
|
|
60
|
+
if (!parsed.ok) {
|
|
61
|
+
console.error("FAIL parse line " + String(parsed.line) + ": " + parsed.message);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
return parsed.spec;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseMessage(text: string): string {
|
|
68
|
+
const parsed = parseSpec(text);
|
|
69
|
+
if (parsed.ok) return "";
|
|
70
|
+
return String(parsed.line) + ": " + parsed.message;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function idsOf(spec: Spec): string[] {
|
|
74
|
+
const outcome = lintSpec(spec);
|
|
75
|
+
if (outcome.kind === "error") return [];
|
|
76
|
+
if (outcome.report.conflicts.length === 0) return [];
|
|
77
|
+
return outcome.report.conflicts[0].ids;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function scenario(spec: Spec, text: string): { violations: number; because: string } {
|
|
81
|
+
const json = parseScenarioJson(text);
|
|
82
|
+
if (!json.ok) return { violations: -1, because: json.message };
|
|
83
|
+
const bound = bindScenario(spec, json.object);
|
|
84
|
+
if (!bound.ok) return { violations: -1, because: bound.message };
|
|
85
|
+
const violations = evaluate(spec, bound.env);
|
|
86
|
+
if (violations.length === 0) return { violations: 0, because: "" };
|
|
87
|
+
return { violations: violations.length, because: violations[0].because };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const orderText = readFileSync("examples/order.ears.md", "utf8");
|
|
91
|
+
const order = mustSpec(orderText);
|
|
92
|
+
check("order has R1", order.requirements.length === 1 && order.requirements[0].id === "R1");
|
|
93
|
+
check("order R1 line", order.requirements[0].line === 7);
|
|
94
|
+
check("order symbols", order.symbols.length === 3);
|
|
95
|
+
check("order phrase", order.symbols[0].phrase === "an order is submitted" && order.symbols[2].phrase === "fulfill the order");
|
|
96
|
+
|
|
97
|
+
const failScenario = scenario(
|
|
98
|
+
order,
|
|
99
|
+
'{ "order_submitted": true, "inventory_available": true, "order_fulfilled": false }',
|
|
100
|
+
);
|
|
101
|
+
check("fail count", failScenario.violations === 1);
|
|
102
|
+
check(
|
|
103
|
+
"fail because",
|
|
104
|
+
failScenario.because === "An order was submitted and inventory was available, but the order was not fulfilled.",
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const passScenario = scenario(
|
|
108
|
+
order,
|
|
109
|
+
'{ "order_submitted": true, "inventory_available": true, "order_fulfilled": true }',
|
|
110
|
+
);
|
|
111
|
+
check("pass count", passScenario.violations === 0);
|
|
112
|
+
|
|
113
|
+
const quietScenario = scenario(
|
|
114
|
+
order,
|
|
115
|
+
'{ "order_submitted": false, "inventory_available": true, "order_fulfilled": false }',
|
|
116
|
+
);
|
|
117
|
+
check("antecedent false passes", quietScenario.violations === 0);
|
|
118
|
+
|
|
119
|
+
const missing = scenario(order, '{ "order_submitted": true, "order_fulfilled": false }');
|
|
120
|
+
check("missing symbol", missing.because === "missing inventory_available");
|
|
121
|
+
|
|
122
|
+
const extra = scenario(
|
|
123
|
+
order,
|
|
124
|
+
'{ "order_submitted": true, "inventory_available": true, "order_fulfilled": true, "extra": false }',
|
|
125
|
+
);
|
|
126
|
+
check("extra symbol", extra.because === "unexpected extra");
|
|
127
|
+
|
|
128
|
+
const wrongType = scenario(
|
|
129
|
+
order,
|
|
130
|
+
'{ "order_submitted": true, "inventory_available": true, "order_fulfilled": 1 }',
|
|
131
|
+
);
|
|
132
|
+
check("wrong type", wrongType.because === "order_fulfilled must be true or false");
|
|
133
|
+
|
|
134
|
+
const smt = printSmt("examples/order.ears.md", order);
|
|
135
|
+
check("smt logic", smt.indexOf("(set-logic ALL)") !== -1);
|
|
136
|
+
check("smt named", smt.indexOf(":named R1_L7") !== -1);
|
|
137
|
+
check(
|
|
138
|
+
"smt sentence",
|
|
139
|
+
smt.indexOf(
|
|
140
|
+
"line 7: WHEN an order is submitted AND inventory is available, THE Order System SHALL fulfill the order.",
|
|
141
|
+
) !== -1,
|
|
142
|
+
);
|
|
143
|
+
check("smt phrase", smt.indexOf('phrase "fulfill the order"') !== -1);
|
|
144
|
+
check(
|
|
145
|
+
"smt outcome",
|
|
146
|
+
smt.indexOf('outcome "the order was fulfilled" / "the order was not fulfilled"') !== -1,
|
|
147
|
+
);
|
|
148
|
+
check("smt implication", smt.indexOf("(=> (and order_submitted inventory_available) order_fulfilled)") !== -1);
|
|
149
|
+
|
|
150
|
+
const conflict = mustSpec(readFileSync("examples/order-conflict.ears.md", "utf8"));
|
|
151
|
+
const conflictLint = lintSpec(conflict);
|
|
152
|
+
check("conflict ran", conflictLint.kind === "report");
|
|
153
|
+
if (conflictLint.kind === "report") {
|
|
154
|
+
check("one conflict", conflictLint.report.conflicts.length === 1);
|
|
155
|
+
check("no gap beside conflict", conflictLint.report.gaps.length === 0);
|
|
156
|
+
const item = conflictLint.report.conflicts[0];
|
|
157
|
+
check("conflict ids", item.ids.length === 2 && item.ids[0] === "R2" && item.ids[1] === "R3");
|
|
158
|
+
check("not every situation", item.everySituation === false);
|
|
159
|
+
check("situation submitted", item.situation.length === 1 && item.situation[0].name === "order_submitted" && item.situation[0].value === "true");
|
|
160
|
+
check("example fulfilled", item.example.length === 2 && item.example[1].value === "true");
|
|
161
|
+
check("keeps R2", item.keeps.length === 1 && item.keeps[0] === "R2" && item.breaks === "R3");
|
|
162
|
+
check("clash voice", item.clash === "They clash when an order was submitted.");
|
|
163
|
+
check(
|
|
164
|
+
"requires voice",
|
|
165
|
+
item.requires.length === 2 &&
|
|
166
|
+
item.requires[0].value === "that the order was fulfilled" &&
|
|
167
|
+
item.requires[1].value === "that the order was not fulfilled",
|
|
168
|
+
);
|
|
169
|
+
check("asks who wins", item.fix.indexOf("ask which requirement should win") !== -1);
|
|
170
|
+
check("offers both keeps", item.fix.indexOf("To keep R2, remove R3.") !== -1 && item.fix.indexOf("To keep R3, remove R2.") !== -1);
|
|
171
|
+
const again = lintSpec(conflict);
|
|
172
|
+
check("deterministic", again.kind === "report" && again.report.conflicts[0].exampleJson === item.exampleJson);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const gapLint = lintSpec(order);
|
|
176
|
+
check("gap ran", gapLint.kind === "report");
|
|
177
|
+
if (gapLint.kind === "report") {
|
|
178
|
+
check("order has a gap", gapLint.report.gaps.length === 1 && gapLint.report.conflicts.length === 0);
|
|
179
|
+
const gap = gapLint.report.gaps[0];
|
|
180
|
+
check("gap output", gap.output === "order_fulfilled");
|
|
181
|
+
check(
|
|
182
|
+
"gap text",
|
|
183
|
+
gap.text ===
|
|
184
|
+
"Nothing says whether the order was fulfilled when an order was submitted and inventory was not available. Suggested fix: add a requirement for this situation, and ask what the system shall do. Do not choose the response.",
|
|
185
|
+
);
|
|
186
|
+
check(
|
|
187
|
+
"gap situation",
|
|
188
|
+
gap.situation.length === 2 &&
|
|
189
|
+
gap.situation[0].name === "order_submitted" &&
|
|
190
|
+
gap.situation[0].value === "true" &&
|
|
191
|
+
gap.situation[1].name === "inventory_available" &&
|
|
192
|
+
gap.situation[1].value === "false",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const covered = lintSpec(mustSpec(readFileSync("examples/order-covered.ears.md", "utf8")));
|
|
197
|
+
check(
|
|
198
|
+
"covered is clean",
|
|
199
|
+
covered.kind === "report" &&
|
|
200
|
+
covered.report.conflicts.length === 0 &&
|
|
201
|
+
covered.report.gaps.length === 0 &&
|
|
202
|
+
covered.report.misuse.length === 0 &&
|
|
203
|
+
covered.report.wording.length === 0 &&
|
|
204
|
+
covered.report.redundant.length === 0,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const misuse = lintSpec(mustSpec(readFileSync("examples/order-misuse.ears.md", "utf8")));
|
|
208
|
+
check(
|
|
209
|
+
"misuse when",
|
|
210
|
+
misuse.kind === "report" &&
|
|
211
|
+
misuse.report.misuse.length === 1 &&
|
|
212
|
+
misuse.report.misuse[0].id === "R4" &&
|
|
213
|
+
misuse.report.misuse[0].symbols.length === 1 &&
|
|
214
|
+
misuse.report.misuse[0].symbols[0] === "payment_declined" &&
|
|
215
|
+
misuse.report.misuse[0].text === '"payment fails" is an unwanted event. Write if ... then, not when.' &&
|
|
216
|
+
misuse.report.conflicts.length === 0,
|
|
217
|
+
);
|
|
218
|
+
const bareMisuse = lintSpec(
|
|
219
|
+
mustSpec('unwanted event payment_declined: Bool "Declined."\noutput order_rejected: Bool "Rejected."\n\nR4 When payment_declined, the Order System shall order_rejected.\n'),
|
|
220
|
+
);
|
|
221
|
+
check(
|
|
222
|
+
"misuse without a phrase",
|
|
223
|
+
bareMisuse.kind === "report" &&
|
|
224
|
+
bareMisuse.report.misuse.length === 1 &&
|
|
225
|
+
bareMisuse.report.misuse[0].text === "payment_declined is an unwanted event. Write if ... then, not when.",
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const payment = lintSpec(mustSpec(readFileSync("examples/payment-if.ears.md", "utf8")));
|
|
229
|
+
check(
|
|
230
|
+
"if then is clean",
|
|
231
|
+
payment.kind === "report" && payment.report.misuse.length === 0 && payment.report.conflicts.length === 0 && payment.report.gaps.length === 0,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const wording = lintSpec(mustSpec(readFileSync("examples/order-wording.ears.md", "utf8")));
|
|
235
|
+
check("wording ran", wording.kind === "report");
|
|
236
|
+
if (wording.kind === "report") {
|
|
237
|
+
check("two wording hits", wording.report.wording.length === 2);
|
|
238
|
+
check("vague quickly", wording.report.wording[0].word === "quickly" && wording.report.wording[0].kind === "vague");
|
|
239
|
+
check("solution database", wording.report.wording[1].word === "database" && wording.report.wording[1].kind === "solution");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const wide = mustSpec(readFileSync("examples/order-wide.ears.md", "utf8"));
|
|
243
|
+
check("wide space", spaceSize(wide) === 250001);
|
|
244
|
+
const wideLint = lintSpec(wide);
|
|
245
|
+
check(
|
|
246
|
+
"wide lint clean",
|
|
247
|
+
wideLint.kind === "report" &&
|
|
248
|
+
wideLint.report.conflicts.length === 0 &&
|
|
249
|
+
wideLint.report.gaps.length === 0 &&
|
|
250
|
+
wideLint.report.redundant.length === 0,
|
|
251
|
+
);
|
|
252
|
+
const widePolicy = analyzePolicy(wide, "", []);
|
|
253
|
+
check(
|
|
254
|
+
"wide policy picks the low end",
|
|
255
|
+
widePolicy.accepted !== null &&
|
|
256
|
+
widePolicy.accepted.code[0] === 0 &&
|
|
257
|
+
widePolicy.rejected !== null &&
|
|
258
|
+
widePolicy.rejected.code[0] === 1,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const exact = mustSpec('output quantity: Int 1..250000 "A count."\n\nR1 The Counter shall quantity = 1.\n');
|
|
262
|
+
check("exact limit", spaceSize(exact) === 250000);
|
|
263
|
+
const exactLint = lintSpec(exact);
|
|
264
|
+
check("exact lint clean", exactLint.kind === "report" && exactLint.report.conflicts.length === 0);
|
|
265
|
+
check("order space", spaceSize(order) === 8);
|
|
266
|
+
|
|
267
|
+
const selfText = 'output order_fulfilled: Bool "Done."\n\nR3 The Order System shall order_fulfilled and not order_fulfilled.\n';
|
|
268
|
+
const self = lintSpec(mustSpec(selfText));
|
|
269
|
+
check("self ran", self.kind === "report");
|
|
270
|
+
if (self.kind === "report") {
|
|
271
|
+
check("self id", self.report.conflicts.length === 1 && self.report.conflicts[0].ids.length === 1 && self.report.conflicts[0].ids[0] === "R3");
|
|
272
|
+
check("self every", self.report.conflicts[0].everySituation === true);
|
|
273
|
+
check("self asks", self.report.conflicts[0].fix.indexOf("Ask what should happen") !== -1);
|
|
274
|
+
check("self breaks", self.report.conflicts[0].keepsSentence === "This scenario breaks R3.");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const countText =
|
|
278
|
+
'output quantity: Int 1..3 "How many ship."\n\nR1 The Warehouse shall quantity = 1.\nR2 The Warehouse shall quantity = 2.\n';
|
|
279
|
+
const count = lintSpec(mustSpec(countText));
|
|
280
|
+
check("count ran", count.kind === "report");
|
|
281
|
+
if (count.kind === "report") {
|
|
282
|
+
check("count ids", count.report.conflicts.length === 1 && count.report.conflicts[0].ids[0] === "R1" && count.report.conflicts[0].ids[1] === "R2");
|
|
283
|
+
check("count every", count.report.conflicts[0].everySituation === true);
|
|
284
|
+
check("count example", count.report.conflicts[0].example.length === 1 && count.report.conflicts[0].example[0].value === "1");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const enumText =
|
|
288
|
+
'enum Decision { fulfill, reject }\noutput decision: Decision "The choice."\n\nR1 The Order System shall decision = fulfill.\nR2 The Order System shall decision = reject.\n';
|
|
289
|
+
const enumSpec = mustSpec(enumText);
|
|
290
|
+
const enumLint = lintSpec(enumSpec);
|
|
291
|
+
check("enum ran", enumLint.kind === "report");
|
|
292
|
+
if (enumLint.kind === "report") {
|
|
293
|
+
check("enum ids", enumLint.report.conflicts[0].ids[0] === "R1" && enumLint.report.conflicts[0].ids[1] === "R2");
|
|
294
|
+
check("enum example", enumLint.report.conflicts[0].example[0].value === "fulfill");
|
|
295
|
+
}
|
|
296
|
+
const enumSmt = printSmt("enum.md", enumSpec);
|
|
297
|
+
check("enum datatype", enumSmt.indexOf("(declare-datatypes ((Decision 0)) (((fulfill) (reject))))") !== -1);
|
|
298
|
+
check("enum assert", enumSmt.indexOf("(= decision fulfill)") !== -1);
|
|
299
|
+
|
|
300
|
+
const neg = mustSpec('input offset: Int -2..2 "A shift."\n\nR1 The Clock shall offset = -1.\n');
|
|
301
|
+
const negSmt = printSmt("neg.md", neg);
|
|
302
|
+
check("negative smt", negSmt.indexOf("(- 2)") !== -1 && negSmt.indexOf("(- 1)") !== -1);
|
|
303
|
+
|
|
304
|
+
const combined = mustSpec(
|
|
305
|
+
'state inventory_available: Bool "Stock."\n' +
|
|
306
|
+
'event order_submitted: Bool "Submitted."\n' +
|
|
307
|
+
'input quantity: Int 1..5 "Count."\n' +
|
|
308
|
+
"enum Region { us, eu }\n" +
|
|
309
|
+
'input region: Region "Where."\n' +
|
|
310
|
+
'output shipped: Int 1..5 "Sent."\n\n' +
|
|
311
|
+
"R5 While inventory_available, when order_submitted, if quantity >= 1, where region = us, then the Warehouse shall shipped = quantity.\n",
|
|
312
|
+
);
|
|
313
|
+
const combinedSmt = printSmt("combined.md", combined);
|
|
314
|
+
check("combined implication", combinedSmt.indexOf("(=> (and (and (and inventory_available order_submitted) (>= quantity 1)) (= region us)) (= shipped quantity))") !== -1);
|
|
315
|
+
check("combined term", formatTerm(combined.requirements[0].consequent) === "shipped = quantity");
|
|
316
|
+
|
|
317
|
+
const compare = mustSpec('input quantity: Int 1..3 "Count."\n\nR1 The Warehouse shall not quantity = 1.\n');
|
|
318
|
+
check("not compare format", formatTerm(compare.requirements[0].consequent) === "not (quantity = 1)");
|
|
319
|
+
const whenOne = scenario(compare, '{ "quantity": 1 }');
|
|
320
|
+
const whenTwo = scenario(compare, '{ "quantity": 2 }');
|
|
321
|
+
check("not compare fails at 1", whenOne.violations === 1);
|
|
322
|
+
check("not compare passes at 2", whenTwo.violations === 0);
|
|
323
|
+
|
|
324
|
+
const arithmetic = mustSpec('input quantity: Int 0..3 "Count."\noutput shipped: Int 0..4 "Sent."\n\nR1 The Warehouse shall shipped = quantity + 1.\n');
|
|
325
|
+
check("add format", formatTerm(arithmetic.requirements[0].consequent) === "shipped = quantity + 1");
|
|
326
|
+
check("add passes", scenario(arithmetic, '{ "quantity": 1, "shipped": 2 }').violations === 0);
|
|
327
|
+
check("add fails", scenario(arithmetic, '{ "quantity": 1, "shipped": 1 }').violations === 1);
|
|
328
|
+
|
|
329
|
+
const constantFalse = mustSpec('output done: Bool "Done."\n\nR1 The Box shall false.\n');
|
|
330
|
+
check("false because", scenario(constantFalse, '{ "done": true }').because === "the response is false.");
|
|
331
|
+
|
|
332
|
+
const triple = mustSpec(
|
|
333
|
+
'output flag: Bool "A flag."\n\nR1 The Box shall flag.\nR2 The Box shall not flag.\nR3 The Box shall flag and not flag.\n',
|
|
334
|
+
);
|
|
335
|
+
const tripleLint = lintSpec(triple);
|
|
336
|
+
check("triple ran", tripleLint.kind === "report");
|
|
337
|
+
if (tripleLint.kind === "report") {
|
|
338
|
+
check("triple count", tripleLint.report.conflicts.length === 2);
|
|
339
|
+
check("triple first", tripleLint.report.conflicts[0].ids.length === 1 && tripleLint.report.conflicts[0].ids[0] === "R3");
|
|
340
|
+
check(
|
|
341
|
+
"triple second",
|
|
342
|
+
tripleLint.report.conflicts[1].ids.length === 2 &&
|
|
343
|
+
tripleLint.report.conflicts[1].ids[0] === "R1" &&
|
|
344
|
+
tripleLint.report.conflicts[1].ids[1] === "R2",
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const both = mustSpec(
|
|
349
|
+
'unwanted event payment_declined: Bool "Declined."\noutput order_rejected: Bool "Rejected."\n\nR4 When payment_declined, if payment_declined, then the Order System shall order_rejected.\n',
|
|
350
|
+
);
|
|
351
|
+
const bothLint = lintSpec(both);
|
|
352
|
+
check("when plus if is not misuse", bothLint.kind === "report" && bothLint.report.misuse.length === 0);
|
|
353
|
+
|
|
354
|
+
check("should", parseMessage('event flag: Bool "Flag."\n\nR1 When flag, the Order System should flag.\n') === '3: expected "shall"');
|
|
355
|
+
check("then missing", parseMessage('event flag: Bool "Flag."\n\nR1 If flag, the Order System shall flag.\n') === '3: expected "then"');
|
|
356
|
+
check(
|
|
357
|
+
"then extra",
|
|
358
|
+
parseMessage('event flag: Bool "Flag."\n\nR1 Then the Order System shall flag.\n') === '3: unexpected "then" without an if clause',
|
|
359
|
+
);
|
|
360
|
+
check(
|
|
361
|
+
"clause order",
|
|
362
|
+
parseMessage('event flag: Bool "Flag."\nstate ready: Bool "Ready."\n\nR1 Where flag, when ready, the Order System shall flag.\n') ===
|
|
363
|
+
"4: expected while, when, if, where in that order",
|
|
364
|
+
);
|
|
365
|
+
check("duplicate id", parseMessage('output flag: Bool "Flag."\n\nR1 The Box shall flag.\nR1 The Box shall flag.\n') === "4: duplicate requirement R1");
|
|
366
|
+
check(
|
|
367
|
+
"duplicate symbol",
|
|
368
|
+
parseMessage('event flag: Bool "Flag."\nevent flag: Bool "Again."\n') === '2: "flag" is already a symbol',
|
|
369
|
+
);
|
|
370
|
+
check("unknown symbol", parseMessage('output flag: Bool "Flag."\n\nR1 The Box shall missing.\n') === '3: unknown symbol "missing"');
|
|
371
|
+
check("period", parseMessage('output flag: Bool "Flag."\n\nR1 The Box shall flag\n') === "3: expected a period");
|
|
372
|
+
check("event int", parseMessage('event count: Int 1..2 "Count."\n') === "1: events must be Bool");
|
|
373
|
+
check("range order", parseMessage('output count: Int 5..4 "Count."\n') === "1: integer range min is greater than max");
|
|
374
|
+
check("dotted id", mustSpec('output flag: Bool "Flag."\n\nR1.2 The Box shall flag.\n').requirements[0].id === "R1.2");
|
|
375
|
+
|
|
376
|
+
const badJson = parseScenarioJson("{");
|
|
377
|
+
check("bad json", badJson.ok === false && badJson.message === "expected '}'");
|
|
378
|
+
const trail = parseScenarioJson('{ "flag": true, }');
|
|
379
|
+
check("trailing comma", trail.ok === false && trail.message === "trailing comma");
|
|
380
|
+
const dup = parseScenarioJson('{ "flag": true, "flag": false }');
|
|
381
|
+
check("duplicate key", dup.ok === false && dup.message === 'duplicate key "flag"');
|
|
382
|
+
const empty = parseScenarioJson("{ }");
|
|
383
|
+
check("empty object", empty.ok === true && empty.object.keys.length === 0);
|
|
384
|
+
const lead = parseScenarioJson('{ "count": 01 }');
|
|
385
|
+
check("leading zero", lead.ok === false);
|
|
386
|
+
|
|
387
|
+
const outOfRange = mustSpec('output quantity: Int 1..3 "Count."\n\nR1 The Box shall quantity = 1.\n');
|
|
388
|
+
check("outside range", scenario(outOfRange, '{ "quantity": 0 }').because === "quantity is 0, outside 1..3");
|
|
389
|
+
|
|
390
|
+
const enumScenario = mustSpec('enum Decision { fulfill, reject }\noutput decision: Decision "Choice."\n\nR1 The Box shall decision = fulfill.\n');
|
|
391
|
+
check("bad variant", scenario(enumScenario, '{ "decision": "nope" }').because === 'decision is "nope", expected fulfill or reject');
|
|
392
|
+
check("enum pass", scenario(enumScenario, '{ "decision": "fulfill" }').violations === 0);
|
|
393
|
+
|
|
394
|
+
const named = mustSpec('output flag: Bool "Flag."\n\nR1 The API Gateway shall flag.\n');
|
|
395
|
+
const namedLint = lintSpec(named);
|
|
396
|
+
check("solution in the system name", namedLint.kind === "report" && namedLint.report.wording.length === 1 && namedLint.report.wording[0].word === "api");
|
|
397
|
+
|
|
398
|
+
check("conflict helper", idsOf(conflict).length === 2);
|
|
399
|
+
|
|
400
|
+
const natural = mustSpec(
|
|
401
|
+
'event order_submitted: Bool "Submitted." phrase " an order\tis submitted "\n' +
|
|
402
|
+
'output order_fulfilled: Bool "Done." phrase "fulfill the order"\n\n' +
|
|
403
|
+
"R1 When AN ORDER IS SUBMITTED, the Order System shall fulfill the order.\n",
|
|
404
|
+
);
|
|
405
|
+
check("phrase normalized", natural.symbols[0].phrase === "an order is submitted");
|
|
406
|
+
check("phrase case", formatTerm(natural.requirements[0].antecedent === null ? natural.requirements[0].consequent : natural.requirements[0].antecedent) === "an order is submitted");
|
|
407
|
+
check("negation phrase", formatTerm(mustSpec('output order_fulfilled: Bool "Done." phrase "fulfill the order"\n\nR1 The Order System shall not fulfill the order.\n').requirements[0].consequent) === "not fulfill the order");
|
|
408
|
+
|
|
409
|
+
const longest = mustSpec(
|
|
410
|
+
'event short: Bool "Short." phrase "an order"\n' +
|
|
411
|
+
'event long: Bool "Long." phrase "an order is submitted"\n' +
|
|
412
|
+
'output flag: Bool "Flag."\n\n' +
|
|
413
|
+
"R1 The Box shall an order is submitted.\n",
|
|
414
|
+
);
|
|
415
|
+
check("longest phrase", longest.requirements[0].consequent.kind === "sym" && longest.requirements[0].consequent.kind === "sym" && formatTerm(longest.requirements[0].consequent) === "an order is submitted");
|
|
416
|
+
|
|
417
|
+
const english = mustSpec(
|
|
418
|
+
'input quantity: Int 0..5 "Count." phrase "the quantity"\n' +
|
|
419
|
+
'output shipped: Int 0..5 "Sent." phrase "the number shipped"\n\n' +
|
|
420
|
+
"R1 The Box shall the quantity is at most 4.\n" +
|
|
421
|
+
"R2 The Box shall the quantity is at least 1.\n" +
|
|
422
|
+
"R3 The Box shall the quantity is less than 5.\n" +
|
|
423
|
+
"R4 The Box shall the quantity is greater than 0.\n" +
|
|
424
|
+
"R5 The Box shall the quantity is not 3.\n" +
|
|
425
|
+
"R6 The Box shall the number shipped is the quantity.\n" +
|
|
426
|
+
"R7 The Box shall the quantity <= 4.\n",
|
|
427
|
+
);
|
|
428
|
+
check("at most", formatTerm(english.requirements[0].consequent) === "the quantity is at most 4");
|
|
429
|
+
check("at least", formatTerm(english.requirements[1].consequent) === "the quantity is at least 1");
|
|
430
|
+
check("less than", formatTerm(english.requirements[2].consequent) === "the quantity is less than 5");
|
|
431
|
+
check("greater than", formatTerm(english.requirements[3].consequent) === "the quantity is greater than 0");
|
|
432
|
+
check("is not", formatTerm(english.requirements[4].consequent) === "the quantity is not 3");
|
|
433
|
+
check("phrase eq", formatTerm(english.requirements[5].consequent) === "the number shipped is the quantity");
|
|
434
|
+
check("symbolic still english", formatTerm(english.requirements[6].consequent) === "the quantity is at most 4");
|
|
435
|
+
check("english holds", scenario(english, '{ "quantity": 2, "shipped": 2 }').violations === 0);
|
|
436
|
+
check("english misses", scenario(english, '{ "quantity": 0, "shipped": 2 }').violations === 3);
|
|
437
|
+
|
|
438
|
+
const combinedFile = mustSpec(readFileSync("examples/order-combined.ears.md", "utf8"));
|
|
439
|
+
check("combined file phrase", formatTerm(combinedFile.requirements[0].consequent) === "the number shipped is the quantity");
|
|
440
|
+
check("combined file smt", printSmt("examples/order-combined.ears.md", combinedFile).indexOf("(= shipped quantity)") !== -1);
|
|
441
|
+
|
|
442
|
+
check(
|
|
443
|
+
"unknown phrase",
|
|
444
|
+
parseMessage(
|
|
445
|
+
'event order_submitted: Bool "Submitted." phrase "an order is submitted"\n' +
|
|
446
|
+
'output order_fulfilled: Bool "Done." phrase "fulfill the order"\n\n' +
|
|
447
|
+
"R1 The Order System shall an order was submitted.\n",
|
|
448
|
+
) === '4: unknown phrase "an order was submitted". Closest: "an order is submitted", "fulfill the order".',
|
|
449
|
+
);
|
|
450
|
+
check(
|
|
451
|
+
"unknown phrase stops at and",
|
|
452
|
+
parseMessage(
|
|
453
|
+
'event order_submitted: Bool "Submitted." phrase "an order is submitted"\n\nR1 The Order System shall an order was submitted and an order is submitted.\n',
|
|
454
|
+
).indexOf('unknown phrase "an order was submitted"') !== -1,
|
|
455
|
+
);
|
|
456
|
+
check("duplicate phrase", parseMessage('event a: Bool "A." phrase "an order is submitted"\nevent b: Bool "B." phrase "An Order Is Submitted"\n') === '2: "An Order Is Submitted" is already a phrase');
|
|
457
|
+
check("phrase reserved", parseMessage('event a: Bool "A." phrase "when"\n') === '1: "when" is a reserved word');
|
|
458
|
+
check("phrase starts with not", parseMessage('event a: Bool "A." phrase "not ready"\n') === '1: a phrase cannot start with "not"');
|
|
459
|
+
check("phrase punctuation", parseMessage('input quantity: Int 1..3 "Count." phrase "quantity = 1"\n') === "1: a phrase can only be words");
|
|
460
|
+
check("phrase empty", parseMessage('event a: Bool "A." phrase ""\n') === "1: expected a phrase");
|
|
461
|
+
check("phrase blank", parseMessage('event a: Bool "A." phrase " "\n') === "1: expected a phrase");
|
|
462
|
+
check("phrase symbol clash", parseMessage('event flag: Bool "F."\nevent other: Bool "O." phrase "flag"\n') === '2: "flag" is already a symbol');
|
|
463
|
+
check("name is phrase", parseMessage('event other: Bool "O." phrase "flag"\nevent flag: Bool "F."\n') === '2: "flag" is already a phrase');
|
|
464
|
+
check("phrase variant", parseMessage('enum Region { us, eu }\ninput region: Region "Where." phrase "us"\n') === '2: "us" is already an enum variant');
|
|
465
|
+
check("phrase enum", parseMessage('enum Region { us, eu }\noutput flag: Bool "F." phrase "region"\n') === '2: "region" is already an enum');
|
|
466
|
+
check("bad phrase escape", parseMessage('event a: Bool "A." phrase "bad\\q"\n') === "1: bad escape in phrase");
|
|
467
|
+
check("phrase trailing", parseMessage('event a: Bool "A." phrase "ready" extra\n') === "1: unexpected trailing text");
|
|
468
|
+
check(
|
|
469
|
+
"is at rewinds",
|
|
470
|
+
parseMessage('input quantity: Int 1..3 "Count." phrase "the quantity"\n\nR1 The Box shall the quantity is at home.\n') === '3: unexpected "is"',
|
|
471
|
+
);
|
|
472
|
+
check("reserved name phrase", parseMessage('event phrase: Bool "Nope."\n') === '1: "phrase" is a reserved word');
|
|
473
|
+
check("reserved name outcome", parseMessage('event outcome: Bool "Nope."\n') === '1: "outcome" is a reserved word');
|
|
474
|
+
check(
|
|
475
|
+
"outcome on int",
|
|
476
|
+
parseMessage('output quantity: Int 0..1 "Count." outcome "the quantity was set"\n') === "1: outcome is only for Bool",
|
|
477
|
+
);
|
|
478
|
+
check(
|
|
479
|
+
"outcome punctuation",
|
|
480
|
+
parseMessage('output done: Bool "Done." outcome "done!"\n') === "1: an outcome can only be words",
|
|
481
|
+
);
|
|
482
|
+
check(
|
|
483
|
+
"outcome then phrase",
|
|
484
|
+
parseMessage('output done: Bool "Done." outcome "done" phrase "do it"\n') === "1: unexpected trailing text",
|
|
485
|
+
);
|
|
486
|
+
check(
|
|
487
|
+
"outcome missing half",
|
|
488
|
+
parseMessage('output done: Bool "Done." outcome "done" /\n') === "1: expected an outcome in quotes",
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
const derived = mustSpec('output done: Bool "Done." phrase "fulfill the order" outcome "the order was fulfilled"\n\nR1 The Box shall fulfill the order.\n');
|
|
492
|
+
check(
|
|
493
|
+
"derived outcome",
|
|
494
|
+
derived.symbols[0].outcomeYes === "the order was fulfilled" && derived.symbols[0].outcomeNo === "the order was not fulfilled",
|
|
495
|
+
);
|
|
496
|
+
check("derived because", scenario(derived, '{ "done": false }').because === "The order was not fulfilled.");
|
|
497
|
+
|
|
498
|
+
const fromIs = mustSpec('output done: Bool "Done." outcome "the order is fulfilled"\n\nR1 The Box shall done.\n');
|
|
499
|
+
check("outcome from is", fromIs.symbols[0].outcomeNo === "the order is not fulfilled");
|
|
500
|
+
|
|
501
|
+
const bareOutcome = mustSpec('output done: Bool "Done." outcome "done"\n\nR1 The Box shall done.\n');
|
|
502
|
+
check("outcome prefix not", bareOutcome.symbols[0].outcomeNo === "not done");
|
|
503
|
+
check("outcome prefix because", scenario(bareOutcome, '{ "done": false }').because === "Not done.");
|
|
504
|
+
|
|
505
|
+
const slashed = mustSpec('output done: Bool "Done." outcome "yes please" / "no thanks"\n\nR1 The Box shall done.\n');
|
|
506
|
+
check("slash outcome", slashed.symbols[0].outcomeYes === "yes please" && slashed.symbols[0].outcomeNo === "no thanks");
|
|
507
|
+
check("slash because", scenario(slashed, '{ "done": false }').because === "No thanks.");
|
|
508
|
+
|
|
509
|
+
const defaultVoice = mustSpec(
|
|
510
|
+
'event order_submitted: Bool "Submits." phrase "an order is submitted"\n' +
|
|
511
|
+
'state inventory_available: Bool "Stock." phrase "inventory is available"\n' +
|
|
512
|
+
'output done: Bool "Done." phrase "mark the order done"\n\n' +
|
|
513
|
+
"R1 When an order is submitted and inventory is available, the Box shall mark the order done.\n",
|
|
514
|
+
);
|
|
515
|
+
check(
|
|
516
|
+
"default was because",
|
|
517
|
+
scenario(defaultVoice, '{ "order_submitted": true, "inventory_available": true, "done": false }').because ===
|
|
518
|
+
"An order was submitted and inventory was available, but not mark the order done.",
|
|
519
|
+
);
|
|
520
|
+
check(
|
|
521
|
+
"unphrased because",
|
|
522
|
+
scenario(mustSpec('output done: Bool "Done."\n\nR1 The Box shall done.\n'), '{ "done": false }').because === "done is false.",
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
const unhandled = lintSpec(mustSpec(readFileSync("examples/order-unhandled.ears.md", "utf8")));
|
|
526
|
+
check("unhandled ran", unhandled.kind === "report");
|
|
527
|
+
if (unhandled.kind === "report") {
|
|
528
|
+
check("unhandled only that event", unhandled.report.gaps.length === 1 && unhandled.report.conflicts.length === 0);
|
|
529
|
+
check("unhandled output", unhandled.report.gaps[0].output === "order_cancelled");
|
|
530
|
+
check(
|
|
531
|
+
"unhandled text",
|
|
532
|
+
unhandled.report.gaps[0].text ===
|
|
533
|
+
'No requirement handles "an order is cancelled". Suggested fix: add a requirement that says what the system shall do, or remove the event.',
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
const bareEvent = lintSpec(mustSpec('event ping: Bool "Ping."\noutput done: Bool "Done."\n\nR1 The Box shall done.\n'));
|
|
537
|
+
check(
|
|
538
|
+
"unhandled name",
|
|
539
|
+
bareEvent.kind === "report" &&
|
|
540
|
+
bareEvent.report.gaps.length === 1 &&
|
|
541
|
+
bareEvent.report.gaps[0].output === "ping" &&
|
|
542
|
+
bareEvent.report.gaps[0].text.indexOf("No requirement handles ping.") === 0,
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
const unanswered = lintSpec(mustSpec(readFileSync("examples/order-unanswered.ears.md", "utf8")));
|
|
546
|
+
check("unanswered ran", unanswered.kind === "report");
|
|
547
|
+
if (unanswered.kind === "report") {
|
|
548
|
+
check("unanswered gap", unanswered.report.gaps.length === 1 && unanswered.report.gaps[0].output === "payment_declined");
|
|
549
|
+
check(
|
|
550
|
+
"unanswered text",
|
|
551
|
+
unanswered.report.gaps[0].text ===
|
|
552
|
+
'No requirement states a response when "payment fails". Suggested fix: add an if ... then requirement, and ask what the system shall do. Do not choose the response.',
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const vague = lintSpec(mustSpec(readFileSync("examples/order-vague.ears.md", "utf8")));
|
|
557
|
+
check("vague ran", vague.kind === "report");
|
|
558
|
+
if (vague.kind === "report") {
|
|
559
|
+
check("vague count", vague.report.wording.length === 3 && vague.report.gaps.length === 0);
|
|
560
|
+
check("vague fast", vague.report.wording[0].word === "fast" && vague.report.wording[0].kind === "vague");
|
|
561
|
+
check("vague appropriate", vague.report.wording[1].word === "appropriate");
|
|
562
|
+
check("vague friendly", vague.report.wording[2].word === "user-friendly");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const quantityLint = lintSpec(mustSpec(readFileSync("examples/order-quantity.ears.md", "utf8")));
|
|
566
|
+
check("quantity ran", quantityLint.kind === "report");
|
|
567
|
+
if (quantityLint.kind === "report") {
|
|
568
|
+
const item = quantityLint.report.conflicts[0];
|
|
569
|
+
check("quantity ids", quantityLint.report.conflicts.length === 1 && quantityLint.report.gaps.length === 0 && item.ids[0] === "R4" && item.ids[1] === "R5");
|
|
570
|
+
check("quantity not every", item.everySituation === false);
|
|
571
|
+
check(
|
|
572
|
+
"quantity requires",
|
|
573
|
+
item.requires[0].value === "that at most 10 units were accepted" && item.requires[1].value === "that 12 units were accepted",
|
|
574
|
+
);
|
|
575
|
+
check("quantity example", item.example.length === 2 && item.example[1].name === "quantity" && item.example[1].value === "0");
|
|
576
|
+
check("quantity keeps", item.keeps.length === 1 && item.keeps[0] === "R4" && item.breaks === "R5");
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const statusLint = lintSpec(mustSpec(readFileSync("examples/order-status.ears.md", "utf8")));
|
|
580
|
+
check("status ran", statusLint.kind === "report");
|
|
581
|
+
if (statusLint.kind === "report") {
|
|
582
|
+
const item = statusLint.report.conflicts[0];
|
|
583
|
+
check("status ids", item.ids[0] === "R6" && item.ids[1] === "R7");
|
|
584
|
+
check("status every", item.everySituation === true);
|
|
585
|
+
check(
|
|
586
|
+
"status requires",
|
|
587
|
+
item.requires[0].value === "that the order was shipped" && item.requires[1].value === "that the order was cancelled",
|
|
588
|
+
);
|
|
589
|
+
check("status example", item.example.length === 1 && item.example[0].value === "shipped");
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const listed = analyzePolicy(order, "", []);
|
|
593
|
+
check(
|
|
594
|
+
"policy accepted",
|
|
595
|
+
listed.accepted !== null && listed.accepted.code[0] === 0 && listed.accepted.code[1] === 0 && listed.accepted.code[2] === 0,
|
|
596
|
+
);
|
|
597
|
+
check(
|
|
598
|
+
"policy rejected",
|
|
599
|
+
listed.rejected !== null && listed.rejected.code[0] === 1 && listed.rejected.code[1] === 1 && listed.rejected.code[2] === 0,
|
|
600
|
+
);
|
|
601
|
+
const alwaysProp = parseProperty(readFileSync("examples/order-always.ears.md", "utf8"), order);
|
|
602
|
+
check("always parsed", alwaysProp.ok === true);
|
|
603
|
+
if (alwaysProp.ok) {
|
|
604
|
+
const counter = analyzePolicy(order, "examples/order-always.ears.md", alwaysProp.requirements);
|
|
605
|
+
check(
|
|
606
|
+
"counterexample",
|
|
607
|
+
counter.holds === false &&
|
|
608
|
+
counter.counterexample !== null &&
|
|
609
|
+
counter.breaks.length === 1 &&
|
|
610
|
+
counter.breaks[0].because === "The order was not fulfilled.",
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
const sameProp = parseProperty(readFileSync("examples/order-same.ears.md", "utf8"), order);
|
|
614
|
+
if (sameProp.ok) {
|
|
615
|
+
const held = analyzePolicy(order, "examples/order-same.ears.md", sameProp.requirements);
|
|
616
|
+
check("property holds", held.holds === true && held.counterexample === null && held.note === "No scenario satisfies every requirement and violates the property.");
|
|
617
|
+
}
|
|
618
|
+
const statusSpec = mustSpec(readFileSync("examples/order-status.ears.md", "utf8"));
|
|
619
|
+
const statusList = analyzePolicy(statusSpec, "", []);
|
|
620
|
+
check("status no model", statusList.accepted === null && statusList.rejected !== null && statusList.breaks.length === 2);
|
|
621
|
+
const placedProp = parseProperty(readFileSync("examples/status-placed.ears.md", "utf8"), statusSpec);
|
|
622
|
+
if (placedProp.ok) {
|
|
623
|
+
const vacuous = analyzePolicy(statusSpec, "examples/status-placed.ears.md", placedProp.requirements);
|
|
624
|
+
check("vacuous property", vacuous.holds === true && vacuous.note === "No scenario satisfies every requirement, so none violates the property.");
|
|
625
|
+
}
|
|
626
|
+
const declaredProp = parseProperty('event flag: Bool "Flag."\n', order);
|
|
627
|
+
check("property declares", declaredProp.ok === false);
|
|
628
|
+
if (!declaredProp.ok) check("property declare message", declaredProp.message === "a property cannot declare symbols" && declaredProp.line === 1);
|
|
629
|
+
const emptyProp = parseProperty("", order);
|
|
630
|
+
check("empty property", emptyProp.ok === false);
|
|
631
|
+
if (!emptyProp.ok) check("empty property message", emptyProp.message === "expected a requirement");
|
|
632
|
+
|
|
633
|
+
const hyphen = mustSpec('event ready: Bool "Ready." phrase "user-friendly checkout"\n\nR1 The Box shall user-friendly checkout.\n');
|
|
634
|
+
check("hyphen phrase", formatTerm(hyphen.requirements[0].consequent) === "user-friendly checkout");
|
|
635
|
+
|
|
636
|
+
const chain = lintSpec(mustSpec(readFileSync("examples/order-chain.ears.md", "utf8")));
|
|
637
|
+
check("chain ran", chain.kind === "report");
|
|
638
|
+
if (chain.kind === "report") {
|
|
639
|
+
const item = chain.report.conflicts[0];
|
|
640
|
+
check(
|
|
641
|
+
"chain is one triple",
|
|
642
|
+
chain.report.conflicts.length === 1 &&
|
|
643
|
+
chain.report.gaps.length === 0 &&
|
|
644
|
+
chain.report.redundant.length === 0 &&
|
|
645
|
+
item.minimal === true &&
|
|
646
|
+
item.ids.length === 3 &&
|
|
647
|
+
item.ids[0] === "R1" &&
|
|
648
|
+
item.ids[1] === "R2" &&
|
|
649
|
+
item.ids[2] === "R3",
|
|
650
|
+
);
|
|
651
|
+
check("chain fix", item.fix.indexOf("Removing any one of R1, R2, and R3") !== -1);
|
|
652
|
+
check("chain keeps both", item.keeps.length === 2 && item.keeps[0] === "R1" && item.keeps[1] === "R2" && item.breaks === "R3");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const redundantLint = lintSpec(mustSpec(readFileSync("examples/order-redundant.ears.md", "utf8")));
|
|
656
|
+
check("redundant ran", redundantLint.kind === "report");
|
|
657
|
+
if (redundantLint.kind === "report") {
|
|
658
|
+
check(
|
|
659
|
+
"only R2 is redundant",
|
|
660
|
+
redundantLint.report.conflicts.length === 0 &&
|
|
661
|
+
redundantLint.report.gaps.length === 0 &&
|
|
662
|
+
redundantLint.report.redundant.length === 1 &&
|
|
663
|
+
redundantLint.report.redundant[0].id === "R2" &&
|
|
664
|
+
redundantLint.report.redundant[0].text === "R2 adds nothing. Whenever the other requirements hold, R2 holds too.",
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const listedScenarios = listScenarios(order);
|
|
669
|
+
check("scenario rows", listedScenarios.length === 3);
|
|
670
|
+
check(
|
|
671
|
+
"scenario accepted true",
|
|
672
|
+
listedScenarios[0].found === true &&
|
|
673
|
+
listedScenarios[0].kind === "accepted" &&
|
|
674
|
+
listedScenarios[0].trigger === "true" &&
|
|
675
|
+
listedScenarios[0].text === "An order was submitted and inventory was available, and the order was fulfilled.",
|
|
676
|
+
);
|
|
677
|
+
check(
|
|
678
|
+
"scenario rejected",
|
|
679
|
+
listedScenarios[2].found === true &&
|
|
680
|
+
listedScenarios[2].kind === "rejected" &&
|
|
681
|
+
listedScenarios[2].text === "An order was submitted and inventory was available, but the order was not fulfilled.",
|
|
682
|
+
);
|
|
683
|
+
const statusRows = listScenarios(statusSpec);
|
|
684
|
+
check(
|
|
685
|
+
"ubiquitous has no trigger false",
|
|
686
|
+
statusRows.length === 6 && statusRows[1].found === false && statusRows[1].text === "R6 has no trigger, so there is no case where the trigger is false.",
|
|
687
|
+
);
|
|
688
|
+
check("status scenarios dedupe", statusRows[5].sameAs === "R6 rejected, trigger true" && statusRows[5].found === true);
|
|
689
|
+
|
|
690
|
+
let manyText = 'event a: Bool "A happens." phrase "a happens"\n';
|
|
691
|
+
manyText += 'event b: Bool "B happens." phrase "b happens"\n';
|
|
692
|
+
manyText += 'output o: Bool "O holds." phrase "o holds"\n';
|
|
693
|
+
manyText += 'output p: Bool "P holds." phrase "p holds"\n\n';
|
|
694
|
+
manyText += "R1 When a happens, the Box shall o holds.\n";
|
|
695
|
+
manyText += "R2 When a happens, the Box shall not o holds.\n";
|
|
696
|
+
manyText += "R3 When b happens, the Box shall p holds.\n";
|
|
697
|
+
manyText += "R4 When b happens, the Box shall not p holds.\n";
|
|
698
|
+
for (let extra = 5; extra <= 13; extra++) manyText += "R" + String(extra) + " The Box shall true.\n";
|
|
699
|
+
const many = lintSpec(mustSpec(manyText));
|
|
700
|
+
check("thirteen requirements ran", many.kind === "report");
|
|
701
|
+
if (many.kind === "report") {
|
|
702
|
+
let pairO = false;
|
|
703
|
+
let pairP = false;
|
|
704
|
+
let onlyCores = many.report.conflicts.length === 2;
|
|
705
|
+
for (let i = 0; i < many.report.conflicts.length; i++) {
|
|
706
|
+
const ids = many.report.conflicts[i].ids;
|
|
707
|
+
if (!many.report.conflicts[i].minimal || ids.length !== 2) onlyCores = false;
|
|
708
|
+
if (ids.length === 2 && ids[0] === "R1" && ids[1] === "R2") pairO = true;
|
|
709
|
+
if (ids.length === 2 && ids[0] === "R3" && ids[1] === "R4") pairP = true;
|
|
710
|
+
}
|
|
711
|
+
check("thirteen requirements keep both pairs", pairO && pairP && onlyCores && many.report.redundant.length === 0);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const checkout = mustSpec(readFileSync("examples/checkout.ears.md", "utf8"));
|
|
715
|
+
check("checkout size", checkout.requirements.length === 50 && spaceSize(checkout) === 196608);
|
|
716
|
+
const checkoutLint = lintSpec(checkout);
|
|
717
|
+
check("checkout ran", checkoutLint.kind === "report");
|
|
718
|
+
if (checkoutLint.kind === "report") {
|
|
719
|
+
const groups = checkoutLint.report.conflicts;
|
|
720
|
+
let accept = false;
|
|
721
|
+
let seats = false;
|
|
722
|
+
let triple = false;
|
|
723
|
+
let shaped = groups.length === 3;
|
|
724
|
+
for (let i = 0; i < groups.length; i++) {
|
|
725
|
+
const ids = groups[i].ids;
|
|
726
|
+
if (!groups[i].minimal) shaped = false;
|
|
727
|
+
if (ids.length === 2 && ids[0] === "R1" && ids[1] === "R4") accept = true;
|
|
728
|
+
if (ids.length === 2 && ids[0] === "R15" && ids[1] === "R16") seats = true;
|
|
729
|
+
if (ids.length === 3 && ids[0] === "R8" && ids[1] === "R12" && ids[2] === "R14") triple = true;
|
|
730
|
+
}
|
|
731
|
+
const gapNames: string[] = [];
|
|
732
|
+
for (let i = 0; i < checkoutLint.report.gaps.length; i++) gapNames.push(checkoutLint.report.gaps[i].output);
|
|
733
|
+
const outside: string[] = [];
|
|
734
|
+
for (let i = 0; i < checkoutLint.report.redundant.length; i++) outside.push(checkoutLint.report.redundant[i].id);
|
|
735
|
+
let sawR10 = false;
|
|
736
|
+
let sawR25 = false;
|
|
737
|
+
let sawR49 = false;
|
|
738
|
+
let sawHidden = false;
|
|
739
|
+
for (let i = 0; i < outside.length; i++) {
|
|
740
|
+
if (outside[i] === "R10") sawR10 = true;
|
|
741
|
+
if (outside[i] === "R25") sawR25 = true;
|
|
742
|
+
if (outside[i] === "R49") sawR49 = true;
|
|
743
|
+
if (outside[i] === "R18" || outside[i] === "R1" || outside[i] === "R8") sawHidden = true;
|
|
744
|
+
}
|
|
745
|
+
check(
|
|
746
|
+
"checkout conflicts and gaps",
|
|
747
|
+
accept &&
|
|
748
|
+
seats &&
|
|
749
|
+
triple &&
|
|
750
|
+
shaped &&
|
|
751
|
+
gapNames.length === 2 &&
|
|
752
|
+
gapNames[0] === "receipt_sent" &&
|
|
753
|
+
gapNames[1] === "billed_seats" &&
|
|
754
|
+
checkoutLint.report.misuse.length === 0 &&
|
|
755
|
+
checkoutLint.report.wording.length === 0 &&
|
|
756
|
+
checkoutLint.report.duplicate.length === 0 &&
|
|
757
|
+
outside.length === 23 &&
|
|
758
|
+
sawR10 &&
|
|
759
|
+
sawR25 &&
|
|
760
|
+
sawR49 &&
|
|
761
|
+
!sawHidden,
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const clashText =
|
|
766
|
+
'output x: Int 0..1000000 "X." phrase "the level"\n\nR1 The Box shall the level is at most 10.\nR2 The Box shall the level is at least 500000.\n';
|
|
767
|
+
const clash = lintSpec(mustSpec(clashText));
|
|
768
|
+
check("wide output clash ran", clash.kind === "report");
|
|
769
|
+
if (clash.kind === "report") {
|
|
770
|
+
check(
|
|
771
|
+
"wide output clash is one pair",
|
|
772
|
+
clash.report.conflicts.length === 1 &&
|
|
773
|
+
clash.report.conflicts[0].minimal &&
|
|
774
|
+
clash.report.conflicts[0].ids.length === 2 &&
|
|
775
|
+
clash.report.conflicts[0].ids[0] === "R1" &&
|
|
776
|
+
clash.report.conflicts[0].ids[1] === "R2" &&
|
|
777
|
+
clash.report.conflicts[0].everySituation,
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const driverText =
|
|
782
|
+
'input count: Int 0..300000 "Count." phrase "the count"\noutput ok: Bool "Ok."\n\nR1 The Box shall the count is 0.\n';
|
|
783
|
+
const driverWide = lintSpec(mustSpec(driverText));
|
|
784
|
+
check("wide driver ran", driverWide.kind === "report");
|
|
785
|
+
if (driverWide.kind === "report") {
|
|
786
|
+
const conflict = driverWide.report.conflicts.length === 1 ? driverWide.report.conflicts[0] : null;
|
|
787
|
+
const gap = driverWide.report.gaps.length === 1 ? driverWide.report.gaps[0] : null;
|
|
788
|
+
check(
|
|
789
|
+
"wide driver singleton",
|
|
790
|
+
conflict !== null &&
|
|
791
|
+
conflict.minimal &&
|
|
792
|
+
conflict.ids.length === 1 &&
|
|
793
|
+
conflict.ids[0] === "R1" &&
|
|
794
|
+
conflict.situation.length === 1 &&
|
|
795
|
+
conflict.situation[0].name === "count" &&
|
|
796
|
+
conflict.situation[0].value === "1" &&
|
|
797
|
+
gap !== null &&
|
|
798
|
+
gap.output === "ok" &&
|
|
799
|
+
gap.situation.length === 1 &&
|
|
800
|
+
gap.situation[0].value === "300000" &&
|
|
801
|
+
spaceSize(mustSpec(driverText)) === 600002,
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const thresholdText =
|
|
806
|
+
'input count: Int 0..300000 "Count." phrase "the count"\noutput ok: Bool "Ok." phrase "it is ok"\n\nR1 When the count is at least 10, the Box shall it is ok.\n';
|
|
807
|
+
const threshold = lintSpec(mustSpec(thresholdText));
|
|
808
|
+
check("wide threshold ran", threshold.kind === "report");
|
|
809
|
+
if (threshold.kind === "report") {
|
|
810
|
+
const gap = threshold.report.gaps.length === 1 ? threshold.report.gaps[0] : null;
|
|
811
|
+
check(
|
|
812
|
+
"wide threshold gap stops at the boundary",
|
|
813
|
+
threshold.report.conflicts.length === 0 &&
|
|
814
|
+
gap !== null &&
|
|
815
|
+
gap.output === "ok" &&
|
|
816
|
+
gap.situation.length === 1 &&
|
|
817
|
+
gap.situation[0].value === "9",
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const coupledText =
|
|
822
|
+
'input quantity: Int 0..1000000 "Q."\noutput shipped: Int 0..1000000 "S."\n\nR1 The Box shall shipped = quantity + 1.\n';
|
|
823
|
+
const coupled = lintSpec(mustSpec(coupledText));
|
|
824
|
+
check("wide coupled ran", coupled.kind === "report");
|
|
825
|
+
if (coupled.kind === "report") {
|
|
826
|
+
const conflict = coupled.report.conflicts.length === 1 ? coupled.report.conflicts[0] : null;
|
|
827
|
+
const accepted = analyzePolicy(mustSpec(coupledText), "", []).accepted;
|
|
828
|
+
check(
|
|
829
|
+
"wide coupled equality",
|
|
830
|
+
conflict !== null &&
|
|
831
|
+
conflict.minimal &&
|
|
832
|
+
conflict.ids.length === 1 &&
|
|
833
|
+
conflict.ids[0] === "R1" &&
|
|
834
|
+
conflict.situation.length === 1 &&
|
|
835
|
+
conflict.situation[0].value === "1000000" &&
|
|
836
|
+
accepted !== null &&
|
|
837
|
+
accepted.code[0] === 0 &&
|
|
838
|
+
accepted.code[1] === 1,
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
let wideMany = 'event flag: Bool "Flag." phrase "the flag"\n';
|
|
843
|
+
wideMany += 'input count: Int 0..300000 "Count." phrase "the count"\n';
|
|
844
|
+
wideMany += 'output o: Bool "O."\n';
|
|
845
|
+
wideMany += 'output p: Bool "P."\n\n';
|
|
846
|
+
wideMany += "R1 When the flag, the Box shall o.\n";
|
|
847
|
+
wideMany += "R2 When the flag, the Box shall not o.\n";
|
|
848
|
+
wideMany += "R3 When the count is at least 10, the Box shall p.\n";
|
|
849
|
+
wideMany += "R4 When the count is at least 10, the Box shall not p.\n";
|
|
850
|
+
for (let extra = 5; extra <= 13; extra++) wideMany += "R" + String(extra) + " The Box shall true.\n";
|
|
851
|
+
const wideManyLint = lintSpec(mustSpec(wideMany));
|
|
852
|
+
check("wide thirteen ran", wideManyLint.kind === "report");
|
|
853
|
+
if (wideManyLint.kind === "report") {
|
|
854
|
+
let pairO = false;
|
|
855
|
+
let pairP = false;
|
|
856
|
+
let onlyCores = wideManyLint.report.conflicts.length === 2;
|
|
857
|
+
for (let i = 0; i < wideManyLint.report.conflicts.length; i++) {
|
|
858
|
+
const ids = wideManyLint.report.conflicts[i].ids;
|
|
859
|
+
if (!wideManyLint.report.conflicts[i].minimal || ids.length !== 2) onlyCores = false;
|
|
860
|
+
if (ids.length === 2 && ids[0] === "R1" && ids[1] === "R2") pairO = true;
|
|
861
|
+
if (ids.length === 2 && ids[0] === "R3" && ids[1] === "R4") pairP = true;
|
|
862
|
+
}
|
|
863
|
+
let gapO = false;
|
|
864
|
+
let gapP = false;
|
|
865
|
+
for (let i = 0; i < wideManyLint.report.gaps.length; i++) {
|
|
866
|
+
const gap = wideManyLint.report.gaps[i];
|
|
867
|
+
if (gap.output === "o" && gap.situation.length === 2 && gap.situation[0].value === "false" && gap.situation[1].value === "300000") {
|
|
868
|
+
gapO = true;
|
|
869
|
+
}
|
|
870
|
+
if (gap.output === "p" && gap.situation.length === 2 && gap.situation[0].value === "true" && gap.situation[1].value === "9") {
|
|
871
|
+
gapP = true;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
check("wide thirteen keeps both pairs", pairO && pairP && onlyCores && gapO && gapP && wideManyLint.report.redundant.length === 0);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const vacuousLint = lintSpec(mustSpec(readFileSync("examples/order-vacuous.ears.md", "utf8")));
|
|
878
|
+
check("vacuous ran", vacuousLint.kind === "report");
|
|
879
|
+
if (vacuousLint.kind === "report") {
|
|
880
|
+
check("vacuous count", vacuousLint.report.vacuous.length === 2 && vacuousLint.report.conflicts.length === 0 && vacuousLint.report.gaps.length === 0);
|
|
881
|
+
check("vacuous ids", vacuousLint.report.vacuous[0].id === "R1" && vacuousLint.report.vacuous[1].id === "R2");
|
|
882
|
+
check(
|
|
883
|
+
"vacuous text",
|
|
884
|
+
vacuousLint.report.vacuous[0].text === "R1 can never trigger. Its condition is impossible for every allowed value." &&
|
|
885
|
+
vacuousLint.report.vacuous[1].text.indexOf("R2 can never trigger.") === 0,
|
|
886
|
+
);
|
|
887
|
+
check("vacuous fix", vacuousLint.report.vacuous[0].fix.indexOf("Suggested fix: rewrite the condition in R1") === 0);
|
|
888
|
+
check(
|
|
889
|
+
"vacuous is not redundant",
|
|
890
|
+
vacuousLint.report.redundant.length === 0 && vacuousLint.report.duplicate.length === 0,
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
const vacuousCopy = lintSpec(
|
|
894
|
+
mustSpec(
|
|
895
|
+
'input quantity: Int 1..5 "Count." phrase "the quantity"\n' +
|
|
896
|
+
'output order_accepted: Bool "Accepted." phrase "accept the order"\n\n' +
|
|
897
|
+
"R1 When the quantity is at least 9, the Checkout shall accept the order.\n" +
|
|
898
|
+
"R2 When the quantity is at least 9, the Checkout shall accept the order.\n" +
|
|
899
|
+
"R3 The Checkout shall accept the order.\n" +
|
|
900
|
+
"R4 The Checkout shall accept the order.\n",
|
|
901
|
+
),
|
|
902
|
+
);
|
|
903
|
+
check("vacuous copy ran", vacuousCopy.kind === "report");
|
|
904
|
+
if (vacuousCopy.kind === "report") {
|
|
905
|
+
check(
|
|
906
|
+
"vacuous copy is not a duplicate",
|
|
907
|
+
vacuousCopy.report.vacuous.length === 2 &&
|
|
908
|
+
vacuousCopy.report.vacuous[0].id === "R1" &&
|
|
909
|
+
vacuousCopy.report.vacuous[1].id === "R2" &&
|
|
910
|
+
vacuousCopy.report.redundant.length === 0 &&
|
|
911
|
+
vacuousCopy.report.duplicate.length === 1 &&
|
|
912
|
+
vacuousCopy.report.duplicate[0].id === "R4" &&
|
|
913
|
+
vacuousCopy.report.duplicate[0].sameAs === "R3",
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const possible = lintSpec(
|
|
918
|
+
mustSpec(
|
|
919
|
+
'state cart_open: Bool "The cart is open." phrase "the cart is open"\noutput order_accepted: Bool "The order is accepted." phrase "accept the order"\n\nR1 When the cart is open, the Checkout shall accept the order.\nR2 When not the cart is open, the Checkout shall not accept the order.\n',
|
|
920
|
+
),
|
|
921
|
+
);
|
|
922
|
+
check(
|
|
923
|
+
"possible condition is not vacuous",
|
|
924
|
+
possible.kind === "report" && possible.report.vacuous.length === 0 && possible.report.unused.length === 0 && possible.report.gaps.length === 0,
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
const unusedLint = lintSpec(mustSpec(readFileSync("examples/order-unused.ears.md", "utf8")));
|
|
928
|
+
check("unused ran", unusedLint.kind === "report");
|
|
929
|
+
if (unusedLint.kind === "report") {
|
|
930
|
+
check("unused no conflict or gap", unusedLint.report.conflicts.length === 0 && unusedLint.report.gaps.length === 0 && unusedLint.report.vacuous.length === 0);
|
|
931
|
+
check("unused count", unusedLint.report.unused.length === 4);
|
|
932
|
+
check("unused state", unusedLint.report.unused[0].name === "loyalty" && unusedLint.report.unused[0].kind === "state");
|
|
933
|
+
check("unused input", unusedLint.report.unused[1].name === "region" && unusedLint.report.unused[1].kind === "input");
|
|
934
|
+
check(
|
|
935
|
+
"unused output never set",
|
|
936
|
+
unusedLint.report.unused[2].name === "receipt_sent" &&
|
|
937
|
+
unusedLint.report.unused[2].kind === "output" &&
|
|
938
|
+
unusedLint.report.unused[2].text.indexOf("Nothing sets ") === 0,
|
|
939
|
+
);
|
|
940
|
+
check("unused variant", unusedLint.report.unused[3].name === "Lane.refund" && unusedLint.report.unused[3].kind === "variant");
|
|
941
|
+
check("unused fix", unusedLint.report.unused[3].fix.indexOf("Suggested fix: use refund") === 0);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const embedded = mustSpec(
|
|
945
|
+
'state cancelling: Bool "Cancels." phrase "the customer is cancelling"\noutput done: Bool "Done."\n\nR1 When the customer is not cancelling, the Box shall done.\nR2 When not the customer is cancelling, the Box shall done.\n',
|
|
946
|
+
);
|
|
947
|
+
check(
|
|
948
|
+
"embedded is not",
|
|
949
|
+
embedded.requirements[0].antecedent !== null &&
|
|
950
|
+
embedded.requirements[0].antecedent.kind === "not" &&
|
|
951
|
+
embedded.requirements[1].antecedent !== null &&
|
|
952
|
+
embedded.requirements[1].antecedent.kind === "not" &&
|
|
953
|
+
formatTerm(embedded.requirements[0].antecedent) === formatTerm(embedded.requirements[1].antecedent),
|
|
954
|
+
);
|
|
955
|
+
const seated = mustSpec(
|
|
956
|
+
'output billed_seats: Int 1..3 "Seats." phrase "the billed seats" as "bill {op} {value} seats" says "{value} seats were billed"\n\nR1 The Box shall bill 1 seat.\nR2 The Box shall bill at most 2 seats.\nR3 The Box shall bill 3 seats.\n',
|
|
957
|
+
);
|
|
958
|
+
check("seat singular", formatTerm(seated.requirements[0].consequent) === "the billed seats is 1");
|
|
959
|
+
check("seats at most", formatTerm(seated.requirements[1].consequent) === "the billed seats is at most 2");
|
|
960
|
+
check("seats equal", seated.requirements[2].consequent.kind === "eq");
|
|
961
|
+
const seatClash = mustSpec(
|
|
962
|
+
'output billed_seats: Int 1..3 "Seats." phrase "the billed seats" as "bill {op} {value} seats" says "{value} seats were billed"\n\nR1 The Box shall bill at most 2 seats.\nR2 The Box shall bill 3 seats.\n',
|
|
963
|
+
);
|
|
964
|
+
const seatedLint = lintSpec(seatClash);
|
|
965
|
+
check(
|
|
966
|
+
"says seats",
|
|
967
|
+
seatedLint.kind === "report" &&
|
|
968
|
+
seatedLint.report.conflicts.length === 1 &&
|
|
969
|
+
seatedLint.report.conflicts[0].requires[0].value === "that at most 2 seats were billed" &&
|
|
970
|
+
seatedLint.report.conflicts[0].requires[1].value === "that 3 seats were billed",
|
|
971
|
+
);
|
|
972
|
+
const routed = mustSpec(
|
|
973
|
+
'enum Lane { hold, fulfill }\noutput lane: Lane "Lane." phrase "the lane" as "route the order to the {value} lane" says "the order was routed to the {value} lane"\n\nR1 The Box shall route the order to the fulfill lane.\nR2 The Box shall the lane is hold.\n',
|
|
974
|
+
);
|
|
975
|
+
check("route phrase", formatTerm(routed.requirements[0].consequent) === "the lane is fulfill");
|
|
976
|
+
check("old lane still parses", formatTerm(routed.requirements[1].consequent) === "the lane is hold");
|
|
977
|
+
const routedLint = lintSpec(routed);
|
|
978
|
+
check(
|
|
979
|
+
"says lane",
|
|
980
|
+
routedLint.kind === "report" &&
|
|
981
|
+
routedLint.report.conflicts.length === 1 &&
|
|
982
|
+
routedLint.report.conflicts[0].requires[0].value === "that the order was routed to the fulfill lane",
|
|
983
|
+
);
|
|
984
|
+
const plainPlural = mustSpec(
|
|
985
|
+
'output billed_seats: Int 1..3 "Seats." phrase "the billed seats"\n\nR1 The Box shall the billed seats is 3.\nR2 The Box shall the billed seats is at most 2.\n',
|
|
986
|
+
);
|
|
987
|
+
const plainLint = lintSpec(plainPlural);
|
|
988
|
+
check(
|
|
989
|
+
"plural default",
|
|
990
|
+
plainLint.kind === "report" &&
|
|
991
|
+
plainLint.report.conflicts.length === 1 &&
|
|
992
|
+
plainLint.report.conflicts[0].requires[0].value === "that the billed seats were set to 3" &&
|
|
993
|
+
plainLint.report.conflicts[0].requires[1].value === "that the billed seats were at most 2",
|
|
994
|
+
);
|
|
995
|
+
const linked = mustSpec(
|
|
996
|
+
'input quantity: Int 0..5 "Count." phrase "the quantity"\noutput shipped: Int 0..6 "Sent." phrase "the number shipped" says "{value} units were shipped" as "ship {value}"\n\nR1 The Box shall ship the quantity.\n',
|
|
997
|
+
);
|
|
998
|
+
check("linked ignores says", formatTerm(linked.requirements[0].consequent) === "the number shipped is the quantity");
|
|
999
|
+
const formLint = lintSpec(mustSpec(readFileSync("examples/order-awkward.ears.md", "utf8")));
|
|
1000
|
+
check("awkward ran", formLint.kind === "report");
|
|
1001
|
+
if (formLint.kind === "report") {
|
|
1002
|
+
check("awkward conflicts", formLint.report.conflicts.length === 0 && formLint.report.gaps.length === 0);
|
|
1003
|
+
let shalls = 0;
|
|
1004
|
+
let whens = 0;
|
|
1005
|
+
let nots = 0;
|
|
1006
|
+
for (let i = 0; i < formLint.report.wording.length; i++) {
|
|
1007
|
+
const item = formLint.report.wording[i];
|
|
1008
|
+
if (item.kind === "form" && item.word === "shall") shalls++;
|
|
1009
|
+
if (item.kind === "form" && item.word === "when") whens++;
|
|
1010
|
+
if (item.kind === "form" && item.word === "not") nots++;
|
|
1011
|
+
}
|
|
1012
|
+
let negations = 0;
|
|
1013
|
+
for (let i = 0; i < formLint.report.wording.length; i++) {
|
|
1014
|
+
if (formLint.report.wording[i].kind === "negation") negations++;
|
|
1015
|
+
}
|
|
1016
|
+
check("awkward forms", shalls === 2 && whens === 2 && nots === 0 && negations === 1 && formLint.report.wording.length === 5);
|
|
1017
|
+
}
|
|
1018
|
+
const clearLint = lintSpec(mustSpec(readFileSync("examples/order-clear.ears.md", "utf8")));
|
|
1019
|
+
check(
|
|
1020
|
+
"clear form",
|
|
1021
|
+
clearLint.kind === "report" &&
|
|
1022
|
+
clearLint.report.wording.length === 0 &&
|
|
1023
|
+
clearLint.report.conflicts.length === 0 &&
|
|
1024
|
+
clearLint.report.gaps.length === 0 &&
|
|
1025
|
+
clearLint.report.unused.length === 0,
|
|
1026
|
+
);
|
|
1027
|
+
check("action on int", parseMessage('output n: Int 0..1 "N." action "count it"\n') === "1: action is only for Bool");
|
|
1028
|
+
check("says on bool", parseMessage('output done: Bool "D." says "done {value}"\n') === "1: says is only for Int or enum");
|
|
1029
|
+
check("as on bool", parseMessage('output done: Bool "D." as "do {value}"\n') === "1: as is only for Int or enum");
|
|
1030
|
+
check("says needs value", parseMessage('output n: Int 0..1 "N." says "the count"\n') === "1: says needs {value}");
|
|
1031
|
+
check("as needs value", parseMessage('output n: Int 0..1 "N." as "count the items"\n') === "1: as needs {value}");
|
|
1032
|
+
check("reserved name as", parseMessage('output as: Int 0..1 "N."\n') === '1: "as" is a reserved word');
|
|
1033
|
+
check("reserved name says", parseMessage('event says: Bool "S."\n') === '1: "says" is a reserved word');
|
|
1034
|
+
check("reserved name action", parseMessage('event action: Bool "A."\n') === '1: "action" is a reserved word');
|
|
1035
|
+
const acted = mustSpec(
|
|
1036
|
+
'output started: Bool "Starts." phrase "the subscription is started" action "start the subscription"\n\nR1 When start the subscription, the Box shall start the subscription.\n',
|
|
1037
|
+
);
|
|
1038
|
+
check(
|
|
1039
|
+
"action phrase",
|
|
1040
|
+
acted.requirements[0].antecedent !== null &&
|
|
1041
|
+
acted.requirements[0].antecedent.kind === "sym" &&
|
|
1042
|
+
acted.requirements[0].consequent.kind === "sym",
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
const repeatLint = lintSpec(mustSpec(readFileSync("examples/order-repeat.ears.md", "utf8")));
|
|
1046
|
+
check("repeat under conflict ran", repeatLint.kind === "report");
|
|
1047
|
+
if (repeatLint.kind === "report") {
|
|
1048
|
+
check(
|
|
1049
|
+
"repeat survives the clash",
|
|
1050
|
+
repeatLint.report.conflicts.length === 1 &&
|
|
1051
|
+
repeatLint.report.conflicts[0].ids[0] === "R1" &&
|
|
1052
|
+
repeatLint.report.conflicts[0].ids[1] === "R2" &&
|
|
1053
|
+
repeatLint.report.redundant.length === 1 &&
|
|
1054
|
+
repeatLint.report.redundant[0].id === "R5" &&
|
|
1055
|
+
repeatLint.report.redundant[0].text ===
|
|
1056
|
+
"R5 adds nothing. Whenever the requirements outside a conflict hold, R5 holds too." &&
|
|
1057
|
+
repeatLint.report.duplicate.length === 0 &&
|
|
1058
|
+
repeatLint.report.gaps.length === 0,
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
const copyLint = lintSpec(mustSpec(readFileSync("examples/order-duplicate.ears.md", "utf8")));
|
|
1062
|
+
check("duplicate ran", copyLint.kind === "report");
|
|
1063
|
+
if (copyLint.kind === "report") {
|
|
1064
|
+
check(
|
|
1065
|
+
"exact copy is not called redundant",
|
|
1066
|
+
copyLint.report.duplicate.length === 1 &&
|
|
1067
|
+
copyLint.report.duplicate[0].id === "R2" &&
|
|
1068
|
+
copyLint.report.duplicate[0].sameAs === "R1" &&
|
|
1069
|
+
copyLint.report.duplicate[0].text === "R2 duplicates R1. The condition and the response are the same." &&
|
|
1070
|
+
copyLint.report.redundant.length === 0 &&
|
|
1071
|
+
copyLint.report.conflicts.length === 0 &&
|
|
1072
|
+
copyLint.report.gaps.length === 0,
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
const tripleCopy = lintSpec(
|
|
1076
|
+
mustSpec(
|
|
1077
|
+
'event order_submitted: Bool "Submitted." phrase "an order is submitted"\n' +
|
|
1078
|
+
'output order_fulfilled: Bool "Fulfilled." phrase "fulfill the order"\n\n' +
|
|
1079
|
+
"R1 When an order is submitted, the Box shall fulfill the order.\n" +
|
|
1080
|
+
"R2 When an order is submitted, the Box shall fulfill the order.\n" +
|
|
1081
|
+
"R3 When an order is submitted, the Box shall fulfill the order.\n" +
|
|
1082
|
+
"R4 When an order is not submitted, the Box shall not fulfill the order.\n",
|
|
1083
|
+
),
|
|
1084
|
+
);
|
|
1085
|
+
check(
|
|
1086
|
+
"three copies name the first",
|
|
1087
|
+
tripleCopy.kind === "report" &&
|
|
1088
|
+
tripleCopy.report.duplicate.length === 2 &&
|
|
1089
|
+
tripleCopy.report.duplicate[0].sameAs === "R1" &&
|
|
1090
|
+
tripleCopy.report.duplicate[1].id === "R3" &&
|
|
1091
|
+
tripleCopy.report.duplicate[1].sameAs === "R1" &&
|
|
1092
|
+
tripleCopy.report.redundant.length === 0,
|
|
1093
|
+
);
|
|
1094
|
+
const negationLint = lintSpec(mustSpec(readFileSync("examples/order-negation.ears.md", "utf8")));
|
|
1095
|
+
check("negated trigger ran", negationLint.kind === "report");
|
|
1096
|
+
if (negationLint.kind === "report") {
|
|
1097
|
+
check(
|
|
1098
|
+
"when not a member",
|
|
1099
|
+
negationLint.report.wording.length === 1 &&
|
|
1100
|
+
negationLint.report.wording[0].kind === "negation" &&
|
|
1101
|
+
negationLint.report.wording[0].word === "not" &&
|
|
1102
|
+
negationLint.report.wording[0].line === 6 &&
|
|
1103
|
+
negationLint.report.conflicts.length === 0 &&
|
|
1104
|
+
negationLint.report.gaps.length === 0,
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
const whileNot = lintSpec(
|
|
1108
|
+
mustSpec(
|
|
1109
|
+
'state member: Bool "A member." phrase "a member is here"\n' +
|
|
1110
|
+
'output open: Bool "Open." phrase "open the desk"\n\n' +
|
|
1111
|
+
"R1 While not a member is here, the Desk shall open the desk.\n" +
|
|
1112
|
+
"R2 While a member is here, the Desk shall not open the desk.\n",
|
|
1113
|
+
),
|
|
1114
|
+
);
|
|
1115
|
+
check(
|
|
1116
|
+
"while not stays a form issue",
|
|
1117
|
+
whileNot.kind === "report" && whileNot.report.wording.length === 1 && whileNot.report.wording[0].kind === "form",
|
|
1118
|
+
);
|
|
1119
|
+
const bareWhile = lintSpec(
|
|
1120
|
+
mustSpec(
|
|
1121
|
+
'event ping: Bool "Ping." phrase "a member asks"\n' +
|
|
1122
|
+
'output open: Bool "Open." phrase "open the desk"\n\n' +
|
|
1123
|
+
"R1 While not a member asks, the Desk shall open the desk.\n" +
|
|
1124
|
+
"R2 While a member asks, the Desk shall not open the desk.\n",
|
|
1125
|
+
),
|
|
1126
|
+
);
|
|
1127
|
+
check(
|
|
1128
|
+
"while not without is",
|
|
1129
|
+
bareWhile.kind === "report" && bareWhile.report.wording.length === 1 && bareWhile.report.wording[0].kind === "negation",
|
|
1130
|
+
);
|
|
1131
|
+
const whereNot = lintSpec(
|
|
1132
|
+
mustSpec(
|
|
1133
|
+
'input region: Int 1..2 "Region." phrase "the region"\n' +
|
|
1134
|
+
'event ping: Bool "Ping." phrase "a member asks"\n' +
|
|
1135
|
+
'output open: Bool "Open." phrase "open the desk"\n\n' +
|
|
1136
|
+
"R1 Where not a member asks, the Desk shall open the desk.\n" +
|
|
1137
|
+
"R2 Where a member asks, the Desk shall not open the desk.\n",
|
|
1138
|
+
),
|
|
1139
|
+
);
|
|
1140
|
+
check(
|
|
1141
|
+
"where not",
|
|
1142
|
+
whereNot.kind === "report" && whereNot.report.wording.length === 1 && whereNot.report.wording[0].kind === "negation",
|
|
1143
|
+
);
|
|
1144
|
+
const ifNot = lintSpec(
|
|
1145
|
+
mustSpec(
|
|
1146
|
+
'unwanted event ping: Bool "Ping." phrase "a member asks"\n' +
|
|
1147
|
+
'output open: Bool "Open." phrase "open the desk"\n\n' +
|
|
1148
|
+
"R1 If not a member asks, then the Desk shall open the desk.\n" +
|
|
1149
|
+
"R2 If a member asks, then the Desk shall not open the desk.\n",
|
|
1150
|
+
),
|
|
1151
|
+
);
|
|
1152
|
+
check(
|
|
1153
|
+
"if not",
|
|
1154
|
+
ifNot.kind === "report" && ifNot.report.wording.length === 1 && ifNot.report.wording[0].kind === "negation",
|
|
1155
|
+
);
|
|
1156
|
+
const isNot = lintSpec(
|
|
1157
|
+
mustSpec(
|
|
1158
|
+
'state fine_owed: Bool "A fine." phrase "a fine is owed"\n' +
|
|
1159
|
+
'output open: Bool "Open." phrase "open the desk"\n\n' +
|
|
1160
|
+
"R1 While a fine is not owed, the Desk shall open the desk.\n" +
|
|
1161
|
+
"R2 While a fine is owed, the Desk shall not open the desk.\n",
|
|
1162
|
+
),
|
|
1163
|
+
);
|
|
1164
|
+
check("is not is not a bare negation", isNot.kind === "report" && isNot.report.wording.length === 0 && isNot.report.gaps.length === 0);
|
|
1165
|
+
const measured = lintSpec(mustSpec(readFileSync("examples/order-unmeasured.ears.md", "utf8")));
|
|
1166
|
+
check("unmeasured ran", measured.kind === "report");
|
|
1167
|
+
if (measured.kind === "report") {
|
|
1168
|
+
check(
|
|
1169
|
+
"as needed and not the bound phrase",
|
|
1170
|
+
measured.report.wording.length === 1 &&
|
|
1171
|
+
measured.report.wording[0].word === "as needed" &&
|
|
1172
|
+
measured.report.wording[0].kind === "vague" &&
|
|
1173
|
+
measured.report.gaps.length === 0,
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
const boundNeeded = lintSpec(
|
|
1177
|
+
mustSpec(
|
|
1178
|
+
'event order_submitted: Bool "Submitted." phrase "an order is submitted"\n' +
|
|
1179
|
+
'output shipped: Bool "Left." phrase "ship as needed"\n\n' +
|
|
1180
|
+
"R1 When an order is submitted, the Box shall ship as needed.\n" +
|
|
1181
|
+
"R2 When an order is not submitted, the Box shall not ship as needed.\n",
|
|
1182
|
+
),
|
|
1183
|
+
);
|
|
1184
|
+
check(
|
|
1185
|
+
"bound as needed",
|
|
1186
|
+
boundNeeded.kind === "report" && boundNeeded.report.wording.length === 0 && boundNeeded.report.gaps.length === 0,
|
|
1187
|
+
);
|
|
1188
|
+
const looseFast = lintSpec(
|
|
1189
|
+
mustSpec(
|
|
1190
|
+
'event order_submitted: Bool "Submitted." phrase "an order is submitted"\n' +
|
|
1191
|
+
'output order_fulfilled: Bool "Fulfilled." phrase "fulfill the order"\n\n' +
|
|
1192
|
+
"R1 When an order is submitted, the Fast Desk shall fulfill the order.\n" +
|
|
1193
|
+
"R2 When an order is not submitted, the Desk shall not fulfill the order.\n",
|
|
1194
|
+
),
|
|
1195
|
+
);
|
|
1196
|
+
check(
|
|
1197
|
+
"unbound fast in the sentence",
|
|
1198
|
+
looseFast.kind === "report" &&
|
|
1199
|
+
looseFast.report.wording.length === 1 &&
|
|
1200
|
+
looseFast.report.wording[0].word === "fast" &&
|
|
1201
|
+
looseFast.report.wording[0].kind === "vague",
|
|
1202
|
+
);
|
|
1203
|
+
|
|
1204
|
+
const repairLint = lintSpec(mustSpec(readFileSync("examples/order-repair.ears.md", "utf8")));
|
|
1205
|
+
check("repair example ran", repairLint.kind === "report");
|
|
1206
|
+
if (repairLint.kind === "report") {
|
|
1207
|
+
const item = repairLint.report.conflicts[0];
|
|
1208
|
+
let drops = 0;
|
|
1209
|
+
let strengthenR1 = false;
|
|
1210
|
+
let strengthenR2 = false;
|
|
1211
|
+
for (let i = 0; i < item.repairs.length; i++) {
|
|
1212
|
+
const repair = item.repairs[i];
|
|
1213
|
+
if (repair.kind === "drop") drops++;
|
|
1214
|
+
if (repair.kind === "strengthen" && repair.id === "R1" && repair.sentence.indexOf("and the copy is not out on loan") !== -1) {
|
|
1215
|
+
strengthenR1 = true;
|
|
1216
|
+
}
|
|
1217
|
+
if (repair.kind === "strengthen" && repair.id === "R2" && repair.sentence.indexOf("and the copy is not in the building") !== -1) {
|
|
1218
|
+
strengthenR2 = true;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
check(
|
|
1222
|
+
"repair example candidates",
|
|
1223
|
+
repairLint.report.conflicts.length === 1 &&
|
|
1224
|
+
item.ids[0] === "R1" &&
|
|
1225
|
+
item.ids[1] === "R2" &&
|
|
1226
|
+
drops === 2 &&
|
|
1227
|
+
strengthenR1 &&
|
|
1228
|
+
strengthenR2 &&
|
|
1229
|
+
item.repairs.length === 4,
|
|
1230
|
+
);
|
|
1231
|
+
const problems = buildProblems("examples/order-repair.ears.md", mustSpec(readFileSync("examples/order-repair.ears.md", "utf8")), repairLint.report);
|
|
1232
|
+
check(
|
|
1233
|
+
"repair report asks and does not choose",
|
|
1234
|
+
problems.text.indexOf("Which requirement should win?") !== -1 &&
|
|
1235
|
+
problems.text.indexOf("This report does not choose.") !== -1 &&
|
|
1236
|
+
problems.text.indexOf("Candidate repairs follow. This report does not choose one.") !== -1 &&
|
|
1237
|
+
problems.text.indexOf("and the copy is not out on loan") !== -1 &&
|
|
1238
|
+
problems.text.indexOf("keeps ") === -1 &&
|
|
1239
|
+
problems.text.indexOf("breaks ") === -1,
|
|
1240
|
+
);
|
|
1241
|
+
let r1Sentence = "";
|
|
1242
|
+
for (let i = 0; i < item.repairs.length; i++) {
|
|
1243
|
+
if (item.repairs[i].kind === "strengthen" && item.repairs[i].id === "R1") r1Sentence = item.repairs[i].sentence;
|
|
1244
|
+
}
|
|
1245
|
+
const strengthened = applySentence(mustSpec(readFileSync("examples/order-repair.ears.md", "utf8")), "R1", r1Sentence);
|
|
1246
|
+
const again = strengthened === null ? null : lintSpec(strengthened);
|
|
1247
|
+
check(
|
|
1248
|
+
"strengthen removes that conflict",
|
|
1249
|
+
again !== null &&
|
|
1250
|
+
again.kind === "report" &&
|
|
1251
|
+
again.report.conflicts.length === 0,
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const sameTrigger = lintSpec(mustSpec(readFileSync("examples/order-conflict.ears.md", "utf8")));
|
|
1256
|
+
check("same trigger has no strengthen", sameTrigger.kind === "report");
|
|
1257
|
+
if (sameTrigger.kind === "report") {
|
|
1258
|
+
const repairs = sameTrigger.report.conflicts[0].repairs;
|
|
1259
|
+
let onlyDrops = repairs.length === 2;
|
|
1260
|
+
for (let i = 0; i < repairs.length; i++) {
|
|
1261
|
+
if (repairs[i].kind !== "drop") onlyDrops = false;
|
|
1262
|
+
}
|
|
1263
|
+
check("same trigger repairs are the two drops", onlyDrops && repairs[0].id === "R2" && repairs[1].id === "R3");
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const boundRepair = lintSpec(
|
|
1267
|
+
mustSpec(
|
|
1268
|
+
'input length: Int 1..10 "How long." phrase "the length"\n' +
|
|
1269
|
+
'output ok: Bool "Allowed." phrase "allow it" outcome "it was allowed" / "it was not allowed"\n\n' +
|
|
1270
|
+
"R1 When the length is at least 1, the Desk shall allow it.\n" +
|
|
1271
|
+
"R2 When the length is at least 5, the Desk shall not allow it.\n",
|
|
1272
|
+
),
|
|
1273
|
+
);
|
|
1274
|
+
check("int bound repair ran", boundRepair.kind === "report");
|
|
1275
|
+
if (boundRepair.kind === "report") {
|
|
1276
|
+
const repairs = boundRepair.report.conflicts[0].repairs;
|
|
1277
|
+
let saw = false;
|
|
1278
|
+
let strengthenedR2 = false;
|
|
1279
|
+
for (let i = 0; i < repairs.length; i++) {
|
|
1280
|
+
if (repairs[i].kind === "strengthen" && repairs[i].id === "R1" && repairs[i].sentence.indexOf("less than 5") !== -1) saw = true;
|
|
1281
|
+
if (repairs[i].kind === "strengthen" && repairs[i].id === "R2") strengthenedR2 = true;
|
|
1282
|
+
}
|
|
1283
|
+
check("int bound strengthens only the wider requirement", saw && !strengthenedR2);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const enumRepair = lintSpec(
|
|
1287
|
+
mustSpec(
|
|
1288
|
+
"enum Lane { fast, slow }\n" +
|
|
1289
|
+
'input lane: Lane "Which lane." phrase "the lane"\n' +
|
|
1290
|
+
'state ready: Bool "Ready." phrase "it is ready"\n' +
|
|
1291
|
+
'output go: Bool "Go." phrase "allow it" outcome "it was allowed" / "it was not allowed"\n\n' +
|
|
1292
|
+
"R1 When it is ready, the Desk shall allow it.\n" +
|
|
1293
|
+
"R2 When it is ready and the lane is fast, the Desk shall not allow it.\n",
|
|
1294
|
+
),
|
|
1295
|
+
);
|
|
1296
|
+
check("enum repair ran", enumRepair.kind === "report");
|
|
1297
|
+
if (enumRepair.kind === "report") {
|
|
1298
|
+
const repairs = enumRepair.report.conflicts[0].repairs;
|
|
1299
|
+
let saw = false;
|
|
1300
|
+
for (let i = 0; i < repairs.length; i++) {
|
|
1301
|
+
if (repairs[i].kind === "strengthen" && repairs[i].id === "R1" && repairs[i].text.indexOf("the lane is slow") !== -1) saw = true;
|
|
1302
|
+
}
|
|
1303
|
+
check("enum equality strengthens the wider requirement", saw);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
const openRepair = lintSpec(
|
|
1307
|
+
mustSpec(
|
|
1308
|
+
'state ready: Bool "Ready." phrase "it is ready"\n' +
|
|
1309
|
+
'output go: Bool "Go." phrase "allow it" outcome "it was allowed" / "it was not allowed"\n\n' +
|
|
1310
|
+
"R1 The Desk shall allow it.\n" +
|
|
1311
|
+
"R2 When it is ready, the Desk shall not allow it.\n",
|
|
1312
|
+
),
|
|
1313
|
+
);
|
|
1314
|
+
check("ubiquitous repair ran", openRepair.kind === "report");
|
|
1315
|
+
if (openRepair.kind === "report") {
|
|
1316
|
+
const repairs = openRepair.report.conflicts[0].repairs;
|
|
1317
|
+
let saw = false;
|
|
1318
|
+
for (let i = 0; i < repairs.length; i++) {
|
|
1319
|
+
if (repairs[i].kind === "strengthen" && repairs[i].sentence === "When it is not ready, the Desk shall allow it.") saw = true;
|
|
1320
|
+
}
|
|
1321
|
+
check("ubiquitous repair adds a when clause", saw);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
const orRepair = lintSpec(
|
|
1325
|
+
mustSpec(
|
|
1326
|
+
'state ready: Bool "Ready." phrase "it is ready"\n' +
|
|
1327
|
+
'state open: Bool "Open." phrase "the desk is open"\n' +
|
|
1328
|
+
'output go: Bool "Go." phrase "allow it" outcome "it was allowed" / "it was not allowed"\n\n' +
|
|
1329
|
+
"R1 When it is ready or the desk is open, the Desk shall allow it.\n" +
|
|
1330
|
+
"R2 When it is ready and the desk is open, the Desk shall not allow it.\n",
|
|
1331
|
+
),
|
|
1332
|
+
);
|
|
1333
|
+
check("or repair ran", orRepair.kind === "report");
|
|
1334
|
+
if (orRepair.kind === "report") {
|
|
1335
|
+
const repairs = orRepair.report.conflicts[0].repairs;
|
|
1336
|
+
let paren = false;
|
|
1337
|
+
for (let i = 0; i < repairs.length; i++) {
|
|
1338
|
+
if (repairs[i].kind === "strengthen" && repairs[i].sentence.indexOf("(it is ready or the desk is open)") !== -1) paren = true;
|
|
1339
|
+
}
|
|
1340
|
+
check("or repair parenthesizes the old condition", paren);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
const soundnessSpecs = runSoundness();
|
|
1344
|
+
check("soundness specs", soundnessSpecs === 1010);
|
|
1345
|
+
console.log("soundness specs: " + String(soundnessSpecs));
|
|
1346
|
+
|
|
1347
|
+
if (failed !== 0) {
|
|
1348
|
+
console.error(String(failed) + " failed");
|
|
1349
|
+
process.exit(1);
|
|
1350
|
+
}
|
|
1351
|
+
console.log("passed " + String(passed));
|
|
1352
|
+
process.exit(0);
|