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/report.ts
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain-language list of logical problems.
|
|
3
|
+
* Contradictions, uncovered situations, impossible conditions, repeats, and
|
|
4
|
+
* exact duplicates come from lint. A stricter repeat is still explained
|
|
5
|
+
* here by naming the wider requirement. A contradiction asks which
|
|
6
|
+
* requirement should win and does not choose one. Candidate repairs are
|
|
7
|
+
* listed after that question, and none of them is selected.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Conflict, Duplicate, Gap, LintReport, Redundant, Vacuous } from "./lint.ts";
|
|
11
|
+
import type { Repair } from "./repair.ts";
|
|
12
|
+
import type { Requirement, Spec, Term } from "./spec.ts";
|
|
13
|
+
import { satisfiable } from "./decide.ts";
|
|
14
|
+
import { termsEqual } from "./term.ts";
|
|
15
|
+
import { displayQuote, englishList, formatInt } from "./text.ts";
|
|
16
|
+
|
|
17
|
+
export interface ProblemDocument {
|
|
18
|
+
text: string;
|
|
19
|
+
count: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Repeated {
|
|
23
|
+
id: string;
|
|
24
|
+
line: number;
|
|
25
|
+
sentence: string;
|
|
26
|
+
byId: string;
|
|
27
|
+
byLine: number;
|
|
28
|
+
bySentence: string;
|
|
29
|
+
text: string;
|
|
30
|
+
fix: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildProblems(specPath: string, spec: Spec, report: LintReport): ProblemDocument {
|
|
34
|
+
const repeated = collectRepeated(spec, report);
|
|
35
|
+
const count =
|
|
36
|
+
report.conflicts.length + report.gaps.length + report.vacuous.length + repeated.length + report.duplicate.length;
|
|
37
|
+
const lines: string[] = [];
|
|
38
|
+
lines.push("# Logical problems");
|
|
39
|
+
lines.push("");
|
|
40
|
+
lines.push("Spec: " + specPath);
|
|
41
|
+
lines.push("");
|
|
42
|
+
if (count === 0) {
|
|
43
|
+
lines.push("No logical problems.");
|
|
44
|
+
return { text: lines.join("\n"), count: count };
|
|
45
|
+
}
|
|
46
|
+
lines.push(
|
|
47
|
+
"These are the logical problems in the requirements. For a contradiction, which requirement should win? This report does not choose.",
|
|
48
|
+
);
|
|
49
|
+
let n = 1;
|
|
50
|
+
for (let i = 0; i < report.conflicts.length; i++) {
|
|
51
|
+
pushConflict(lines, report.conflicts[i], n);
|
|
52
|
+
n++;
|
|
53
|
+
}
|
|
54
|
+
for (let i = 0; i < report.gaps.length; i++) {
|
|
55
|
+
pushGap(lines, spec, report.gaps[i], n);
|
|
56
|
+
n++;
|
|
57
|
+
}
|
|
58
|
+
for (let i = 0; i < report.vacuous.length; i++) {
|
|
59
|
+
pushVacuous(lines, report.vacuous[i], n);
|
|
60
|
+
n++;
|
|
61
|
+
}
|
|
62
|
+
for (let i = 0; i < repeated.length; i++) {
|
|
63
|
+
pushRepeated(lines, repeated[i], n);
|
|
64
|
+
n++;
|
|
65
|
+
}
|
|
66
|
+
for (let i = 0; i < report.duplicate.length; i++) {
|
|
67
|
+
pushDuplicate(lines, spec, report.duplicate[i], n);
|
|
68
|
+
n++;
|
|
69
|
+
}
|
|
70
|
+
return { text: lines.join("\n"), count: count };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function pushConflict(lines: string[], conflict: Conflict, index: number): void {
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push("## " + formatInt(index) + ". " + conflict.summary);
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push(conflict.clash);
|
|
78
|
+
for (let r = 0; r < conflict.ids.length; r++) {
|
|
79
|
+
lines.push("");
|
|
80
|
+
lines.push(conflict.ids[r] + " line " + formatInt(conflict.lines[r]));
|
|
81
|
+
lines.push(displayQuote(conflict.sentences[r]));
|
|
82
|
+
}
|
|
83
|
+
if (conflict.requires.length > 0) {
|
|
84
|
+
lines.push("");
|
|
85
|
+
for (let r = 0; r < conflict.requires.length; r++) {
|
|
86
|
+
lines.push(conflict.requires[r].name + " requires " + conflict.requires[r].value + ".");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
lines.push("");
|
|
90
|
+
lines.push(winnerQuestion(conflict.ids));
|
|
91
|
+
pushRepairs(lines, conflict.repairs);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function pushRepairs(lines: string[], repairs: Repair[]): void {
|
|
95
|
+
if (repairs.length === 0) return;
|
|
96
|
+
lines.push("");
|
|
97
|
+
lines.push("Candidate repairs follow. This report does not choose one.");
|
|
98
|
+
for (let i = 0; i < repairs.length; i++) {
|
|
99
|
+
lines.push("");
|
|
100
|
+
lines.push(repairs[i].text);
|
|
101
|
+
pushQuotedLines(lines, repairs[i].sentence);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function pushQuotedLines(lines: string[], sentence: string): void {
|
|
106
|
+
if (sentence.length === 0) return;
|
|
107
|
+
let start = 0;
|
|
108
|
+
for (let i = 0; i <= sentence.length; i++) {
|
|
109
|
+
if (i === sentence.length || sentence.charCodeAt(i) === 10) {
|
|
110
|
+
const part = sentence.slice(start, i);
|
|
111
|
+
if (part.length > 0) lines.push(displayQuote(part));
|
|
112
|
+
start = i + 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function winnerQuestion(ids: string[]): string {
|
|
118
|
+
if (ids.length === 1) {
|
|
119
|
+
return (
|
|
120
|
+
"Which behavior should replace " +
|
|
121
|
+
ids[0] +
|
|
122
|
+
"? It cannot be satisfied for any allowed values. This report does not choose a behavior."
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (ids.length === 2) {
|
|
126
|
+
return (
|
|
127
|
+
"Which requirement should win? To keep " +
|
|
128
|
+
ids[0] +
|
|
129
|
+
", remove " +
|
|
130
|
+
ids[1] +
|
|
131
|
+
". To keep " +
|
|
132
|
+
ids[1] +
|
|
133
|
+
", remove " +
|
|
134
|
+
ids[0] +
|
|
135
|
+
". This report does not choose."
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return (
|
|
139
|
+
"Which requirement should win? Removing any one of " +
|
|
140
|
+
englishList(ids) +
|
|
141
|
+
" leaves the rest consistent. This report does not choose."
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function pushGap(lines: string[], spec: Spec, gap: Gap, index: number): void {
|
|
146
|
+
lines.push("");
|
|
147
|
+
lines.push("## " + formatInt(index) + ". " + gapTitle(spec, gap));
|
|
148
|
+
lines.push("");
|
|
149
|
+
lines.push(gap.text);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function gapTitle(spec: Spec, gap: Gap): string {
|
|
153
|
+
const phrase = phraseOf(spec, gap.output);
|
|
154
|
+
if (phrase === gap.output) return "Nothing covers " + gap.output;
|
|
155
|
+
return "Nothing covers " + phrase + " (" + gap.output + ")";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function phraseOf(spec: Spec, name: string): string {
|
|
159
|
+
for (let i = 0; i < spec.symbols.length; i++) {
|
|
160
|
+
if (spec.symbols[i].name === name && spec.symbols[i].phrase.length > 0) return spec.symbols[i].phrase;
|
|
161
|
+
}
|
|
162
|
+
return name;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function pushVacuous(lines: string[], item: Vacuous, index: number): void {
|
|
166
|
+
lines.push("");
|
|
167
|
+
lines.push("## " + formatInt(index) + ". " + item.id + " can never trigger");
|
|
168
|
+
lines.push("");
|
|
169
|
+
lines.push(item.id + " line " + formatInt(item.line));
|
|
170
|
+
lines.push(displayQuote(item.sentence));
|
|
171
|
+
lines.push("");
|
|
172
|
+
lines.push(item.text);
|
|
173
|
+
lines.push(item.fix);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function pushDuplicate(lines: string[], spec: Spec, item: Duplicate, index: number): void {
|
|
177
|
+
lines.push("");
|
|
178
|
+
lines.push("## " + formatInt(index) + ". " + item.id + " duplicates " + item.sameAs);
|
|
179
|
+
lines.push("");
|
|
180
|
+
lines.push(item.id + " line " + formatInt(item.line));
|
|
181
|
+
lines.push(displayQuote(item.sentence));
|
|
182
|
+
lines.push("");
|
|
183
|
+
lines.push(item.text);
|
|
184
|
+
const prior = requirementById(spec, item.sameAs);
|
|
185
|
+
if (prior !== null) {
|
|
186
|
+
lines.push("");
|
|
187
|
+
lines.push(prior.id + " line " + formatInt(prior.line));
|
|
188
|
+
lines.push(displayQuote(prior.sentence));
|
|
189
|
+
}
|
|
190
|
+
lines.push("");
|
|
191
|
+
lines.push(item.fix);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function requirementById(spec: Spec, id: string): Requirement | null {
|
|
195
|
+
for (let i = 0; i < spec.requirements.length; i++) {
|
|
196
|
+
if (spec.requirements[i].id === id) return spec.requirements[i];
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function pushRepeated(lines: string[], item: Repeated, index: number): void {
|
|
202
|
+
lines.push("");
|
|
203
|
+
lines.push("## " + formatInt(index) + ". " + item.id + " adds nothing");
|
|
204
|
+
lines.push("");
|
|
205
|
+
lines.push(item.id + " line " + formatInt(item.line));
|
|
206
|
+
lines.push(displayQuote(item.sentence));
|
|
207
|
+
lines.push("");
|
|
208
|
+
if (item.byId.length > 0) {
|
|
209
|
+
lines.push(
|
|
210
|
+
item.id +
|
|
211
|
+
" adds nothing. It only repeats " +
|
|
212
|
+
item.byId +
|
|
213
|
+
" in a stricter situation. Whenever " +
|
|
214
|
+
item.byId +
|
|
215
|
+
" holds, " +
|
|
216
|
+
item.id +
|
|
217
|
+
" holds too.",
|
|
218
|
+
);
|
|
219
|
+
lines.push("");
|
|
220
|
+
lines.push(item.byId + " line " + formatInt(item.byLine));
|
|
221
|
+
lines.push(displayQuote(item.bySentence));
|
|
222
|
+
lines.push("");
|
|
223
|
+
lines.push(
|
|
224
|
+
"Ask whether " +
|
|
225
|
+
item.id +
|
|
226
|
+
" was meant to say something " +
|
|
227
|
+
item.byId +
|
|
228
|
+
" does not already require. Do not delete it until the product manager agrees.",
|
|
229
|
+
);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
lines.push(item.text);
|
|
233
|
+
lines.push(item.fix);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function collectRepeated(spec: Spec, report: LintReport): Repeated[] {
|
|
237
|
+
const found = repeatedRequirements(spec);
|
|
238
|
+
const seen: string[] = [];
|
|
239
|
+
for (let i = 0; i < found.length; i++) seen.push(found[i].id);
|
|
240
|
+
for (let i = 0; i < report.redundant.length; i++) {
|
|
241
|
+
const item = report.redundant[i];
|
|
242
|
+
if (hasId(seen, item.id)) continue;
|
|
243
|
+
found.push(fromLint(item));
|
|
244
|
+
seen.push(item.id);
|
|
245
|
+
}
|
|
246
|
+
return found;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function fromLint(item: Redundant): Repeated {
|
|
250
|
+
return {
|
|
251
|
+
id: item.id,
|
|
252
|
+
line: item.line,
|
|
253
|
+
sentence: item.sentence,
|
|
254
|
+
byId: "",
|
|
255
|
+
byLine: 0,
|
|
256
|
+
bySentence: "",
|
|
257
|
+
text: item.text,
|
|
258
|
+
fix: item.fix,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function hasId(ids: string[], id: string): boolean {
|
|
263
|
+
for (let i = 0; i < ids.length; i++) {
|
|
264
|
+
if (ids[i] === id) return true;
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function repeatedRequirements(spec: Spec): Repeated[] {
|
|
270
|
+
const found: Repeated[] = [];
|
|
271
|
+
const reqs = spec.requirements;
|
|
272
|
+
for (let j = 0; j < reqs.length; j++) {
|
|
273
|
+
const narrower = reqs[j];
|
|
274
|
+
const narrowGuard = guardOf(narrower);
|
|
275
|
+
if (!holdsTerm(spec, narrowGuard)) continue;
|
|
276
|
+
for (let i = 0; i < reqs.length; i++) {
|
|
277
|
+
if (i === j) continue;
|
|
278
|
+
const wider = reqs[i];
|
|
279
|
+
if (!termsEqual(wider.consequent, narrower.consequent)) continue;
|
|
280
|
+
const wideGuard = guardOf(wider);
|
|
281
|
+
if (!holdsTerm(spec, wideGuard)) continue;
|
|
282
|
+
if (!strictlyWider(spec, wideGuard, narrowGuard)) continue;
|
|
283
|
+
found.push({
|
|
284
|
+
id: narrower.id,
|
|
285
|
+
line: narrower.line,
|
|
286
|
+
sentence: narrower.sentence,
|
|
287
|
+
byId: wider.id,
|
|
288
|
+
byLine: wider.line,
|
|
289
|
+
bySentence: wider.sentence,
|
|
290
|
+
text: "",
|
|
291
|
+
fix: "",
|
|
292
|
+
});
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return found;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function strictlyWider(spec: Spec, wide: Term, narrow: Term): boolean {
|
|
300
|
+
if (holdsTerm(spec, andNot(narrow, wide))) return false;
|
|
301
|
+
return holdsTerm(spec, andNot(wide, narrow));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function andNot(left: Term, right: Term): Term {
|
|
305
|
+
return { kind: "and", left: left, right: { kind: "not", term: right } };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function guardOf(req: Requirement): Term {
|
|
309
|
+
if (req.antecedent === null) return { kind: "bool", value: true };
|
|
310
|
+
return req.antecedent;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function holdsTerm(spec: Spec, term: Term): boolean {
|
|
314
|
+
const probe: Requirement = {
|
|
315
|
+
id: "probe",
|
|
316
|
+
line: 0,
|
|
317
|
+
sentence: "",
|
|
318
|
+
system: "",
|
|
319
|
+
antecedent: null,
|
|
320
|
+
consequent: term,
|
|
321
|
+
hasWhen: false,
|
|
322
|
+
hasIf: false,
|
|
323
|
+
whenTerm: null,
|
|
324
|
+
};
|
|
325
|
+
const holds: Requirement[] = [];
|
|
326
|
+
holds.push(probe);
|
|
327
|
+
return satisfiable(spec, holds);
|
|
328
|
+
}
|
|
329
|
+
|
package/src/scenarios.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small, deterministic set of accepted and rejected scenarios.
|
|
3
|
+
* Each requirement is shown with its trigger true, its trigger false, and
|
|
4
|
+
* a case that breaks it. The first hit is the smallest assignment.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { evaluateRequirements, explainHold } from "./check.ts";
|
|
8
|
+
import { displayCode, type Requirement, type Spec } from "./spec.ts";
|
|
9
|
+
import { firstMatching } from "./solver.ts";
|
|
10
|
+
import { Binding, formatScenario } from "./term.ts";
|
|
11
|
+
import { formatInt } from "./text.ts";
|
|
12
|
+
|
|
13
|
+
export interface ScenarioPair {
|
|
14
|
+
name: string;
|
|
15
|
+
value: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ScenarioRow {
|
|
19
|
+
id: string;
|
|
20
|
+
line: number;
|
|
21
|
+
kind: string;
|
|
22
|
+
trigger: string;
|
|
23
|
+
found: boolean;
|
|
24
|
+
sameAs: string;
|
|
25
|
+
scenario: ScenarioPair[];
|
|
26
|
+
exampleJson: string;
|
|
27
|
+
text: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function listScenarios(spec: Spec): ScenarioRow[] {
|
|
31
|
+
const rows: ScenarioRow[] = [];
|
|
32
|
+
for (let i = 0; i < spec.requirements.length; i++) {
|
|
33
|
+
const req = spec.requirements[i];
|
|
34
|
+
pushKind(spec, rows, req, "accepted", "true", 1);
|
|
35
|
+
pushKind(spec, rows, req, "accepted", "false", 2);
|
|
36
|
+
pushKind(spec, rows, req, "rejected", "true", 1);
|
|
37
|
+
}
|
|
38
|
+
return rows;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pushKind(spec: Spec, rows: ScenarioRow[], req: Requirement, kind: string, trigger: string, wantGuard: number): void {
|
|
42
|
+
if (wantGuard === 2 && req.antecedent === null) {
|
|
43
|
+
rows.push(emptyRow(req, kind, trigger, req.id + " has no trigger, so there is no case where the trigger is false."));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const env =
|
|
47
|
+
kind === "accepted"
|
|
48
|
+
? firstMatching(spec, spec.requirements, [], req.antecedent, wantGuard)
|
|
49
|
+
: firstMatching(spec, [], [req], req.antecedent, wantGuard);
|
|
50
|
+
if (env === null) {
|
|
51
|
+
rows.push(emptyRow(req, kind, trigger, missingText(req.id, kind, trigger)));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const sameAs = earlierLabel(spec, rows, env);
|
|
55
|
+
let text = explainHold(spec, env, req.antecedent, req.consequent);
|
|
56
|
+
if (kind === "rejected") {
|
|
57
|
+
const breaks = evaluateRequirements(spec, [req], env);
|
|
58
|
+
if (breaks.length > 0) text = breaks[0].because;
|
|
59
|
+
}
|
|
60
|
+
rows.push({
|
|
61
|
+
id: req.id,
|
|
62
|
+
line: req.line,
|
|
63
|
+
kind: kind,
|
|
64
|
+
trigger: trigger,
|
|
65
|
+
found: true,
|
|
66
|
+
sameAs: sameAs,
|
|
67
|
+
scenario: pairs(spec, env),
|
|
68
|
+
exampleJson: formatScenario(spec, env),
|
|
69
|
+
text: text,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function emptyRow(req: Requirement, kind: string, trigger: string, text: string): ScenarioRow {
|
|
74
|
+
return {
|
|
75
|
+
id: req.id,
|
|
76
|
+
line: req.line,
|
|
77
|
+
kind: kind,
|
|
78
|
+
trigger: trigger,
|
|
79
|
+
found: false,
|
|
80
|
+
sameAs: "",
|
|
81
|
+
scenario: [],
|
|
82
|
+
exampleJson: "",
|
|
83
|
+
text: text,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function missingText(id: string, kind: string, trigger: string): string {
|
|
88
|
+
if (kind === "rejected") return "No scenario makes this trigger true and breaks " + id + ".";
|
|
89
|
+
if (trigger === "false") return "No scenario satisfies every requirement while this trigger is false.";
|
|
90
|
+
return "No scenario satisfies every requirement while this trigger is true.";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function earlierLabel(spec: Spec, rows: ScenarioRow[], env: Binding): string {
|
|
94
|
+
for (let i = 0; i < rows.length; i++) {
|
|
95
|
+
const row = rows[i];
|
|
96
|
+
if (!row.found || row.scenario.length !== spec.symbols.length) continue;
|
|
97
|
+
let same = true;
|
|
98
|
+
for (let s = 0; s < spec.symbols.length; s++) {
|
|
99
|
+
if (row.scenario[s].value !== displayCode(spec, spec.symbols[s], env.code[s])) same = false;
|
|
100
|
+
}
|
|
101
|
+
if (same) return rowLabel(row);
|
|
102
|
+
}
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function pairs(spec: Spec, env: Binding): ScenarioPair[] {
|
|
107
|
+
const out: ScenarioPair[] = [];
|
|
108
|
+
for (let i = 0; i < spec.symbols.length; i++) {
|
|
109
|
+
out.push({ name: spec.symbols[i].name, value: displayCode(spec, spec.symbols[i], env.code[i]) });
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function rowLabel(row: ScenarioRow): string {
|
|
115
|
+
return row.id + " " + row.kind + ", trigger " + row.trigger;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function formatScenariosText(specPath: string, rows: ScenarioRow[]): string {
|
|
119
|
+
const lines: string[] = [];
|
|
120
|
+
lines.push("spec: " + specPath);
|
|
121
|
+
lines.push("scenarios: " + formatInt(rows.length));
|
|
122
|
+
for (let i = 0; i < rows.length; i++) {
|
|
123
|
+
const row = rows[i];
|
|
124
|
+
lines.push("");
|
|
125
|
+
lines.push(rowLabel(row));
|
|
126
|
+
if (row.found && row.sameAs.length === 0) lines.push(row.exampleJson);
|
|
127
|
+
if (row.sameAs.length > 0) lines.push("Same scenario as " + row.sameAs + ".");
|
|
128
|
+
lines.push(row.text);
|
|
129
|
+
}
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function formatScenariosJson(specPath: string, rows: ScenarioRow[]): string {
|
|
134
|
+
const scenarios = [];
|
|
135
|
+
for (let i = 0; i < rows.length; i++) {
|
|
136
|
+
const row = rows[i];
|
|
137
|
+
scenarios.push({
|
|
138
|
+
id: row.id,
|
|
139
|
+
line: row.line,
|
|
140
|
+
kind: row.kind,
|
|
141
|
+
trigger: row.trigger,
|
|
142
|
+
found: row.found,
|
|
143
|
+
sameAs: row.sameAs,
|
|
144
|
+
scenario: row.scenario,
|
|
145
|
+
text: row.text,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return JSON.stringify({ ok: true, spec: specPath, scenarios: scenarios }, null, 2);
|
|
149
|
+
}
|
package/src/smt.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Print a spec as SMT-LIB. Each requirement becomes one named implication.
|
|
3
|
+
* Integer ranges are unnamed assertions. The file is for people to read;
|
|
4
|
+
* lint uses the solver in solver.ts and does not call an external prover.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { findEnum, type Spec, type SymbolDecl } from "./spec.ts";
|
|
8
|
+
import { formatSmtTerm } from "./term.ts";
|
|
9
|
+
import { formatInt, formatSmtInt } from "./text.ts";
|
|
10
|
+
|
|
11
|
+
export function printSmt(specPath: string, spec: Spec): string {
|
|
12
|
+
const lines: string[] = [];
|
|
13
|
+
lines.push("; spec: " + specPath);
|
|
14
|
+
lines.push("(set-logic ALL)");
|
|
15
|
+
for (let i = 0; i < spec.enums.length; i++) {
|
|
16
|
+
const decl = spec.enums[i];
|
|
17
|
+
let variants = "";
|
|
18
|
+
for (let v = 0; v < decl.variants.length; v++) {
|
|
19
|
+
if (v > 0) variants += " ";
|
|
20
|
+
variants += "(" + decl.variants[v] + ")";
|
|
21
|
+
}
|
|
22
|
+
lines.push("; enum " + decl.name + " { " + decl.variants.join(", ") + " }");
|
|
23
|
+
lines.push("(declare-datatypes ((" + decl.name + " 0)) ((" + variants + ")))");
|
|
24
|
+
}
|
|
25
|
+
for (let i = 0; i < spec.symbols.length; i++) {
|
|
26
|
+
const sym = spec.symbols[i];
|
|
27
|
+
lines.push("; " + symbolComment(spec, sym));
|
|
28
|
+
lines.push("(declare-fun " + sym.name + " () " + sortName(sym) + ")");
|
|
29
|
+
if (sym.sort.kind === "int") {
|
|
30
|
+
lines.push(
|
|
31
|
+
"(assert (and (>= " +
|
|
32
|
+
sym.name +
|
|
33
|
+
" " +
|
|
34
|
+
formatSmtInt(sym.sort.min) +
|
|
35
|
+
") (<= " +
|
|
36
|
+
sym.name +
|
|
37
|
+
" " +
|
|
38
|
+
formatSmtInt(sym.sort.max) +
|
|
39
|
+
")))",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (let i = 0; i < spec.requirements.length; i++) {
|
|
44
|
+
const req = spec.requirements[i];
|
|
45
|
+
const name = req.id + "_L" + formatInt(req.line);
|
|
46
|
+
lines.push("; " + name + " line " + formatInt(req.line) + ": " + req.sentence);
|
|
47
|
+
const body =
|
|
48
|
+
req.antecedent === null
|
|
49
|
+
? formatSmtTerm(req.consequent)
|
|
50
|
+
: "(=> " + formatSmtTerm(req.antecedent) + " " + formatSmtTerm(req.consequent) + ")";
|
|
51
|
+
lines.push("(assert (! " + body + " :named " + name + "))");
|
|
52
|
+
}
|
|
53
|
+
return lines.join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatEmitJson(specPath: string, smt: string): string {
|
|
57
|
+
return JSON.stringify({ ok: true, spec: specPath, smt: smt }, null, 2);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function symbolComment(spec: Spec, sym: SymbolDecl): string {
|
|
61
|
+
const prefix = sym.unwanted ? "unwanted " + sym.role : sym.role;
|
|
62
|
+
let text = prefix + " " + sym.name + ": " + sortComment(spec, sym) + " \"" + escapeComment(sym.description) + "\"";
|
|
63
|
+
if (sym.phrase.length > 0) text += " phrase \"" + escapeComment(sym.phrase) + "\"";
|
|
64
|
+
if (sym.outcomeYes.length > 0) {
|
|
65
|
+
text += " outcome \"" + escapeComment(sym.outcomeYes) + "\"";
|
|
66
|
+
if (sym.outcomeNo.length > 0) text += " / \"" + escapeComment(sym.outcomeNo) + "\"";
|
|
67
|
+
}
|
|
68
|
+
if (sym.action.length > 0) text += " action \"" + escapeComment(sym.action) + "\"";
|
|
69
|
+
if (sym.says.length > 0) text += " says \"" + escapeComment(sym.says) + "\"";
|
|
70
|
+
if (sym.asText.length > 0) text += " as \"" + escapeComment(sym.asText) + "\"";
|
|
71
|
+
return text;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sortComment(spec: Spec, sym: SymbolDecl): string {
|
|
75
|
+
if (sym.sort.kind === "bool") return "Bool";
|
|
76
|
+
if (sym.sort.kind === "int") return "Int " + formatInt(sym.sort.min) + ".." + formatInt(sym.sort.max);
|
|
77
|
+
const decl = findEnum(spec, sym.sort.enumName);
|
|
78
|
+
if (decl === null) return sym.sort.enumName;
|
|
79
|
+
return decl.name;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sortName(sym: SymbolDecl): string {
|
|
83
|
+
if (sym.sort.kind === "bool") return "Bool";
|
|
84
|
+
if (sym.sort.kind === "int") return "Int";
|
|
85
|
+
return sym.sort.enumName;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function escapeComment(text: string): string {
|
|
89
|
+
let out = "";
|
|
90
|
+
for (let i = 0; i < text.length; i++) {
|
|
91
|
+
const code = text.charCodeAt(i);
|
|
92
|
+
if (code === 10 || code === 13) out += " ";
|
|
93
|
+
else if (code === 34) out += "\\\"";
|
|
94
|
+
else out += text.slice(i, i + 1);
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|