eyeprolog 1.5.32 → 1.5.33
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/README.md +3 -3
- package/conformance-report.md +6 -0
- package/package.json +21 -20
- package/test/conformance/ISO-COMPLIANCE.md +18 -5
- package/test/conformance/NEUMERKEL-LATEST.md +34 -0
- package/test/conformance/NEUMERKEL-LIVE.md +63 -0
- package/test/conformance/README.md +22 -40
- package/test/conformance/WG17-SYNTAX-STATUS.md +1 -1
- package/test/neumerkel.mjs +470 -0
- package/test/run-all.mjs +6 -0
- package/test/run-conformance-all.mjs +16 -0
- package/test/run-conformance-report.mjs +13 -1
- package/test/run-conformance.mjs +1 -1
- package/test/run-neumerkel-tests.mjs +80 -0
- package/test/run-neumerkel.mjs +95 -0
- package/test/run-regression.mjs +8 -3
- package/the-art-of-eyeprolog.md +1 -1
- package/tools/report-wg17-syntax-coverage.mjs +2 -2
- package/tools/upgrade-wg17.mjs +3 -3
package/README.md
CHANGED
|
@@ -74,9 +74,9 @@ All 33 bundled modules that overlap Scryer's current `src/lib/` surface cover Sc
|
|
|
74
74
|
- [Symbiotic Knowledge Graphs](https://eyereasoner.github.io/eyeprolog/examples/deck/symbiotic-knowledge-graphs) — RDF ↔ Prolog heatwave-response demo for human/AI/KG co-evolution
|
|
75
75
|
- [rdf-prolog-roundtrip](https://github.com/eyereasoner/rdf-prolog-roundtrip) — standalone RDF 1.2 ↔ ISO Prolog bridge used by the RDF examples
|
|
76
76
|
- [ISO conformance audit](test/conformance/ISO-COMPLIANCE.md) — supported Part 1 profile
|
|
77
|
-
- [
|
|
77
|
+
- [Latest Neumerkel conformity](test/conformance/NEUMERKEL-LATEST.md) — tracked result from the current live upstream inventory
|
|
78
|
+
- [Conformance report](conformance-report.md) — generated executable conformance status and local corpus summary
|
|
78
79
|
- [OpenRuleBench](openrulebench/README.md) — portable benchmark profile
|
|
79
|
-
|
|
80
80
|
## RDF, Prolog, and symbiotic knowledge graphs
|
|
81
81
|
|
|
82
82
|
EyeProlog can sit behind an RDF knowledge graph without inventing a private graph representation. [`rdf-prolog-roundtrip`](https://github.com/eyereasoner/rdf-prolog-roundtrip) converts RDF 1.2 datasets to ordinary `rdf(Subject, Predicate, Object, Graph)` facts, EyeProlog applies portable rules, and ground `rdf/4` results can be converted back to RDF.
|
|
@@ -95,5 +95,5 @@ cd eyeprolog
|
|
|
95
95
|
npm install
|
|
96
96
|
npm test
|
|
97
97
|
```
|
|
98
|
-
|
|
98
|
+
`npm test` is the release gate and fetches the latest seven Neumerkel conformity sources before the local gates; use `npm run test:offline` for a network-free pass, `npm run test:conformance` for all conformance layers, `npm run test:neumerkel` for live upstream only, and `npm run test:neumerkel:cached` only to reproduce the last fetch. Upstream counts are discovered dynamically and verified against the tracked [latest Neumerkel report](test/conformance/NEUMERKEL-LATEST.md). Exact bytes/hashes stay under Git-ignored `.cache/neumerkel/`; refresh the tracked report with `npm run conformance:update:neumerkel` when upstream changes. Benchmarks remain `npm run benchmark` and `npm run benchmark:lips`.
|
|
99
99
|
EyeProlog is released under the [MIT License](LICENSE.md).
|
package/conformance-report.md
CHANGED
|
@@ -4,6 +4,12 @@ This report combines an executable external conformance gate with the file-based
|
|
|
4
4
|
conformance corpus under `test/conformance/`. The executable result is measured
|
|
5
5
|
when this report is generated; it is not inferred from fixture counts.
|
|
6
6
|
|
|
7
|
+
## Latest Neumerkel evidence
|
|
8
|
+
|
|
9
|
+
See the tracked [latest Neumerkel conformity report](test/conformance/NEUMERKEL-LATEST.md).
|
|
10
|
+
The live release gate fetches all seven TU Wien sources again and verifies that tracked
|
|
11
|
+
report still matches the discovered upstream inventory.
|
|
12
|
+
|
|
7
13
|
## Executable conformance status
|
|
8
14
|
|
|
9
15
|
| Gate | Passed | Total | Status |
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.5.
|
|
6
|
+
"version": "1.5.33",
|
|
7
7
|
"description": "EyeProlog turns facts and rules into answers and proofs.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./index.js",
|
|
@@ -46,31 +46,32 @@
|
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"test": "node test/run-all.mjs",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"test:
|
|
52
|
-
"test:
|
|
53
|
-
"test:
|
|
54
|
-
"test:
|
|
55
|
-
"test:
|
|
49
|
+
"test:offline": "node test/run-all.mjs --offline",
|
|
50
|
+
"test:conformance": "node test/run-conformance-all.mjs",
|
|
51
|
+
"test:conformance:offline": "node test/run-conformance-all.mjs --offline",
|
|
52
|
+
"test:neumerkel": "node test/run-neumerkel.mjs",
|
|
53
|
+
"test:neumerkel:cached": "node test/run-neumerkel.mjs --cached --no-verify-report",
|
|
54
|
+
"test:neumerkel:harness": "node test/run-neumerkel-tests.mjs",
|
|
55
|
+
"test:iso": "node test/run-iso-strict.mjs",
|
|
56
56
|
"test:wg17": "node test/run-wg17.mjs",
|
|
57
|
-
"test:examples": "node test/run-examples.mjs",
|
|
58
57
|
"test:regression": "node test/run-regression.mjs",
|
|
59
|
-
"test:
|
|
60
|
-
"test:
|
|
61
|
-
"test:regression:docs": "node test/run-regression.mjs docs",
|
|
62
|
-
"test:regression:white-box": "node test/run-regression.mjs white-box",
|
|
58
|
+
"test:examples": "node test/run-examples.mjs",
|
|
59
|
+
"test:interop": "node test/run-interop.mjs",
|
|
63
60
|
"test:playground": "node test/run-playground.mjs",
|
|
64
|
-
"wg17:upgrade": "node tools/upgrade-wg17.mjs",
|
|
65
|
-
"report:wg17": "node tools/report-wg17-syntax-coverage.mjs",
|
|
66
|
-
"report:wg17-syntax": "node tools/report-wg17-syntax-coverage.mjs",
|
|
67
|
-
"preversion": "npm test && node test/run-conformance-report.mjs conformance-report.md",
|
|
68
|
-
"postversion": "git push origin HEAD --follow-tags",
|
|
69
61
|
"test:architecture": "node test/run-architecture.mjs",
|
|
62
|
+
"test:openrulebench": "node test/run-openrulebench.mjs",
|
|
63
|
+
"test:benchmark": "node test/run-benchmark-tests.mjs",
|
|
70
64
|
"benchmark": "node test/benchmark.mjs",
|
|
65
|
+
"benchmark:lips": "node test/lips-benchmark.mjs",
|
|
71
66
|
"benchmark:baseline": "node test/benchmark.mjs --save .benchmarks/baseline.json",
|
|
72
|
-
"
|
|
67
|
+
"conformance:update": "npm run conformance:update:wg17 && npm run conformance:update:neumerkel",
|
|
68
|
+
"conformance:report": "node test/run-conformance-report.mjs conformance-report.md",
|
|
69
|
+
"generate": "node tools/generate-library-autoload-index.mjs && node tools/generate-predicate-reference.mjs && node tools/extract-book-examples.mjs",
|
|
70
|
+
"generate:autoload": "node tools/generate-library-autoload-index.mjs",
|
|
73
71
|
"generate:reference": "node tools/generate-predicate-reference.mjs",
|
|
74
|
-
"
|
|
72
|
+
"preversion": "npm test && npm run conformance:report",
|
|
73
|
+
"postversion": "git push origin HEAD --follow-tags",
|
|
74
|
+
"conformance:update:wg17": "node tools/upgrade-wg17.mjs",
|
|
75
|
+
"conformance:update:neumerkel": "node test/run-neumerkel.mjs --update-report"
|
|
75
76
|
}
|
|
76
77
|
}
|
|
@@ -270,17 +270,30 @@ requirement of ISO/IEC 13211-2:2000 or ISO/IEC TS 13211-3.
|
|
|
270
270
|
|
|
271
271
|
## Release gate
|
|
272
272
|
|
|
273
|
-
A release intended to advance ISO conformance
|
|
273
|
+
A release intended to advance ISO conformance uses one canonical command:
|
|
274
274
|
|
|
275
275
|
```sh
|
|
276
276
|
npm test
|
|
277
|
-
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
That command first fetches and runs the **latest seven Neumerkel conformity
|
|
280
|
+
sources**, then executes the deterministic local conformance, strict ISO,
|
|
281
|
+
vendored WG17 regression, regression/API, examples, documentation and
|
|
282
|
+
architecture gates. The upstream case counts are discovered dynamically.
|
|
283
|
+
|
|
284
|
+
Useful focused commands are:
|
|
285
|
+
|
|
286
|
+
```sh
|
|
278
287
|
npm run test:conformance
|
|
288
|
+
npm run test:neumerkel
|
|
289
|
+
npm run test:iso
|
|
279
290
|
npm run test:wg17
|
|
291
|
+
npm run test:offline
|
|
280
292
|
```
|
|
281
293
|
|
|
282
|
-
|
|
283
|
-
|
|
294
|
+
`test:offline` is a development/reproduction aid; it is not sufficient for a
|
|
295
|
+
release claim that says EyeProlog passes the latest Neumerkel suites. Expected
|
|
296
|
+
conformance outputs are never auto-accepted.
|
|
284
297
|
|
|
285
298
|
## Exit criteria for a full conformance claim
|
|
286
299
|
|
|
@@ -300,7 +313,7 @@ post-N289 STC drafts remain review input until standardized.
|
|
|
300
313
|
| Implementation-specific strict/normal boundary is documented and tested | covered | all 5.5 hooks have explicit dispositions; the WG17 cross-profile gate verifies syntax-preservation for standard text accepted by the strict reader. |
|
|
301
314
|
| Published Corrigenda 1-3 are incorporated | covered | `ISO-CORRIGENDA-MATRIX.md` inventories every published amendment cluster with executable, editorial, or superseded disposition |
|
|
302
315
|
| Current post-N289 draft is tracked without silently changing the published baseline | covered | `STC-DRAFT-STATUS.md` tracks reviewed draft items separately from normative requirements |
|
|
303
|
-
|
|
|
316
|
+
| Latest Neumerkel conformity is a live release gate | covered | `npm test` fetches and executes the seven current TU Wien conformity sources with dynamic inventories; the vendored WG17 matrix remains an offline reviewed-outcome regression layer |
|
|
304
317
|
| Third-party standard-core regression provenance is retained | covered | adapted Logtalk, Scryer, Trealla, and SWI-Prolog cases retain source identifiers and licenses in `THIRD_PARTY.md` |
|
|
305
318
|
| No unexplained deviation remains in the release-facing ledger | covered | the release-facing ledger contains no remaining `audit` rows; documented variation points are implementation-defined/specific or draft-only rather than unexplained deviations. |
|
|
306
319
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# EyeProlog — latest Neumerkel conformity
|
|
2
|
+
|
|
3
|
+
Status: **PASS** — **665/665** discovered upstream cases passed.
|
|
4
|
+
|
|
5
|
+
This tracked report records the latest upstream inventory successfully checked by EyeProlog.
|
|
6
|
+
`npm test` fetches the seven TU Wien sources again and verifies that these discovered counts
|
|
7
|
+
still match the live suites. Counts are output from upstream, not hard-coded test constants.
|
|
8
|
+
|
|
9
|
+
| Suite | Passed | Total |
|
|
10
|
+
|---|---:|---:|
|
|
11
|
+
| syntax | 366 | 366 |
|
|
12
|
+
| number_chars/2 | 78 | 78 |
|
|
13
|
+
| variable_names/1 | 75 | 75 |
|
|
14
|
+
| dif/2 | 26 | 26 |
|
|
15
|
+
| length/2 | 37 | 37 |
|
|
16
|
+
| phrase/2,3 | 58 | 58 |
|
|
17
|
+
| setup_call_cleanup/3 | 25 | 25 |
|
|
18
|
+
| **Total** | **665** | **665** |
|
|
19
|
+
|
|
20
|
+
## Upstream sources
|
|
21
|
+
|
|
22
|
+
- [syntax](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/conformity_testing)
|
|
23
|
+
- [number_chars](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/number_chars_cont_quad.pl)
|
|
24
|
+
- [variable_names](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/variable_names_quad.pl)
|
|
25
|
+
- [dif](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/dif)
|
|
26
|
+
- [length](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/length_quad.pl)
|
|
27
|
+
- [phrase](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/phrase_quad.pl)
|
|
28
|
+
- [cleanup](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/cleanup)
|
|
29
|
+
|
|
30
|
+
Exact fetched bytes, SHA-256 hashes, fetch timestamps, and HTTP validators remain under
|
|
31
|
+
Git-ignored `.cache/neumerkel/` for local audit/reproduction and are intentionally not committed.
|
|
32
|
+
Refresh this tracked report with `npm run conformance:update:neumerkel` and commit it whenever
|
|
33
|
+
the live upstream inventory changes.
|
|
34
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Live Neumerkel conformity gate
|
|
2
|
+
|
|
3
|
+
EyeProlog treats Ulrich Neumerkel's current ISO/WG17 conformity material as a
|
|
4
|
+
moving upstream release gate, not as a frozen snapshot with permanent case
|
|
5
|
+
counts.
|
|
6
|
+
|
|
7
|
+
`npm run test:neumerkel` fetches these seven TU Wien sources on every live run:
|
|
8
|
+
|
|
9
|
+
1. `conformity_testing` — Part 1 syntax/reader/writer matrix;
|
|
10
|
+
2. `number_chars_cont_quad.pl` — `number_chars/2` continuation corpus;
|
|
11
|
+
3. `variable_names_quad.pl` — `variable_names/1` corpus;
|
|
12
|
+
4. `dif` — `dif/2` comparison table;
|
|
13
|
+
5. `length_quad.pl` — `length/2` corpus;
|
|
14
|
+
6. `phrase_quad.pl` — `phrase/2,3` corpus;
|
|
15
|
+
7. `cleanup` — `setup_call_cleanup/3` examples.
|
|
16
|
+
|
|
17
|
+
The runner discovers the inventory at run time. A new upstream row is therefore
|
|
18
|
+
executed automatically and a removed row disappears automatically.
|
|
19
|
+
|
|
20
|
+
## Tracked GitHub evidence
|
|
21
|
+
|
|
22
|
+
The latest successful discovered inventory is committed as
|
|
23
|
+
[`NEUMERKEL-LATEST.md`](NEUMERKEL-LATEST.md). This is the stable report to link
|
|
24
|
+
from GitHub, releases, or other documentation.
|
|
25
|
+
|
|
26
|
+
A normal live run verifies that the tracked Markdown still matches the current
|
|
27
|
+
upstream suite counts. If the upstream inventory changed, the gate fails with an
|
|
28
|
+
instruction to refresh it:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npm run conformance:update:neumerkel
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Commit the resulting `test/conformance/NEUMERKEL-LATEST.md` after reviewing the
|
|
35
|
+
change. The tracked report intentionally omits fetch timestamps and HTTP
|
|
36
|
+
validators, so repeated runs against unchanged upstream suites do not dirty the
|
|
37
|
+
checkout.
|
|
38
|
+
|
|
39
|
+
## Local audit cache
|
|
40
|
+
|
|
41
|
+
Exact downloaded bytes, SHA-256 hashes, fetch timestamps, HTTP validators, and
|
|
42
|
+
machine-readable results are kept under `.cache/neumerkel/`. `.cache/` remains
|
|
43
|
+
Git-ignored: it is an audit/reproduction cache, not published project evidence.
|
|
44
|
+
|
|
45
|
+
Use:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npm run test:neumerkel
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
for the canonical live check. For offline reproduction of the exact last live
|
|
52
|
+
fetch, use:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
npm run test:neumerkel:cached
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The cached command never claims to check the latest upstream suites and is not
|
|
59
|
+
used by the release gate.
|
|
60
|
+
|
|
61
|
+
The vendored WG17 syntax matrix remains useful as a deterministic reviewed
|
|
62
|
+
regression snapshot, but it is secondary to this live gate: passing the snapshot
|
|
63
|
+
cannot mask a new or changed Neumerkel case.
|
|
@@ -69,68 +69,55 @@ Case names may be nested in category directories such as `arithmetic/`, `strings
|
|
|
69
69
|
|
|
70
70
|
## Running the suite
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
The release gate is network-aware by design: it checks the current upstream
|
|
73
|
+
Neumerkel conformity material before the deterministic local corpus.
|
|
74
74
|
|
|
75
75
|
```sh
|
|
76
76
|
npm test
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
For an offline-only development pass:
|
|
80
80
|
|
|
81
81
|
```sh
|
|
82
|
-
|
|
82
|
+
npm run test:offline
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
The conformance commands are:
|
|
86
86
|
|
|
87
87
|
```sh
|
|
88
|
-
npm run test:
|
|
88
|
+
npm run test:conformance # live Neumerkel + local ISO/conformance layers
|
|
89
|
+
npm run test:conformance:offline # same local layers, no network
|
|
90
|
+
npm run test:neumerkel # the seven live upstream suites only
|
|
91
|
+
npm run test:neumerkel:cached # exact last fetched bytes; reproduction only
|
|
92
|
+
npm run test:iso # Part 1 + Corrigenda strict-core processor gate
|
|
93
|
+
npm run test:wg17 # vendored reviewed WG17 syntax regression
|
|
89
94
|
```
|
|
90
95
|
|
|
91
|
-
|
|
96
|
+
`test:neumerkel` always fetches the current TU Wien sources. It does not skip a
|
|
97
|
+
fetch because a cache exists. The runner discovers the number of active tests from those sources and fails on any newly introduced case EyeProlog does not pass. It verifies the stable, tracked [NEUMERKEL-LATEST.md](NEUMERKEL-LATEST.md), which is the GitHub-facing result to cite. Exact bytes, SHA-256 hashes, timestamps, and HTTP validators stay under Git-ignored `.cache/neumerkel/` for audit/reproduction only. Refresh the tracked report with `npm run conformance:update:neumerkel` when the live inventory changes. See [NEUMERKEL-LIVE.md](NEUMERKEL-LIVE.md).
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
npm run test:wg17
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Refresh the WG17 snapshot from the TU Wien conformity tables before a release
|
|
98
|
-
or whenever upstream changes:
|
|
99
|
+
The vendored WG17 syntax snapshot is intentionally secondary. Update all upstream conformance evidence with `npm run conformance:update`, or use the focused commands:
|
|
99
100
|
|
|
100
101
|
```sh
|
|
101
|
-
npm run wg17
|
|
102
|
+
npm run conformance:update:wg17
|
|
103
|
+
npm run conformance:update:neumerkel
|
|
102
104
|
npm run test:wg17
|
|
103
105
|
```
|
|
104
106
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
no local snapshot. **Every case is always checked independently against the
|
|
109
|
-
upstream Codex expectation**, so a reviewed EyeProlog outcome can never make a
|
|
110
|
-
non-conforming result pass (the failure mode that previously hid WG17 #227).
|
|
111
|
-
The runner follows the upstream `read(G), G` input protocol, including the
|
|
112
|
-
terminating newline, so stream-sensitive cases such as #270 and #271 exercise
|
|
113
|
-
the characters left after `read/1`. Normal `npm test` remains offline and uses
|
|
114
|
-
only the committed upstream snapshot.
|
|
107
|
+
Every vendored syntax case is still checked against its upstream Codex
|
|
108
|
+
expectation; reviewed exact outcomes are an additional regression lock rather
|
|
109
|
+
than an alternative acceptance rule.
|
|
115
110
|
|
|
116
|
-
Regenerate the
|
|
111
|
+
Regenerate the top-level local-corpus report with `npm run conformance:report`. It links to [NEUMERKEL-LATEST.md](NEUMERKEL-LATEST.md) rather than duplicating live evidence. Counts in the tracked Neumerkel report are generated evidence, not permanent constants in project policy.
|
|
117
112
|
|
|
118
|
-
|
|
119
|
-
node test/run-conformance-report.mjs
|
|
120
|
-
node test/run-conformance-report.mjs conformance-report.md
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
Run matching conformance cases by passing a filename or directory fragment:
|
|
113
|
+
Run a matching local file-based conformance subset directly with:
|
|
124
114
|
|
|
125
115
|
```sh
|
|
126
116
|
node test/run-conformance.mjs reusable
|
|
127
|
-
node test/run-conformance.mjs 092_scalar_string_conversions
|
|
128
117
|
node test/run-conformance.mjs variables/
|
|
129
118
|
node test/run-conformance.mjs error/variables
|
|
130
119
|
```
|
|
131
120
|
|
|
132
|
-
The runner executes normal programs with queries in-process through the public JavaScript API so small conformance cases avoid measuring Node startup overhead. Warning and proof cases intentionally use the CLI because warning output and `why/2` proof output are host-interface contracts.
|
|
133
|
-
|
|
134
121
|
## Scope
|
|
135
122
|
|
|
136
123
|
The corpus covers accepted syntax, typed scalar identity and explicit scalar conversions, query answers,
|
|
@@ -147,12 +134,7 @@ Selected cases are adapted from the ISO and standard-core suites of Logtalk,
|
|
|
147
134
|
Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
|
|
148
135
|
are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
|
|
149
136
|
|
|
150
|
-
The corpus has 386 cases in `iso/` and 802 file-based conformance cases in
|
|
151
|
-
total. Of those, 11 cases in `stc/` are explicitly labelled working-draft
|
|
152
|
-
review evidence rather than normative ISO claims. The separate strict-reader WG17 matrix has 366 executable dispositions.
|
|
153
|
-
The generated `conformance-report.md` is the authoritative source for the current
|
|
154
|
-
executable WG17 syntax result and file-based category totals. Together with regression, documentation-sync, API, example,
|
|
155
|
-
and book-example checks, `npm test` is the release gate.
|
|
137
|
+
The corpus has 386 cases in `iso/` and 802 file-based conformance cases in total. Of those, 11 cases in `stc/` are explicitly labelled working-draft review evidence rather than normative ISO claims. The separate vendored strict-reader WG17 matrix is a deterministic regression snapshot; the live Neumerkel gate discovers the current upstream inventory at run time and verifies the tracked `NEUMERKEL-LATEST.md`. The generated `conformance-report.md` records local corpus totals and links to that live evidence. Together with regression, documentation-sync, API, example, and book-example checks, `npm test` is the release gate.
|
|
156
138
|
|
|
157
139
|
## Updating expected output
|
|
158
140
|
|
|
@@ -46,6 +46,6 @@ None.
|
|
|
46
46
|
|
|
47
47
|
## Maintenance
|
|
48
48
|
|
|
49
|
-
1. Run `npm run
|
|
49
|
+
1. Run `npm run conformance:update` to reconcile the dated fixture with upstream.
|
|
50
50
|
2. Review every new or changed ISO expectation before adding its expected outcome.
|
|
51
51
|
3. Run `npm run test:wg17` and keep this generated status page synchronized.
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { run } from '../src/index.js';
|
|
8
|
+
import {
|
|
9
|
+
countTopLevelTerms,
|
|
10
|
+
decodeDocument,
|
|
11
|
+
htmlCellText,
|
|
12
|
+
parseWg17SyntaxTable,
|
|
13
|
+
setupInput,
|
|
14
|
+
} from '../tools/upgrade-wg17.mjs';
|
|
15
|
+
import {
|
|
16
|
+
executeWg17Item,
|
|
17
|
+
matchesUpstreamExpectation,
|
|
18
|
+
wg17TestDescription,
|
|
19
|
+
} from './run-wg17.mjs';
|
|
20
|
+
|
|
21
|
+
const testRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const packageRoot = path.resolve(testRoot, '..');
|
|
23
|
+
const defaultCacheDir = path.join(packageRoot, '.cache', 'neumerkel');
|
|
24
|
+
export const NEUMERKEL_REPORT_PATH = path.join(packageRoot, 'test', 'conformance', 'NEUMERKEL-LATEST.md');
|
|
25
|
+
const baseUrl = 'https://www.complang.tuwien.ac.at/ulrich/iso-prolog/';
|
|
26
|
+
|
|
27
|
+
export const NEUMERKEL_SOURCES = Object.freeze([
|
|
28
|
+
{ key: 'syntax', filename: 'conformity_testing.html', url: `${baseUrl}conformity_testing`, kind: 'syntax' },
|
|
29
|
+
{ key: 'number_chars', filename: 'number_chars_cont_quad.pl', url: `${baseUrl}number_chars_cont_quad.pl`, kind: 'quad' },
|
|
30
|
+
{ key: 'variable_names', filename: 'variable_names_quad.pl', url: `${baseUrl}variable_names_quad.pl`, kind: 'quad' },
|
|
31
|
+
{ key: 'dif', filename: 'dif.html', url: `${baseUrl}dif`, kind: 'dif' },
|
|
32
|
+
{ key: 'length', filename: 'length_quad.pl', url: `${baseUrl}length_quad.pl`, kind: 'quad' },
|
|
33
|
+
{ key: 'phrase', filename: 'phrase_quad.pl', url: `${baseUrl}phrase_quad.pl`, kind: 'quad' },
|
|
34
|
+
{ key: 'cleanup', filename: 'cleanup.html', url: `${baseUrl}cleanup`, kind: 'cleanup' },
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
function sha256(bytes) {
|
|
38
|
+
return crypto.createHash('sha256').update(bytes).digest('hex');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function safeMkdir(dir) {
|
|
42
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sourceRevision(text) {
|
|
46
|
+
const normalized = htmlCellText(text);
|
|
47
|
+
const matches = [...normalized.matchAll(/\b(?:revision|version)\s+([0-9]+(?:\.[0-9]+)*)/gi)];
|
|
48
|
+
return matches.at(-1)?.[1] ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function fetchOne(source) {
|
|
52
|
+
let response;
|
|
53
|
+
try {
|
|
54
|
+
response = await fetch(source.url, {
|
|
55
|
+
headers: { 'user-agent': 'EyeProlog-Neumerkel-Conformance/1' },
|
|
56
|
+
redirect: 'follow',
|
|
57
|
+
cache: 'no-store',
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Neumerkel live fetch failed for ${source.key} (${source.url}). ` +
|
|
62
|
+
'The canonical conformance gate requires network access; use npm run test:offline ' +
|
|
63
|
+
'for a network-free local run or npm run test:neumerkel:cached to reproduce the last live fetch.',
|
|
64
|
+
{ cause: error },
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new Error(`Neumerkel fetch failed for ${source.key} (${source.url}): ${response.status} ${response.statusText}`);
|
|
69
|
+
}
|
|
70
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
71
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
72
|
+
return {
|
|
73
|
+
bytes,
|
|
74
|
+
text: decodeDocument(bytes, contentType),
|
|
75
|
+
metadata: {
|
|
76
|
+
key: source.key,
|
|
77
|
+
url: source.url,
|
|
78
|
+
filename: source.filename,
|
|
79
|
+
sha256: sha256(bytes),
|
|
80
|
+
bytes: bytes.byteLength,
|
|
81
|
+
etag: response.headers.get('etag'),
|
|
82
|
+
lastModified: response.headers.get('last-modified'),
|
|
83
|
+
contentType,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readCachedOne(source, cacheDir) {
|
|
89
|
+
const filename = path.join(cacheDir, source.filename);
|
|
90
|
+
if (!fs.existsSync(filename)) {
|
|
91
|
+
throw new Error(`Neumerkel cache is incomplete: missing ${path.relative(packageRoot, filename)}; run npm run test:neumerkel online first`);
|
|
92
|
+
}
|
|
93
|
+
const bytes = new Uint8Array(fs.readFileSync(filename));
|
|
94
|
+
return {
|
|
95
|
+
bytes,
|
|
96
|
+
text: decodeDocument(bytes, ''),
|
|
97
|
+
metadata: {
|
|
98
|
+
key: source.key,
|
|
99
|
+
url: source.url,
|
|
100
|
+
filename: source.filename,
|
|
101
|
+
sha256: sha256(bytes),
|
|
102
|
+
bytes: bytes.byteLength,
|
|
103
|
+
cached: true,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readSourceDirOne(source, sourceDir) {
|
|
109
|
+
const filename = path.join(sourceDir, source.filename);
|
|
110
|
+
if (!fs.existsSync(filename)) throw new Error(`missing Neumerkel source fixture: ${filename}`);
|
|
111
|
+
const bytes = new Uint8Array(fs.readFileSync(filename));
|
|
112
|
+
return {
|
|
113
|
+
bytes,
|
|
114
|
+
text: decodeDocument(bytes, ''),
|
|
115
|
+
metadata: {
|
|
116
|
+
key: source.key,
|
|
117
|
+
url: source.url,
|
|
118
|
+
filename: source.filename,
|
|
119
|
+
sha256: sha256(bytes),
|
|
120
|
+
bytes: bytes.byteLength,
|
|
121
|
+
sourceDir: true,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function loadNeumerkelSources({ mode = 'live', cacheDir = defaultCacheDir, sourceDir = null } = {}) {
|
|
127
|
+
const loaded = new Map();
|
|
128
|
+
for (const source of NEUMERKEL_SOURCES) {
|
|
129
|
+
const item = sourceDir != null
|
|
130
|
+
? readSourceDirOne(source, sourceDir)
|
|
131
|
+
: mode === 'cached'
|
|
132
|
+
? readCachedOne(source, cacheDir)
|
|
133
|
+
: await fetchOne(source);
|
|
134
|
+
loaded.set(source.key, { ...source, ...item, localPath: sourceDir != null ? path.join(sourceDir, source.filename) : path.join(cacheDir, source.filename) });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (mode === 'live' && sourceDir == null) {
|
|
138
|
+
safeMkdir(cacheDir);
|
|
139
|
+
for (const item of loaded.values()) fs.writeFileSync(path.join(cacheDir, item.filename), item.bytes);
|
|
140
|
+
const fetchedAt = new Date().toISOString();
|
|
141
|
+
const manifest = {
|
|
142
|
+
schema: 1,
|
|
143
|
+
policy: 'live Neumerkel sources fetched for this conformance run; cached copies are reproducibility aids only',
|
|
144
|
+
fetchedAt,
|
|
145
|
+
sources: [...loaded.values()].map((item) => ({
|
|
146
|
+
...item.metadata,
|
|
147
|
+
revision: item.kind === 'syntax' || item.kind === 'cleanup' ? sourceRevision(item.text) : null,
|
|
148
|
+
})),
|
|
149
|
+
};
|
|
150
|
+
fs.writeFileSync(path.join(cacheDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return loaded;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function materializeSyntaxCases(html) {
|
|
157
|
+
const rows = parseWg17SyntaxTable(html);
|
|
158
|
+
let precedingBaseQuery = null;
|
|
159
|
+
return rows.map((row) => {
|
|
160
|
+
const input = setupInput(row.query, precedingBaseQuery);
|
|
161
|
+
const item = {
|
|
162
|
+
id: row.id,
|
|
163
|
+
query: row.query,
|
|
164
|
+
input,
|
|
165
|
+
readCount: countTopLevelTerms(input),
|
|
166
|
+
expected: row.expected,
|
|
167
|
+
assertion: 'upstream-live',
|
|
168
|
+
};
|
|
169
|
+
if (!row.query.includes('/**/')) precedingBaseQuery = row.query;
|
|
170
|
+
return item;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function htmlTableRows(html) {
|
|
175
|
+
const rowStarts = [...html.matchAll(/<tr\b[^>]*>/gi)];
|
|
176
|
+
const rows = [];
|
|
177
|
+
for (let rowIndex = 0; rowIndex < rowStarts.length; rowIndex++) {
|
|
178
|
+
const rowStart = rowStarts[rowIndex];
|
|
179
|
+
const bodyStart = rowStart.index + rowStart[0].length;
|
|
180
|
+
const nextRow = rowStarts[rowIndex + 1]?.index ?? html.length;
|
|
181
|
+
const explicitEnd = html.slice(bodyStart, nextRow).search(/<\/tr\s*>/i);
|
|
182
|
+
const rowEnd = explicitEnd < 0 ? nextRow : bodyStart + explicitEnd;
|
|
183
|
+
const body = html.slice(bodyStart, rowEnd);
|
|
184
|
+
const cellStarts = [...body.matchAll(/<t[dh]\b[^>]*>/gi)];
|
|
185
|
+
const cells = [];
|
|
186
|
+
for (let cellIndex = 0; cellIndex < cellStarts.length; cellIndex++) {
|
|
187
|
+
const cellStart = cellStarts[cellIndex];
|
|
188
|
+
const cellBodyStart = cellStart.index + cellStart[0].length;
|
|
189
|
+
const cellEnd = cellStarts[cellIndex + 1]?.index ?? body.length;
|
|
190
|
+
cells.push(body.slice(cellBodyStart, cellEnd));
|
|
191
|
+
}
|
|
192
|
+
rows.push(cells);
|
|
193
|
+
}
|
|
194
|
+
return rows;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function stripPromptAndDot(query) {
|
|
198
|
+
return String(query)
|
|
199
|
+
.replace(/^\s*\?-\s*/, '')
|
|
200
|
+
.trim()
|
|
201
|
+
.replace(/\.\s*$/, '')
|
|
202
|
+
.trim();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function classifyDifExpected(text) {
|
|
206
|
+
const expected = String(text).replace(/\s+/g, ' ').trim();
|
|
207
|
+
if (expected.length === 0) return null;
|
|
208
|
+
if (expected.includes('|')) return 'lenient';
|
|
209
|
+
if (/^(?:false|fails?)\b/i.test(expected)) return 'fails';
|
|
210
|
+
if (/^(?:true|succeeds?)\b/i.test(expected)) return 'succeeds';
|
|
211
|
+
if (expected.includes('=')) return 'succeeds';
|
|
212
|
+
return 'lenient';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function parseDifCases(html) {
|
|
216
|
+
const result = [];
|
|
217
|
+
const seen = new Set();
|
|
218
|
+
for (const cells of htmlTableRows(html)) {
|
|
219
|
+
if (cells.length < 3) continue;
|
|
220
|
+
const idText = htmlCellText(cells[0]).replace(/^#\s*/, '').trim();
|
|
221
|
+
if (!/^\d+$/.test(idText)) continue;
|
|
222
|
+
const id = Number(idText);
|
|
223
|
+
if (seen.has(id)) continue;
|
|
224
|
+
const query = stripPromptAndDot(htmlCellText(cells[1]));
|
|
225
|
+
const expectedText = htmlCellText(cells[2]);
|
|
226
|
+
const expected = classifyDifExpected(expectedText);
|
|
227
|
+
if (!query || expected == null) continue;
|
|
228
|
+
result.push({ id, query, expected, expectedText });
|
|
229
|
+
seen.add(id);
|
|
230
|
+
}
|
|
231
|
+
if (result.length === 0) throw new Error('no Neumerkel dif/2 cases found; upstream HTML format may have changed');
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function htmlToText(html) {
|
|
236
|
+
return html
|
|
237
|
+
.replace(/<!--[^]*?-->/g, '')
|
|
238
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
239
|
+
.replace(/<\/(?:p|div|pre|li|blockquote|h[1-6]|table|tr)>/gi, '\n')
|
|
240
|
+
.replace(/<[^>]+>/g, '')
|
|
241
|
+
.replace(/ /gi, ' ')
|
|
242
|
+
.replace(/&/gi, '&')
|
|
243
|
+
.replace(/</gi, '<')
|
|
244
|
+
.replace(/>/gi, '>')
|
|
245
|
+
.replace(/"/gi, '"')
|
|
246
|
+
.replace(/'|'/gi, "'")
|
|
247
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
|
|
248
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, n) => String.fromCodePoint(Number.parseInt(n, 16)))
|
|
249
|
+
.replace(/\r\n?/g, '\n');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function classifyCleanupDescription(text) {
|
|
253
|
+
const first = String(text).trim();
|
|
254
|
+
if (/^Fails\b/i.test(first)) return 'fails';
|
|
255
|
+
if (/^Instantiation error\b/i.test(first)) return 'instantiation_error';
|
|
256
|
+
if (/^Type error\b/i.test(first)) return 'type_error';
|
|
257
|
+
if (/^System error\b/i.test(first)) return 'error';
|
|
258
|
+
if (/^Succeeds\b/i.test(first)) return 'succeeds';
|
|
259
|
+
return 'lenient';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function parseCleanupCases(html) {
|
|
263
|
+
const lines = htmlToText(html).split('\n');
|
|
264
|
+
const cases = [];
|
|
265
|
+
for (let index = 0; index < lines.length; index++) {
|
|
266
|
+
const line = lines[index].trimEnd();
|
|
267
|
+
const trimmed = line.trim();
|
|
268
|
+
if (!trimmed.includes('setup_call_cleanup') || !trimmed.endsWith('.')) continue;
|
|
269
|
+
// Match the reference extraction protocol: only complete one-line example
|
|
270
|
+
// goals are executable. Multi-line prose examples remain specification
|
|
271
|
+
// evidence but are not silently reassembled by the harness.
|
|
272
|
+
if (/^setup_call_cleanup\b/.test(trimmed) === false && /^catch\(.*setup_call_cleanup/.test(trimmed) === false) continue;
|
|
273
|
+
const goal = trimmed.slice(0, -1).trim();
|
|
274
|
+
if (/\bopen\s*\(/.test(goal)) continue;
|
|
275
|
+
|
|
276
|
+
let description = '';
|
|
277
|
+
for (let next = index + 1; next < lines.length; next++) {
|
|
278
|
+
const candidate = lines[next];
|
|
279
|
+
if (candidate.trim() === '') continue;
|
|
280
|
+
description = candidate.trim();
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
if (!description) continue;
|
|
284
|
+
cases.push({ id: cases.length + 1, goal, expected: classifyCleanupDescription(description), description });
|
|
285
|
+
}
|
|
286
|
+
if (cases.length === 0) throw new Error('no Neumerkel setup_call_cleanup/3 examples found; upstream HTML format may have changed');
|
|
287
|
+
return cases;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function executeGoal(goal) {
|
|
291
|
+
try {
|
|
292
|
+
const result = run('', { goal: `once((${goal}))` });
|
|
293
|
+
return result.stats.completed_goal_lists > 0 ? { type: 'succeeds' } : { type: 'fails' };
|
|
294
|
+
} catch (error) {
|
|
295
|
+
const formal = String(error?.formal ?? error?.message ?? error);
|
|
296
|
+
if (/instantiation_error/.test(formal)) return { type: 'instantiation_error', formal };
|
|
297
|
+
if (/type_error/.test(formal)) return { type: 'type_error', formal };
|
|
298
|
+
return { type: 'error', formal };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function expectedMatches(expected, actual) {
|
|
303
|
+
if (expected === 'lenient') return true;
|
|
304
|
+
if (expected === 'error') return ['error', 'instantiation_error', 'type_error'].includes(actual.type);
|
|
305
|
+
return expected === actual.type;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function ensureQuadSuccess(label, item) {
|
|
309
|
+
const cli = path.join(packageRoot, 'bin', 'eyeprolog.js');
|
|
310
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'eyeprolog-neumerkel-'));
|
|
311
|
+
try {
|
|
312
|
+
// Some upstream option tests deliberately open a relative file named `f`.
|
|
313
|
+
// Run every live quad in an isolated cwd so conformance cannot dirty the checkout.
|
|
314
|
+
const child = spawnSync(process.execPath, [cli, '-q', item.localPath], { encoding: 'utf8', cwd: scratch });
|
|
315
|
+
if (child.status !== 0) {
|
|
316
|
+
throw new Error(`${label}: EyeProlog quad runner exited ${child.status}\n${child.stdout}${child.stderr}`);
|
|
317
|
+
}
|
|
318
|
+
const match = String(child.stdout).match(/quads:\s+(\d+) run,\s+(\d+) passed,\s+(\d+) failed(?:,\s+(\d+) undecided)?\./);
|
|
319
|
+
if (match == null) throw new Error(`${label}: could not parse quad report\n${child.stdout}${child.stderr}`);
|
|
320
|
+
const total = Number(match[1]);
|
|
321
|
+
const passed = Number(match[2]);
|
|
322
|
+
const failed = Number(match[3]);
|
|
323
|
+
const undecided = Number(match[4] ?? 0);
|
|
324
|
+
if (failed !== 0 || undecided !== 0 || passed !== total) {
|
|
325
|
+
throw new Error(`${label}: ${passed}/${total} passed, ${failed} failed, ${undecided} undecided\n${child.stdout}${child.stderr}`);
|
|
326
|
+
}
|
|
327
|
+
return { total, passed, failed, undecided, stdout: child.stdout };
|
|
328
|
+
} finally {
|
|
329
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export async function executeNeumerkel({ reporter, mode = 'live', cacheDir = defaultCacheDir, sourceDir = null } = {}) {
|
|
334
|
+
const sources = await loadNeumerkelSources({ mode, cacheDir, sourceDir });
|
|
335
|
+
const summary = {};
|
|
336
|
+
|
|
337
|
+
reporter.section(`Neumerkel conformity (${mode === 'live' && sourceDir == null ? 'live upstream' : sourceDir != null ? 'source fixtures' : 'cached'})`);
|
|
338
|
+
|
|
339
|
+
const syntaxCases = materializeSyntaxCases(sources.get('syntax').text);
|
|
340
|
+
for (const item of syntaxCases) {
|
|
341
|
+
reporter.test(`syntax ${wg17TestDescription(item)}`, () => {
|
|
342
|
+
const actual = executeWg17Item(item);
|
|
343
|
+
if (!matchesUpstreamExpectation(item.expected, actual, item)) {
|
|
344
|
+
throw new Error(`syntax #${item.id} expected ${item.expected}; actual ${JSON.stringify(actual)}`);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
summary.syntax = { passed: syntaxCases.length, total: syntaxCases.length };
|
|
349
|
+
|
|
350
|
+
for (const key of ['number_chars', 'variable_names', 'length', 'phrase']) {
|
|
351
|
+
let result;
|
|
352
|
+
reporter.test(`${key.replace('_', ' ')} live corpus`, () => {
|
|
353
|
+
result = ensureQuadSuccess(key, sources.get(key));
|
|
354
|
+
});
|
|
355
|
+
summary[key] = { passed: result.total, total: result.total };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const difCases = parseDifCases(sources.get('dif').text);
|
|
359
|
+
for (const item of difCases) {
|
|
360
|
+
reporter.test(`dif #${item.id} ${item.query}`, () => {
|
|
361
|
+
const actual = executeGoal(item.query);
|
|
362
|
+
if (!expectedMatches(item.expected, actual)) {
|
|
363
|
+
throw new Error(`dif #${item.id} expected ${item.expected} (${item.expectedText}); actual ${JSON.stringify(actual)}`);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
summary.dif = { passed: difCases.length, total: difCases.length };
|
|
368
|
+
|
|
369
|
+
const cleanupCases = parseCleanupCases(sources.get('cleanup').text);
|
|
370
|
+
for (const item of cleanupCases) {
|
|
371
|
+
reporter.test(`cleanup #${item.id} ${item.goal}`, () => {
|
|
372
|
+
const actual = executeGoal(item.goal);
|
|
373
|
+
if (!expectedMatches(item.expected, actual)) {
|
|
374
|
+
throw new Error(`cleanup #${item.id} expected ${item.expected} (${item.description}); actual ${JSON.stringify(actual)}`);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
summary.cleanup = { passed: cleanupCases.length, total: cleanupCases.length };
|
|
379
|
+
|
|
380
|
+
reporter.sectionTotal('live Neumerkel conformity');
|
|
381
|
+
|
|
382
|
+
const cacheManifest = path.join(cacheDir, 'manifest.json');
|
|
383
|
+
const cachedManifest = fs.existsSync(cacheManifest) ? JSON.parse(fs.readFileSync(cacheManifest, 'utf8')) : null;
|
|
384
|
+
const manifest = sourceDir != null
|
|
385
|
+
? {
|
|
386
|
+
schema: 1,
|
|
387
|
+
policy: 'local source fixtures used for this conformance run',
|
|
388
|
+
fetchedAt: null,
|
|
389
|
+
sources: [...sources.values()].map((item) => ({
|
|
390
|
+
...item.metadata,
|
|
391
|
+
revision: item.kind === 'syntax' || item.kind === 'cleanup' ? sourceRevision(item.text) : null,
|
|
392
|
+
})),
|
|
393
|
+
}
|
|
394
|
+
: cachedManifest;
|
|
395
|
+
if (mode === 'live' && sourceDir == null && manifest != null) {
|
|
396
|
+
fs.writeFileSync(path.join(cacheDir, 'results.json'), `${JSON.stringify({
|
|
397
|
+
schema: 1,
|
|
398
|
+
fetchedAt: manifest.fetchedAt,
|
|
399
|
+
summary,
|
|
400
|
+
sourceSha256: Object.fromEntries(manifest.sources.map((source) => [source.key, source.sha256])),
|
|
401
|
+
}, null, 2)}\n`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const reportText = formatNeumerkelMarkdown({ summary, manifest });
|
|
405
|
+
return { summary, manifest, cacheDir, reportPath: NEUMERKEL_REPORT_PATH, reportText };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function formatNeumerkelSummary(summary) {
|
|
409
|
+
const labels = [
|
|
410
|
+
['syntax', 'syntax'],
|
|
411
|
+
['number_chars', 'number_chars/2'],
|
|
412
|
+
['variable_names', 'variable_names/1'],
|
|
413
|
+
['dif', 'dif/2'],
|
|
414
|
+
['length', 'length/2'],
|
|
415
|
+
['phrase', 'phrase/2,3'],
|
|
416
|
+
['cleanup', 'setup_call_cleanup/3'],
|
|
417
|
+
];
|
|
418
|
+
return labels.map(([key, label]) => {
|
|
419
|
+
const row = summary[key];
|
|
420
|
+
return `${label.padEnd(22)} ${row.passed}/${row.total}`;
|
|
421
|
+
}).join('\n');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function formatNeumerkelMarkdown({ summary }) {
|
|
425
|
+
const labels = [
|
|
426
|
+
['syntax', 'syntax'],
|
|
427
|
+
['number_chars', 'number_chars/2'],
|
|
428
|
+
['variable_names', 'variable_names/1'],
|
|
429
|
+
['dif', 'dif/2'],
|
|
430
|
+
['length', 'length/2'],
|
|
431
|
+
['phrase', 'phrase/2,3'],
|
|
432
|
+
['cleanup', 'setup_call_cleanup/3'],
|
|
433
|
+
];
|
|
434
|
+
const rows = labels.map(([key, label]) => ({ key, label, ...summary[key] }));
|
|
435
|
+
const passed = rows.reduce((sum, row) => sum + row.passed, 0);
|
|
436
|
+
const total = rows.reduce((sum, row) => sum + row.total, 0);
|
|
437
|
+
const lines = [
|
|
438
|
+
'# EyeProlog — latest Neumerkel conformity',
|
|
439
|
+
'',
|
|
440
|
+
`Status: **${passed === total ? 'PASS' : 'FAIL'}** — **${passed}/${total}** discovered upstream cases passed.`,
|
|
441
|
+
'',
|
|
442
|
+
'This tracked report records the latest upstream inventory successfully checked by EyeProlog.',
|
|
443
|
+
'`npm test` fetches the seven TU Wien sources again and verifies that these discovered counts',
|
|
444
|
+
'still match the live suites. Counts are output from upstream, not hard-coded test constants.',
|
|
445
|
+
'',
|
|
446
|
+
'| Suite | Passed | Total |',
|
|
447
|
+
'|---|---:|---:|',
|
|
448
|
+
];
|
|
449
|
+
for (const row of rows) lines.push(`| ${row.label} | ${row.passed} | ${row.total} |`);
|
|
450
|
+
lines.push(`| **Total** | **${passed}** | **${total}** |`);
|
|
451
|
+
|
|
452
|
+
lines.push(
|
|
453
|
+
'',
|
|
454
|
+
'## Upstream sources',
|
|
455
|
+
'',
|
|
456
|
+
);
|
|
457
|
+
for (const source of NEUMERKEL_SOURCES) lines.push(`- [${source.key}](${source.url})`);
|
|
458
|
+
|
|
459
|
+
lines.push(
|
|
460
|
+
'',
|
|
461
|
+
'Exact fetched bytes, SHA-256 hashes, fetch timestamps, and HTTP validators remain under',
|
|
462
|
+
'Git-ignored `.cache/neumerkel/` for local audit/reproduction and are intentionally not committed.',
|
|
463
|
+
'Refresh this tracked report with `npm run conformance:update:neumerkel` and commit it whenever',
|
|
464
|
+
'the live upstream inventory changes.',
|
|
465
|
+
'',
|
|
466
|
+
);
|
|
467
|
+
return `${lines.join('\n')}\n`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
|
package/test/run-all.mjs
CHANGED
|
@@ -13,8 +13,14 @@ import { runWg17 } from './run-wg17.mjs';
|
|
|
13
13
|
import { runOpenRuleBenchChecks } from './run-openrulebench.mjs';
|
|
14
14
|
import { runArchitecture } from './run-architecture.mjs';
|
|
15
15
|
import { runCleanup } from './run-cleanup.mjs';
|
|
16
|
+
import { runNeumerkel } from './run-neumerkel.mjs';
|
|
17
|
+
import { runNeumerkelHarnessTests } from './run-neumerkel-tests.mjs';
|
|
18
|
+
|
|
19
|
+
const offline = process.argv.includes('--offline');
|
|
16
20
|
|
|
17
21
|
await runStandalone(async (reporter) => {
|
|
22
|
+
runNeumerkelHarnessTests(reporter);
|
|
23
|
+
if (!offline) await runNeumerkel(reporter);
|
|
18
24
|
runConformance(reporter);
|
|
19
25
|
runIsoStrict(reporter);
|
|
20
26
|
runWg17(reporter);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { runStandalone } from './test-style.mjs';
|
|
4
|
+
import { runNeumerkel } from './run-neumerkel.mjs';
|
|
5
|
+
import { runConformance } from './run-conformance.mjs';
|
|
6
|
+
import { runIsoStrict } from './run-iso-strict.mjs';
|
|
7
|
+
import { runWg17 } from './run-wg17.mjs';
|
|
8
|
+
|
|
9
|
+
const offline = process.argv.includes('--offline');
|
|
10
|
+
|
|
11
|
+
await runStandalone(async (reporter) => {
|
|
12
|
+
if (!offline) await runNeumerkel(reporter);
|
|
13
|
+
runConformance(reporter);
|
|
14
|
+
runIsoStrict(reporter);
|
|
15
|
+
runWg17(reporter);
|
|
16
|
+
});
|
|
@@ -78,11 +78,23 @@ export function formatConformanceReport(report = buildConformanceReport()) {
|
|
|
78
78
|
'conformance corpus under `test/conformance/`. The executable result is measured',
|
|
79
79
|
'when this report is generated; it is not inferred from fixture counts.',
|
|
80
80
|
'',
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
lines.push(
|
|
84
|
+
'## Latest Neumerkel evidence',
|
|
85
|
+
'',
|
|
86
|
+
'See the tracked [latest Neumerkel conformity report](test/conformance/NEUMERKEL-LATEST.md).',
|
|
87
|
+
'The live release gate fetches all seven TU Wien sources again and verifies that tracked',
|
|
88
|
+
'report still matches the discovered upstream inventory.',
|
|
89
|
+
'',
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
lines.push(
|
|
81
93
|
'## Executable conformance status',
|
|
82
94
|
'',
|
|
83
95
|
'| Gate | Passed | Total | Status |',
|
|
84
96
|
'|---|---:|---:|---|',
|
|
85
|
-
|
|
97
|
+
);
|
|
86
98
|
|
|
87
99
|
for (const gate of report.executable) {
|
|
88
100
|
const status = gate.passed === gate.total ? 'pass' : 'fail';
|
package/test/run-conformance.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { goalsFromSource } from './goal-metadata.mjs';
|
|
|
11
11
|
import { listPrologFiles, withStandardModules } from './test-support.mjs';
|
|
12
12
|
|
|
13
13
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
|
|
14
|
-
const filterArg = process.argv[2] ?? null;
|
|
14
|
+
const filterArg = process.argv[2]?.startsWith('--') ? null : process.argv[2] ?? null;
|
|
15
15
|
|
|
16
16
|
export function runConformance(reporter = new TestReporter(), requestedFilter = null) {
|
|
17
17
|
const filter = requestedFilter ?? filterArg;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { NEUMERKEL_SOURCES, formatNeumerkelMarkdown, materializeSyntaxCases, parseCleanupCases, parseDifCases } from './neumerkel.mjs';
|
|
3
|
+
import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
|
|
4
|
+
|
|
5
|
+
function syntaxHtml(count = 101) {
|
|
6
|
+
const rows = [];
|
|
7
|
+
for (let id = 1; id <= count; id++) {
|
|
8
|
+
const query = id === 2 ? '/**/ writeq(a).' : id === 1 ? 'op(9,fy,x).' : 'true.';
|
|
9
|
+
const expected = id === 2 ? 'a' : 'succeeds';
|
|
10
|
+
rows.push(`<tr><td>${id}<td>${query}<td>${expected}`);
|
|
11
|
+
}
|
|
12
|
+
return `<table>${rows.join('')}</table>`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function runNeumerkelHarnessTests(reporter = new TestReporter()) {
|
|
16
|
+
reporter.section('Neumerkel harness');
|
|
17
|
+
|
|
18
|
+
reporter.test('live manifest tracks exactly seven upstream suite sources', () => {
|
|
19
|
+
if (NEUMERKEL_SOURCES.length !== 7) throw new Error(`expected 7 sources, got ${NEUMERKEL_SOURCES.length}`);
|
|
20
|
+
const keys = new Set(NEUMERKEL_SOURCES.map(({ key }) => key));
|
|
21
|
+
if (keys.size !== 7) throw new Error('Neumerkel source keys are not unique');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
reporter.test('syntax inventory is discovered dynamically and reconstructs /**/ setup', () => {
|
|
25
|
+
const cases = materializeSyntaxCases(syntaxHtml());
|
|
26
|
+
if (cases.length !== 101) throw new Error(`expected 101 cases, got ${cases.length}`);
|
|
27
|
+
if (!cases[1].input.includes('op(9,fy,x)')) throw new Error('/**/ setup was not reconstructed');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
reporter.test('dif table discovery uses upstream row ids and answer descriptions', () => {
|
|
31
|
+
const cases = parseDifCases('<table><tr><td>1<td>?- dif(1,2).<td>true<tr><td>2<td>?- dif(X,X).<td>false</table>');
|
|
32
|
+
if (cases.length !== 2 || cases[0].expected !== 'succeeds' || cases[1].expected !== 'fails') {
|
|
33
|
+
throw new Error(`unexpected dif parse ${JSON.stringify(cases)}`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
reporter.test('tracked Markdown report records dynamic counts without volatile metadata', () => {
|
|
38
|
+
const summary = {
|
|
39
|
+
syntax: { passed: 366, total: 366 },
|
|
40
|
+
number_chars: { passed: 78, total: 78 },
|
|
41
|
+
variable_names: { passed: 75, total: 75 },
|
|
42
|
+
dif: { passed: 26, total: 26 },
|
|
43
|
+
length: { passed: 37, total: 37 },
|
|
44
|
+
phrase: { passed: 58, total: 58 },
|
|
45
|
+
cleanup: { passed: 25, total: 25 },
|
|
46
|
+
};
|
|
47
|
+
const text = formatNeumerkelMarkdown({
|
|
48
|
+
summary,
|
|
49
|
+
manifest: { fetchedAt: '2026-09-03T12:00:00.000Z', sources: [{ etag: '"volatile-tag"' }] },
|
|
50
|
+
});
|
|
51
|
+
if (!text.includes('**665/665**')) throw new Error('Markdown total is not derived from suite counts');
|
|
52
|
+
if (!text.includes('| setup_call_cleanup/3 | 25 | 25 |')) throw new Error('cleanup row missing');
|
|
53
|
+
if (!text.includes('https://www.complang.tuwien.ac.at/ulrich/iso-prolog/conformity_testing')) throw new Error('upstream source link missing');
|
|
54
|
+
if (text.includes('2026-09-03T12:00:00.000Z') || text.includes('volatile-tag')) {
|
|
55
|
+
throw new Error('tracked Markdown must exclude volatile fetch metadata');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
reporter.test('cleanup discovery follows the reference one-line example protocol', () => {
|
|
60
|
+
const html = `<pre>
|
|
61
|
+
setup_call_cleanup(fail,_,_).
|
|
62
|
+
Fails.
|
|
63
|
+
setup_call_cleanup(open(f,read,S),true,close(S)).
|
|
64
|
+
Succeeds.
|
|
65
|
+
catch(setup_call_cleanup(true,throw(x),true),E,true).
|
|
66
|
+
Succeeds.
|
|
67
|
+
catch(
|
|
68
|
+
setup_call_cleanup(true,throw(x),true),
|
|
69
|
+
E,true).
|
|
70
|
+
Succeeds.
|
|
71
|
+
</pre>`;
|
|
72
|
+
const cases = parseCleanupCases(html);
|
|
73
|
+
if (cases.length !== 2) throw new Error(`expected 2 executable one-line non-file cases, got ${cases.length}`);
|
|
74
|
+
if (cases[0].expected !== 'fails' || cases[1].expected !== 'succeeds') throw new Error('cleanup classification mismatch');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
reporter.sectionTotal('Neumerkel harness');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (isMainModule(import.meta.url)) await runStandalone(runNeumerkelHarnessTests);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { executeNeumerkel } from './neumerkel.mjs';
|
|
6
|
+
import { isMainModule, nowMs, runStandalone } from './test-style.mjs';
|
|
7
|
+
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const options = {
|
|
10
|
+
mode: 'live',
|
|
11
|
+
sourceDir: process.env.EYEPROLOG_NEUMERKEL_SOURCE_DIR ?? null,
|
|
12
|
+
updateReport: false,
|
|
13
|
+
verifyReport: true,
|
|
14
|
+
};
|
|
15
|
+
for (let index = 0; index < argv.length; index++) {
|
|
16
|
+
const arg = argv[index];
|
|
17
|
+
if (arg === '--cached') options.mode = 'cached';
|
|
18
|
+
else if (arg === '--update-report') options.updateReport = true;
|
|
19
|
+
else if (arg === '--no-verify-report') options.verifyReport = false;
|
|
20
|
+
else if (arg === '--source-dir') {
|
|
21
|
+
if (argv[index + 1] == null) throw new Error('--source-dir requires a directory');
|
|
22
|
+
options.sourceDir = path.resolve(argv[++index]);
|
|
23
|
+
} else if (arg === '--help' || arg === '-h') options.help = true;
|
|
24
|
+
else throw new Error(`unknown option ${arg}`);
|
|
25
|
+
}
|
|
26
|
+
return options;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function printHelp() {
|
|
30
|
+
process.stdout.write(
|
|
31
|
+
'Usage: npm run test:neumerkel -- [--cached] [--source-dir DIR] [--update-report] [--no-verify-report]\n\n' +
|
|
32
|
+
'Default: fetch all seven current Neumerkel conformity sources live, run the\n' +
|
|
33
|
+
'discovered cases, and verify test/conformance/NEUMERKEL-LATEST.md matches\n' +
|
|
34
|
+
'the successful upstream result. --update-report refreshes that tracked file.\n' +
|
|
35
|
+
'--cached is for offline reproduction only; it is never the release gate.\n',
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function runNeumerkel(reporter, options = {}) {
|
|
40
|
+
const effective = { verifyReport: true, updateReport: false, ...options };
|
|
41
|
+
if (effective.sourceDir == null && process.env.EYEPROLOG_NEUMERKEL_SOURCE_DIR) {
|
|
42
|
+
effective.sourceDir = path.resolve(process.env.EYEPROLOG_NEUMERKEL_SOURCE_DIR);
|
|
43
|
+
}
|
|
44
|
+
const result = await executeNeumerkel({ reporter: quietNeumerkelReporter(reporter), ...effective });
|
|
45
|
+
const relativeReportPath = path.relative(process.cwd(), result.reportPath);
|
|
46
|
+
|
|
47
|
+
if (effective.updateReport) {
|
|
48
|
+
fs.mkdirSync(path.dirname(result.reportPath), { recursive: true });
|
|
49
|
+
fs.writeFileSync(result.reportPath, result.reportText);
|
|
50
|
+
reporter.stdout.write(`Updated Neumerkel report: ${relativeReportPath}\n`);
|
|
51
|
+
} else if (effective.verifyReport !== false) {
|
|
52
|
+
const committed = fs.existsSync(result.reportPath) ? fs.readFileSync(result.reportPath, 'utf8') : null;
|
|
53
|
+
if (committed !== result.reportText) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`tracked Neumerkel report is stale: ${relativeReportPath}\n` +
|
|
56
|
+
'Run npm run conformance:update:neumerkel and commit the updated report.',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
reporter.stdout.write(`Neumerkel report: ${relativeReportPath}\n`);
|
|
60
|
+
} else {
|
|
61
|
+
reporter.stdout.write(`Neumerkel report verification skipped (${relativeReportPath})\n`);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function quietNeumerkelReporter(reporter) {
|
|
67
|
+
return {
|
|
68
|
+
section(name) {
|
|
69
|
+
reporter.section(name);
|
|
70
|
+
},
|
|
71
|
+
sectionTotal(label, elapsedMs = null) {
|
|
72
|
+
reporter.sectionTotal(label, elapsedMs);
|
|
73
|
+
},
|
|
74
|
+
test(name, run) {
|
|
75
|
+
reporter.total++;
|
|
76
|
+
const nr = String(reporter.total).padStart(3, '0');
|
|
77
|
+
const startedAt = nowMs();
|
|
78
|
+
try {
|
|
79
|
+
run();
|
|
80
|
+
reporter.ok++;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const ms = nowMs() - startedAt;
|
|
83
|
+
reporter.stderr.write(`FAIL ${nr} ${name} (${ms} ms)\n`);
|
|
84
|
+
reporter.stderr.write(`${error?.stack ?? String(error)}\n`);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isMainModule(import.meta.url)) {
|
|
92
|
+
const options = parseArgs(process.argv.slice(2));
|
|
93
|
+
if (options.help) printHelp();
|
|
94
|
+
else await runStandalone((reporter) => runNeumerkel(reporter, options));
|
|
95
|
+
}
|
package/test/run-regression.mjs
CHANGED
|
@@ -5363,7 +5363,7 @@ ${profile}`;
|
|
|
5363
5363
|
'| Clause 7 semantic requirements have explicit dispositions | covered |',
|
|
5364
5364
|
'| Clause 8 built-in modes/errors have explicit dispositions | covered |',
|
|
5365
5365
|
'| Clause 9 evaluable-functor requirements have explicit dispositions | covered |',
|
|
5366
|
-
'|
|
|
5366
|
+
'| Latest Neumerkel conformity is a live release gate | covered |',
|
|
5367
5367
|
'| No unexplained deviation remains in the release-facing ledger | covered |',
|
|
5368
5368
|
]) assertIncludes(exit, item, item);
|
|
5369
5369
|
assertNotIncludes(exit, '| audit |', 'no release-facing audit rows remain');
|
|
@@ -5468,7 +5468,9 @@ ${profile}`;
|
|
|
5468
5468
|
run: () => {
|
|
5469
5469
|
const reportFile = path.join(packageRoot, 'conformance-report.md');
|
|
5470
5470
|
assertEqual(fs.existsSync(reportFile), true, 'conformance-report.md exists');
|
|
5471
|
-
|
|
5471
|
+
const actual = fs.readFileSync(reportFile, 'utf8');
|
|
5472
|
+
const expected = formatConformanceReport(buildConformanceReport());
|
|
5473
|
+
assertEqual(actual, expected, 'conformance-report.md');
|
|
5472
5474
|
},
|
|
5473
5475
|
},
|
|
5474
5476
|
{
|
|
@@ -8557,7 +8559,10 @@ function documentedConformanceMetricIssues() {
|
|
|
8557
8559
|
...[...text.matchAll(/\b(\d+)-case[^\n|]*WG17/g)].map((match) => Number(match[1])),
|
|
8558
8560
|
...[...text.matchAll(/WG17[^\n|]*?\b(\d+) executable/g)].map((match) => Number(match[1])),
|
|
8559
8561
|
];
|
|
8560
|
-
if (claims.length === 0)
|
|
8562
|
+
if (claims.length === 0) {
|
|
8563
|
+
const dynamicPolicy = /live Neumerkel|current upstream inventory|discovered dynamically/i.test(text);
|
|
8564
|
+
if (!dynamicPolicy) issues.push(`${relative}: WG17 total or dynamic-upstream policy not found`);
|
|
8565
|
+
}
|
|
8561
8566
|
for (const claim of claims) {
|
|
8562
8567
|
if (claim !== wg17) issues.push(`${relative}: WG17 count ${claim} != ${wg17}`);
|
|
8563
8568
|
}
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -10228,7 +10228,7 @@ syntax. Separate corpora cover expected errors, warnings, and proofs:
|
|
|
10228
10228
|
|
|
10229
10229
|
```sh
|
|
10230
10230
|
npm run test:conformance
|
|
10231
|
-
npm run test:iso
|
|
10231
|
+
npm run test:iso
|
|
10232
10232
|
npm run test:wg17
|
|
10233
10233
|
node test/run-conformance-report.mjs
|
|
10234
10234
|
```
|
|
@@ -134,7 +134,7 @@ ${untracedIds.length === 0 ? 'None.' : `${formatRanges(untracedIds)}.`}
|
|
|
134
134
|
|
|
135
135
|
## Maintenance
|
|
136
136
|
|
|
137
|
-
1. Run \`npm run
|
|
137
|
+
1. Run \`npm run conformance:update\` to reconcile the dated fixture with upstream.
|
|
138
138
|
2. Review every new or changed ISO expectation before adding its expected outcome.
|
|
139
139
|
3. Run \`npm run test:wg17\` and keep this generated status page synchronized.
|
|
140
140
|
`;
|
|
@@ -158,7 +158,7 @@ if (process.argv[1] != null && path.resolve(process.argv[1]) === fileURLToPath(i
|
|
|
158
158
|
if (process.argv.includes('--check')) {
|
|
159
159
|
const current = fs.readFileSync(statusPath, 'utf8');
|
|
160
160
|
if (current !== rendered) {
|
|
161
|
-
process.stderr.write('WG17 syntax status is stale; run npm run
|
|
161
|
+
process.stderr.write('WG17 syntax status is stale; run npm run conformance:update and update the file.\n');
|
|
162
162
|
process.exitCode = 1;
|
|
163
163
|
}
|
|
164
164
|
} else {
|
package/tools/upgrade-wg17.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Refresh the vendored WG17 conformity fixtures from their public upstream
|
|
3
|
-
// tables.
|
|
3
|
+
// tables. The vendored snapshot remains an offline deterministic regression layer; the release gate also runs live upstream suites.
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
@@ -381,7 +381,7 @@ function parseArgs(argv) {
|
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
function printHelp() {
|
|
384
|
-
process.stdout.write(`Usage: npm run
|
|
384
|
+
process.stdout.write(`Usage: npm run conformance:update -- [--check] [--source URL_OR_FILE]\n\n` +
|
|
385
385
|
`Refreshes the vendored WG17 conformity tests from the TU Wien table.\n` +
|
|
386
386
|
`New or changed rows are executable immediately against the upstream\n` +
|
|
387
387
|
`Codex expectation; existing reviewed exact outcomes remain pinned only as additional regression checks.\n`);
|
|
@@ -401,7 +401,7 @@ export async function upgradeWg17({ check = false, source = syntaxSource } = {})
|
|
|
401
401
|
|
|
402
402
|
if (check) {
|
|
403
403
|
if (semanticChanges > 0) {
|
|
404
|
-
process.stderr.write('WG17 snapshot is stale; run npm run
|
|
404
|
+
process.stderr.write('WG17 snapshot is stale; run npm run conformance:update.\n');
|
|
405
405
|
process.exitCode = 1;
|
|
406
406
|
return { changed: true, ...reconciliation };
|
|
407
407
|
}
|