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/run-all.ts
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run the typecheck, ScriptC coverage gate, native build, unit tests,
|
|
3
|
+
* and golden CLI tests. Unit tests and each CLI case run once under Node
|
|
4
|
+
* and once as the native binary; stdout, stderr, and exit status must match
|
|
5
|
+
* each other and the checked-in golden files. Samples in README.md and
|
|
6
|
+
* skills/plan-verify/SKILL.md marked with `<!-- verify stdout|stderr <exit>`
|
|
7
|
+
* are run under Node and build/plan-verify and must match the fenced bytes.
|
|
8
|
+
*
|
|
9
|
+
* Write or refresh goldens with: node --experimental-strip-types test/run-all.ts --write-golden
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
|
|
15
|
+
interface RunResult {
|
|
16
|
+
status: number;
|
|
17
|
+
stdout: string;
|
|
18
|
+
stderr: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Case {
|
|
22
|
+
name: string;
|
|
23
|
+
args: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const WRITE = hasFlag("--write-golden");
|
|
27
|
+
let failed = 0;
|
|
28
|
+
|
|
29
|
+
function hasFlag(flag: string): boolean {
|
|
30
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
31
|
+
if (process.argv[i] === flag) return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function run(command: string, args: string[]): RunResult {
|
|
37
|
+
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
38
|
+
if (result.error !== undefined) {
|
|
39
|
+
let stderr = result.stderr;
|
|
40
|
+
if (stderr.length === 0) stderr = result.error.message + "\n";
|
|
41
|
+
return { status: -1, stdout: result.stdout, stderr: stderr };
|
|
42
|
+
}
|
|
43
|
+
let status = -1;
|
|
44
|
+
if (result.status !== null) status = result.status;
|
|
45
|
+
return { status: status, stdout: result.stdout, stderr: result.stderr };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fail(message: string): void {
|
|
49
|
+
console.error("FAIL " + message);
|
|
50
|
+
failed++;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function step(title: string, result: RunResult, ok: boolean): void {
|
|
54
|
+
if (ok) {
|
|
55
|
+
console.log("PASS " + title);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
fail(title);
|
|
59
|
+
if (result.stdout.length > 0) console.error(result.stdout);
|
|
60
|
+
if (result.stderr.length > 0) console.error(result.stderr);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function requireCoverage(entry: string): void {
|
|
64
|
+
const result = run("node_modules/.bin/scriptc", ["coverage", entry, "--fail-on=blockers"]);
|
|
65
|
+
const text = result.stdout + result.stderr;
|
|
66
|
+
const ok =
|
|
67
|
+
result.status === 0 &&
|
|
68
|
+
text.indexOf("(100%)") !== -1 &&
|
|
69
|
+
text.indexOf("fully static") !== -1 &&
|
|
70
|
+
text.indexOf("blockers") === -1;
|
|
71
|
+
step("coverage " + entry, result, ok);
|
|
72
|
+
console.log(result.stdout);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function requireBuild(entry: string, out: string): void {
|
|
76
|
+
const result = run("node_modules/.bin/scriptc", ["build", entry, "-o", out]);
|
|
77
|
+
step("build " + out, result, result.status === 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function nodeRun(entry: string, args: string[]): RunResult {
|
|
81
|
+
const full = ["--experimental-strip-types", entry];
|
|
82
|
+
for (let i = 0; i < args.length; i++) full.push(args[i]);
|
|
83
|
+
return run(process.execPath, full);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function comparePair(name: string, nodeResult: RunResult, binResult: RunResult): void {
|
|
87
|
+
if (
|
|
88
|
+
nodeResult.status !== binResult.status ||
|
|
89
|
+
nodeResult.stdout !== binResult.stdout ||
|
|
90
|
+
nodeResult.stderr !== binResult.stderr
|
|
91
|
+
) {
|
|
92
|
+
fail(name + " node/native differ");
|
|
93
|
+
console.error("node status " + String(nodeResult.status));
|
|
94
|
+
console.error("native status " + String(binResult.status));
|
|
95
|
+
console.error("--- node stdout ---\n" + nodeResult.stdout);
|
|
96
|
+
console.error("--- native stdout ---\n" + binResult.stdout);
|
|
97
|
+
console.error("--- node stderr ---\n" + nodeResult.stderr);
|
|
98
|
+
console.error("--- native stderr ---\n" + binResult.stderr);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const dir = "test/golden/" + name;
|
|
102
|
+
const stdoutPath = dir + ".stdout";
|
|
103
|
+
const stderrPath = dir + ".stderr";
|
|
104
|
+
const exitPath = dir + ".exit";
|
|
105
|
+
const exitText = String(nodeResult.status) + "\n";
|
|
106
|
+
if (WRITE) {
|
|
107
|
+
writeFileSync(stdoutPath, nodeResult.stdout, "utf8");
|
|
108
|
+
writeFileSync(stderrPath, nodeResult.stderr, "utf8");
|
|
109
|
+
writeFileSync(exitPath, exitText, "utf8");
|
|
110
|
+
}
|
|
111
|
+
const expectedOut = readFileSync(stdoutPath, "utf8");
|
|
112
|
+
const expectedErr = readFileSync(stderrPath, "utf8");
|
|
113
|
+
const expectedExit = readFileSync(exitPath, "utf8");
|
|
114
|
+
if (nodeResult.stdout !== expectedOut || nodeResult.stderr !== expectedErr || exitText !== expectedExit) {
|
|
115
|
+
fail(name + " golden mismatch");
|
|
116
|
+
console.error("--- actual stdout ---\n" + nodeResult.stdout);
|
|
117
|
+
console.error("--- golden stdout ---\n" + expectedOut);
|
|
118
|
+
console.error("--- actual stderr ---\n" + nodeResult.stderr);
|
|
119
|
+
console.error("--- golden stderr ---\n" + expectedErr);
|
|
120
|
+
console.error("--- actual exit ---\n" + exitText);
|
|
121
|
+
console.error("--- golden exit ---\n" + expectedExit);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
console.log("PASS " + name + " (node = native = golden, exit " + String(nodeResult.status) + ")");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function main(): void {
|
|
128
|
+
mkdirSync("build", { recursive: true });
|
|
129
|
+
mkdirSync("test/golden", { recursive: true });
|
|
130
|
+
|
|
131
|
+
const version = run("node_modules/.bin/scriptc", ["--version"]);
|
|
132
|
+
const versionText = version.stdout.trim();
|
|
133
|
+
step("scriptc version 0.4.1", version, version.status === 0 && versionText === "0.4.1");
|
|
134
|
+
console.log(version.stdout);
|
|
135
|
+
|
|
136
|
+
const tsc = run(process.execPath, ["node_modules/typescript/bin/tsc", "--noEmit", "-p", "tsconfig.json"]);
|
|
137
|
+
step("tsc --noEmit", tsc, tsc.status === 0);
|
|
138
|
+
|
|
139
|
+
requireCoverage("src/main.ts");
|
|
140
|
+
requireCoverage("test/unit.ts");
|
|
141
|
+
requireCoverage("test/run-all.ts");
|
|
142
|
+
|
|
143
|
+
requireBuild("src/main.ts", "build/plan-verify");
|
|
144
|
+
requireBuild("test/unit.ts", "build/unit-tests");
|
|
145
|
+
|
|
146
|
+
if (failed !== 0) {
|
|
147
|
+
console.error("stopping before golden tests (" + String(failed) + " failed)");
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const unitNode = nodeRun("test/unit.ts", []);
|
|
152
|
+
const unitBin = run("build/unit-tests", []);
|
|
153
|
+
comparePair("unit", unitNode, unitBin);
|
|
154
|
+
|
|
155
|
+
const cases: Case[] = [
|
|
156
|
+
{ name: "check-fail", args: ["check", "examples/order.ears.md", "--input", "examples/order-fail.json"] },
|
|
157
|
+
{ name: "check-pass", args: ["check", "examples/order.ears.md", "--input", "examples/order-pass.json"] },
|
|
158
|
+
{ name: "check-fail-json", args: ["check", "--json", "examples/order.ears.md", "--input", "examples/order-fail.json"] },
|
|
159
|
+
{ name: "emit-smt", args: ["emit-smt", "examples/order.ears.md"] },
|
|
160
|
+
{ name: "emit-smt-json", args: ["emit-smt", "--json", "examples/order.ears.md"] },
|
|
161
|
+
{ name: "emit-smt-json-usage", args: ["emit-smt", "--json"] },
|
|
162
|
+
{ name: "check-pass-json", args: ["check", "--json", "examples/order.ears.md", "--input", "examples/order-pass.json"] },
|
|
163
|
+
{ name: "lint-conflict", args: ["lint", "examples/order-conflict.ears.md"] },
|
|
164
|
+
{ name: "lint-repair", args: ["lint", "examples/order-repair.ears.md"] },
|
|
165
|
+
{ name: "lint-repair-json", args: ["lint", "--json", "examples/order-repair.ears.md"] },
|
|
166
|
+
{ name: "lint-conflict-json", args: ["lint", "--json", "examples/order-conflict.ears.md"] },
|
|
167
|
+
{ name: "lint-gap", args: ["lint", "examples/order.ears.md"] },
|
|
168
|
+
{ name: "lint-clean", args: ["lint", "examples/order-covered.ears.md"] },
|
|
169
|
+
{ name: "lint-misuse", args: ["lint", "examples/order-misuse.ears.md"] },
|
|
170
|
+
{ name: "lint-wording", args: ["lint", "examples/order-wording.ears.md"] },
|
|
171
|
+
{ name: "lint-unhandled", args: ["lint", "examples/order-unhandled.ears.md"] },
|
|
172
|
+
{ name: "lint-unanswered", args: ["lint", "examples/order-unanswered.ears.md"] },
|
|
173
|
+
{ name: "lint-vague", args: ["lint", "examples/order-vague.ears.md"] },
|
|
174
|
+
{ name: "lint-quantity", args: ["lint", "examples/order-quantity.ears.md"] },
|
|
175
|
+
{ name: "lint-status", args: ["lint", "examples/order-status.ears.md"] },
|
|
176
|
+
{ name: "lint-chain", args: ["lint", "examples/order-chain.ears.md"] },
|
|
177
|
+
{ name: "lint-redundant", args: ["lint", "examples/order-redundant.ears.md"] },
|
|
178
|
+
{ name: "lint-repeat", args: ["lint", "examples/order-repeat.ears.md"] },
|
|
179
|
+
{ name: "lint-duplicate", args: ["lint", "examples/order-duplicate.ears.md"] },
|
|
180
|
+
{ name: "lint-negation", args: ["lint", "examples/order-negation.ears.md"] },
|
|
181
|
+
{ name: "lint-unmeasured", args: ["lint", "examples/order-unmeasured.ears.md"] },
|
|
182
|
+
{ name: "lint-vacuous", args: ["lint", "examples/order-vacuous.ears.md"] },
|
|
183
|
+
{ name: "lint-vacuous-json", args: ["lint", "--json", "examples/order-vacuous.ears.md"] },
|
|
184
|
+
{ name: "lint-unused", args: ["lint", "examples/order-unused.ears.md"] },
|
|
185
|
+
{ name: "lint-unused-json", args: ["lint", "--json", "examples/order-unused.ears.md"] },
|
|
186
|
+
{ name: "lint-checkout", args: ["lint", "examples/checkout.ears.md"] },
|
|
187
|
+
{ name: "lint-awkward", args: ["lint", "examples/order-awkward.ears.md"] },
|
|
188
|
+
{ name: "lint-clear", args: ["lint", "examples/order-clear.ears.md"] },
|
|
189
|
+
{ name: "scenarios", args: ["scenarios", "examples/order.ears.md"] },
|
|
190
|
+
{ name: "scenarios-json", args: ["scenarios", "--json", "examples/order.ears.md"] },
|
|
191
|
+
{ name: "scenarios-status", args: ["scenarios", "examples/order-status.ears.md"] },
|
|
192
|
+
{ name: "lint-wide", args: ["lint", "examples/order-wide.ears.md"] },
|
|
193
|
+
{ name: "malformed-spec", args: ["check", "test/fixtures/malformed.ears.md", "--input", "examples/order-pass.json"] },
|
|
194
|
+
{ name: "unknown-phrase", args: ["lint", "test/fixtures/unknown-phrase.ears.md"] },
|
|
195
|
+
{ name: "malformed-scenario", args: ["check", "examples/order.ears.md", "--input", "examples/order-bad.json"] },
|
|
196
|
+
{ name: "policy", args: ["policy", "examples/order.ears.md"] },
|
|
197
|
+
{ name: "policy-covered", args: ["policy", "examples/order-covered.ears.md"] },
|
|
198
|
+
{ name: "policy-counterexample", args: ["policy", "examples/order.ears.md", "--property", "examples/order-always.ears.md"] },
|
|
199
|
+
{ name: "policy-holds", args: ["policy", "examples/order.ears.md", "--property", "examples/order-same.ears.md"] },
|
|
200
|
+
{ name: "policy-status", args: ["policy", "examples/order-status.ears.md"] },
|
|
201
|
+
{ name: "policy-vacuous", args: ["policy", "examples/order-status.ears.md", "--property", "examples/status-placed.ears.md"] },
|
|
202
|
+
{ name: "policy-json", args: ["policy", "--json", "examples/order.ears.md", "--property", "examples/order-always.ears.md"] },
|
|
203
|
+
{ name: "policy-wide", args: ["policy", "examples/order-wide.ears.md"] },
|
|
204
|
+
{ name: "usage", args: [] },
|
|
205
|
+
{ name: "help", args: ["help"] },
|
|
206
|
+
];
|
|
207
|
+
for (let i = 0; i < cases.length; i++) {
|
|
208
|
+
const item = cases[i];
|
|
209
|
+
const nodeResult = nodeRun("src/main.ts", item.args);
|
|
210
|
+
const binResult = run("build/plan-verify", item.args);
|
|
211
|
+
comparePair(item.name, nodeResult, binResult);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
checkDocSamples("readme", "README.md", false);
|
|
215
|
+
checkDocSamples("skill", "skills/plan-verify/SKILL.md", true);
|
|
216
|
+
checkPlanToProblems();
|
|
217
|
+
|
|
218
|
+
if (failed !== 0) {
|
|
219
|
+
console.error(String(failed) + " failed");
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
console.log("all tests passed");
|
|
223
|
+
process.exit(0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function checkPlanToProblems(): void {
|
|
227
|
+
const dir = "examples/plan-to-problems/";
|
|
228
|
+
const spec = dir + "generated.ears.md";
|
|
229
|
+
const lintNode = nodeRun("src/main.ts", ["lint", "--json", spec]);
|
|
230
|
+
const lintBin = run("build/plan-verify", ["lint", "--json", spec]);
|
|
231
|
+
const lintOk = sameSaved("plan-to-problems lint", lintNode, lintBin, dir + "lint.json", 1);
|
|
232
|
+
const reportNode = nodeRun("src/main.ts", ["report", spec]);
|
|
233
|
+
const reportBin = run("build/plan-verify", ["report", spec]);
|
|
234
|
+
const reportOk = sameSaved("plan-to-problems report", reportNode, reportBin, dir + "problems.md", 1);
|
|
235
|
+
if (!lintOk || !reportOk) return;
|
|
236
|
+
const planted = readFileSync(dir + "planted.txt", "utf8");
|
|
237
|
+
const expected = readFileSync(dir + "expected-problems.md", "utf8");
|
|
238
|
+
if (plantedDetected(planted, expected, lintNode.stdout, reportNode.stdout)) {
|
|
239
|
+
console.log("PASS plan-to-problems planted problems");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function sameSaved(name: string, nodeResult: RunResult, binResult: RunResult, savedPath: string, exitCode: number): boolean {
|
|
244
|
+
if (
|
|
245
|
+
nodeResult.status !== binResult.status ||
|
|
246
|
+
nodeResult.stdout !== binResult.stdout ||
|
|
247
|
+
nodeResult.stderr !== binResult.stderr
|
|
248
|
+
) {
|
|
249
|
+
fail(name + " node/native differ");
|
|
250
|
+
console.error("--- node stdout ---\n" + nodeResult.stdout);
|
|
251
|
+
console.error("--- native stdout ---\n" + binResult.stdout);
|
|
252
|
+
console.error("--- node stderr ---\n" + nodeResult.stderr);
|
|
253
|
+
console.error("--- native stderr ---\n" + binResult.stderr);
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
const saved = readFileSync(savedPath, "utf8");
|
|
257
|
+
if (nodeResult.stdout !== saved || nodeResult.stderr.length !== 0 || nodeResult.status !== exitCode) {
|
|
258
|
+
fail(name + " saved output mismatch");
|
|
259
|
+
console.error("expected exit " + String(exitCode) + ", got " + String(nodeResult.status));
|
|
260
|
+
console.error("--- actual stdout ---\n" + nodeResult.stdout);
|
|
261
|
+
console.error("--- saved stdout ---\n" + saved);
|
|
262
|
+
if (nodeResult.stderr.length > 0) console.error("--- stderr ---\n" + nodeResult.stderr);
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
console.log("PASS " + name + " (node = native = saved, exit " + String(exitCode) + ")");
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function plantedDetected(planted: string, expected: string, lintJson: string, problems: string): boolean {
|
|
270
|
+
const lines = nonEmptyLines(planted);
|
|
271
|
+
if (lines.length === 0) {
|
|
272
|
+
fail("plan-to-problems planted list is empty");
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
if (problems.indexOf("keeps ") !== -1 || problems.indexOf("breaks ") !== -1) {
|
|
276
|
+
fail("plan-to-problems report picks a winner");
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
let conflicts = 0;
|
|
280
|
+
for (let i = 0; i < lines.length; i++) {
|
|
281
|
+
const words = wordsOf(lines[i]);
|
|
282
|
+
if (words.length < 2) {
|
|
283
|
+
fail("plan-to-problems planted line " + lines[i]);
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const kind = words[0];
|
|
287
|
+
if (kind === "conflict") {
|
|
288
|
+
if (words.length !== 3) {
|
|
289
|
+
fail("plan-to-problems conflict needs two ids");
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
conflicts++;
|
|
293
|
+
const pair = "\"" + words[1] + "\",\n \"" + words[2] + "\"";
|
|
294
|
+
if (lintJson.indexOf(pair) < 0 || problems.indexOf(words[1]) < 0 || problems.indexOf(words[2]) < 0) {
|
|
295
|
+
fail("plan-to-problems conflict " + words[1] + " " + words[2]);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
} else if (kind === "gap") {
|
|
299
|
+
const marker = "\"output\": \"" + words[1] + "\"";
|
|
300
|
+
if (lintJson.indexOf(marker) < 0 || problems.indexOf(words[1]) < 0) {
|
|
301
|
+
fail("plan-to-problems gap " + words[1]);
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
} else if (kind === "vacuous") {
|
|
305
|
+
const marker = "\"id\": \"" + words[1] + "\"";
|
|
306
|
+
if (lintJson.indexOf(marker) < 0 || problems.indexOf(words[1]) < 0) {
|
|
307
|
+
fail("plan-to-problems vacuous " + words[1]);
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
} else if (kind === "redundant") {
|
|
311
|
+
const redundantAt = lintJson.indexOf('"redundant":');
|
|
312
|
+
const marker = '"id": "' + words[1] + '"';
|
|
313
|
+
if (
|
|
314
|
+
redundantAt < 0 ||
|
|
315
|
+
lintJson.indexOf(marker, redundantAt) < 0 ||
|
|
316
|
+
problems.indexOf(words[1]) < 0 ||
|
|
317
|
+
problems.indexOf("adds nothing") < 0
|
|
318
|
+
) {
|
|
319
|
+
fail("plan-to-problems redundant " + words[1]);
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
fail("plan-to-problems unknown planted kind " + kind);
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
for (let w = 1; w < words.length; w++) {
|
|
327
|
+
if (expected.indexOf(words[w]) < 0) {
|
|
328
|
+
fail("plan-to-problems expected-problems.md omits " + words[w]);
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (conflicts < 2) {
|
|
334
|
+
fail("plan-to-problems needs two contradictions");
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
if (countText(lintJson, '"kind": "negation"') !== 2) {
|
|
338
|
+
fail("plan-to-problems negated triggers");
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
const questions = countText(problems, "Which requirement should win?");
|
|
342
|
+
if (questions < conflicts) {
|
|
343
|
+
fail("plan-to-problems winner questions " + String(questions));
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function wordsOf(line: string): string[] {
|
|
350
|
+
const words: string[] = [];
|
|
351
|
+
let start = 0;
|
|
352
|
+
for (let i = 0; i <= line.length; i++) {
|
|
353
|
+
if (i === line.length || line.charCodeAt(i) === 32) {
|
|
354
|
+
if (i > start) words.push(line.slice(start, i));
|
|
355
|
+
start = i + 1;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return words;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function countText(text: string, needle: string): number {
|
|
362
|
+
let count = 0;
|
|
363
|
+
let from = 0;
|
|
364
|
+
while (from < text.length) {
|
|
365
|
+
const at = text.indexOf(needle, from);
|
|
366
|
+
if (at < 0) break;
|
|
367
|
+
count++;
|
|
368
|
+
from = at + needle.length;
|
|
369
|
+
}
|
|
370
|
+
return count;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function checkDocSamples(label: string, path: string, strictText: boolean): void {
|
|
374
|
+
const text = readFileSync(path, "utf8");
|
|
375
|
+
const startMark = "<!-- verify ";
|
|
376
|
+
const fences: number[] = [];
|
|
377
|
+
let cursor = 0;
|
|
378
|
+
let found = 0;
|
|
379
|
+
while (cursor < text.length) {
|
|
380
|
+
const at = text.indexOf(startMark, cursor);
|
|
381
|
+
if (at < 0) break;
|
|
382
|
+
const headerEnd = text.indexOf("\n", at);
|
|
383
|
+
if (headerEnd < 0) {
|
|
384
|
+
fail(label + " verify header is truncated");
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const header = text.slice(at + startMark.length, headerEnd).trim();
|
|
388
|
+
const space = header.indexOf(" ");
|
|
389
|
+
if (space < 0) {
|
|
390
|
+
fail(label + " verify header needs a stream and an exit code");
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const stream = header.slice(0, space);
|
|
394
|
+
const exitCode = parseExit(header.slice(space + 1).trim());
|
|
395
|
+
if ((stream !== "stdout" && stream !== "stderr") || exitCode < 0) {
|
|
396
|
+
fail(label + " verify header " + header);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const argsEnd = text.indexOf("\n-->", headerEnd);
|
|
400
|
+
if (argsEnd < 0) {
|
|
401
|
+
fail(label + " verify block is missing -->");
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const args = nonEmptyLines(text.slice(headerEnd + 1, argsEnd));
|
|
405
|
+
if (args.length === 0 || args[0] !== "build/plan-verify") {
|
|
406
|
+
fail(label + " verify block must start with build/plan-verify");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const fenceAt = text.indexOf("```", argsEnd);
|
|
410
|
+
const fenceLineEnd = fenceAt < 0 ? -1 : text.indexOf("\n", fenceAt);
|
|
411
|
+
const closeAt = fenceLineEnd < 0 ? -1 : text.indexOf("\n```", fenceLineEnd);
|
|
412
|
+
if (fenceAt < 0 || fenceLineEnd < 0 || closeAt < 0) {
|
|
413
|
+
fail(label + " verify block is missing a fence");
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
fences.push(fenceAt);
|
|
417
|
+
const body = text.slice(fenceLineEnd + 1, closeAt + 1);
|
|
418
|
+
const native = run(args[0], args.slice(1));
|
|
419
|
+
const nodeArgs: string[] = ["--experimental-strip-types", "src/main.ts"];
|
|
420
|
+
for (let i = 1; i < args.length; i++) nodeArgs.push(args[i]);
|
|
421
|
+
const nodeResult = run(process.execPath, nodeArgs);
|
|
422
|
+
const command = args.join(" ");
|
|
423
|
+
if (native.status !== nodeResult.status || native.stdout !== nodeResult.stdout || native.stderr !== nodeResult.stderr) {
|
|
424
|
+
fail(label + " node/native differ " + command);
|
|
425
|
+
console.error("--- node stdout ---\n" + nodeResult.stdout);
|
|
426
|
+
console.error("--- native stdout ---\n" + native.stdout);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const actual = stream === "stdout" ? native.stdout : native.stderr;
|
|
430
|
+
const other = stream === "stdout" ? native.stderr : native.stdout;
|
|
431
|
+
if (native.status !== exitCode || actual !== body || other.length !== 0) {
|
|
432
|
+
fail(label + " sample " + command);
|
|
433
|
+
console.error("expected exit " + String(exitCode) + ", got " + String(native.status));
|
|
434
|
+
console.error("--- cli ---");
|
|
435
|
+
console.error(actual);
|
|
436
|
+
console.error("--- doc ---");
|
|
437
|
+
console.error(body);
|
|
438
|
+
if (other.length > 0) console.error("--- other stream ---\n" + other);
|
|
439
|
+
} else {
|
|
440
|
+
console.log("PASS " + label + " " + command);
|
|
441
|
+
}
|
|
442
|
+
found++;
|
|
443
|
+
cursor = closeAt + 4;
|
|
444
|
+
}
|
|
445
|
+
if (found === 0) fail(label + " has no verify blocks");
|
|
446
|
+
if (strictText) requireTextFencesVerified(label, text, fences);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function requireTextFencesVerified(label: string, text: string, fences: number[]): void {
|
|
450
|
+
const mark = "```text\n";
|
|
451
|
+
let cursor = 0;
|
|
452
|
+
let found = 0;
|
|
453
|
+
while (cursor < text.length) {
|
|
454
|
+
const at = text.indexOf(mark, cursor);
|
|
455
|
+
if (at < 0) break;
|
|
456
|
+
let covered = false;
|
|
457
|
+
for (let i = 0; i < fences.length; i++) {
|
|
458
|
+
if (fences[i] === at) covered = true;
|
|
459
|
+
}
|
|
460
|
+
if (!covered) {
|
|
461
|
+
fail(label + " text sample is not tied to a verify block");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
found++;
|
|
465
|
+
cursor = at + mark.length;
|
|
466
|
+
}
|
|
467
|
+
if (found !== fences.length) fail(label + " verify block is missing a text sample");
|
|
468
|
+
else console.log("PASS " + label + " text samples are all verified");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function parseExit(text: string): number {
|
|
472
|
+
if (text.length === 0) return -1;
|
|
473
|
+
let value = 0;
|
|
474
|
+
for (let i = 0; i < text.length; i++) {
|
|
475
|
+
const code = text.charCodeAt(i);
|
|
476
|
+
if (code < 48 || code > 57) return -1;
|
|
477
|
+
value = value * 10 + (code - 48);
|
|
478
|
+
}
|
|
479
|
+
return value;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function nonEmptyLines(text: string): string[] {
|
|
483
|
+
const lines: string[] = [];
|
|
484
|
+
let start = 0;
|
|
485
|
+
for (let i = 0; i <= text.length; i++) {
|
|
486
|
+
if (i === text.length || text.charCodeAt(i) === 10) {
|
|
487
|
+
let line = text.slice(start, i);
|
|
488
|
+
if (line.length > 0 && line.charCodeAt(line.length - 1) === 13) {
|
|
489
|
+
line = line.slice(0, line.length - 1);
|
|
490
|
+
}
|
|
491
|
+
const trimmed = line.trim();
|
|
492
|
+
if (trimmed.length > 0) lines.push(trimmed);
|
|
493
|
+
start = i + 1;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return lines;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
main();
|