eyeprolog 1.2.30 → 1.2.32

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 CHANGED
@@ -105,8 +105,9 @@ modules, DCGs, quads, libraries, proofs, and the other documented extensions.
105
105
  The auditable processor-requirement checklist lives in
106
106
  [`test/conformance/ISO-COMPLIANCE.md`](test/conformance/ISO-COMPLIANCE.md).
107
107
  The separate [WG17 syntax ledger](test/conformance/WG17-SYNTAX-STATUS.md)
108
- records executable dispositions for all 366 active upstream syntax cases
109
- (100% traced) and runs as part of `npm test`.
108
+ records executable dispositions for the vendored active upstream WG17 syntax
109
+ cases and runs as part of `npm test`. Reviewed cases can pin exact outcomes;
110
+ newly upgraded cases run directly against the upstream Codex expectation.
110
111
  EyeProlog does not yet claim independent certification or closure of every
111
112
  normative Part 1 requirement.
112
113
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.30",
6
+ "version": "1.2.32",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -50,10 +50,12 @@
50
50
  "test:eyeprolog": "node test/run-all.mjs",
51
51
  "test:conformance": "node test/run-conformance.mjs",
52
52
  "test:iso-strict": "node test/run-iso-strict.mjs",
53
- "test:wg17-syntax": "node test/run-wg17-syntax.mjs",
53
+ "test:wg17": "node test/run-wg17.mjs",
54
54
  "test:examples": "node test/run-examples.mjs",
55
55
  "test:regression": "node test/run-regression.mjs",
56
56
  "test:playground": "node test/run-playground.mjs",
57
+ "wg17:upgrade": "node tools/upgrade-wg17.mjs",
58
+ "report:wg17": "node tools/report-wg17-syntax-coverage.mjs",
57
59
  "report:wg17-syntax": "node tools/report-wg17-syntax-coverage.mjs",
58
60
  "preversion": "npm test && node test/run-conformance-report.mjs conformance-report.md",
59
61
  "postversion": "git push origin HEAD --follow-tags"
@@ -18,7 +18,7 @@ error-ordering alternative to an individual executable assertion.
18
18
 
19
19
  | Requirement | Status | EyeProlog evidence / remaining work |
20
20
  | --- | --- | --- |
21
- | 5.1(a) prepare conforming Prolog text | audit | Clause 6 parser/tokenizer coverage, directive coverage, syntax-error corpus, and the [complete 366-case WG17 syntax matrix](WG17-SYNTAX-STATUS.md). Wider shall-by-shall text-processing audit remains open. |
21
+ | 5.1(a) prepare conforming Prolog text | audit | Clause 6 parser/tokenizer coverage, directive coverage, syntax-error corpus, and the [complete vendored WG17 syntax matrix](WG17-SYNTAX-STATUS.md). Wider shall-by-shall text-processing audit remains open. |
22
22
  | 5.1(b) execute conforming Prolog goals | audit | Clause 7-9 conformance corpus plus regression/API/example gates. A normative goal-semantics ledger is still being expanded. |
23
23
  | 5.1(c) reject nonconforming text/read-terms | audit | Dedicated syntax-error cases and strict-core extension rejection. Exhaustive lexical rejection coverage remains open. |
24
24
  | 5.1(d) document permitted variations | audit | Major implementation-defined choices are documented in *The Art of EyeProlog*. Every occurrence of “implementation defined/dependent/specific” in Part 1 still needs a final documentation cross-check. |
@@ -30,7 +30,7 @@ error-ordering alternative to an individual executable assertion.
30
30
 
31
31
  | Standard area | Status | Current evidence |
32
32
  | --- | --- | --- |
33
- | Clause 6 — tokens, terms, lists, operators, quoted text | audit | Complete 366-case WG17 syntax matrix, `lexical_and_curly_terms`, `scryer_lexical_terms`, operator suites, syntax-error cases, quoted-layout/escape error cases, and writer/read-back regressions. |
33
+ | Clause 6 — tokens, terms, lists, operators, quoted text | audit | Complete vendored WG17 syntax matrix, `lexical_and_curly_terms`, `scryer_lexical_terms`, operator suites, syntax-error cases, quoted-layout/escape error cases, and writer/read-back regressions. |
34
34
  | 7.1-7.3 — term types, term order, unification | audit | Standard-order, identity, finite-tree and occurs-check suites, Corrigendum 2 term predicates. |
35
35
  | 7.4 — Prolog text and directives | audit | All Part 1 directive indicators are parsed; include/ensure-loaded/operator/flag/character-conversion behavior has executable coverage. Cross-text `multifile/1` and ordering constraints require explicit shall-by-shall audit. |
36
36
  | 7.5-7.6 — database and term/clause conversion | audit | Dynamic database and logical-update-view suites. Strict mode restores Part 1 private-static/public-dynamic `clause/2` access. Public/private and multi-text requirements still need complete mapping. |
@@ -75,7 +75,7 @@ A release intended to advance ISO conformance must pass all of:
75
75
  npm test
76
76
  npm run test:iso-strict
77
77
  npm run test:conformance
78
- npm run test:wg17-syntax
78
+ npm run test:wg17
79
79
  ```
80
80
 
81
81
  The unified `npm test` gate includes the strict-core suite. Expected conformance
@@ -10,7 +10,7 @@ and proof output test the behavior of the JavaScript implementation.
10
10
  [ISO-MATRIX.md](ISO-MATRIX.md) maps Part 1 normative clause families, all three
11
11
  corrigenda, Part 2 modules, and Part 3 definite clause grammars to representative
12
12
  executable cases. [WG17-SYNTAX-STATUS.md](WG17-SYNTAX-STATUS.md) records the
13
- complete one-to-one trace for all 366 active upstream syntax cases.
13
+ complete one-to-one trace for the vendored active upstream WG17 syntax cases.
14
14
 
15
15
  “Conformance” here means conformance to EyeProlog's documented ISO compatibility
16
16
  profile and implementation extensions. The default registry covers the exact
@@ -69,12 +69,28 @@ Run the Part 1 + Corrigenda strict-core processor gate:
69
69
  npm run test:iso-strict
70
70
  ```
71
71
 
72
- Run the complete WG17 syntax matrix independently:
72
+ Run all vendored WG17 conformity matrices independently:
73
73
 
74
74
  ```sh
75
- npm run test:wg17-syntax
75
+ npm run test:wg17
76
76
  ```
77
77
 
78
+ Refresh the WG17 snapshot from the TU Wien conformity tables before a release
79
+ or whenever upstream changes:
80
+
81
+ ```sh
82
+ npm run wg17:upgrade
83
+ npm run test:wg17
84
+ ```
85
+
86
+ `wg17:upgrade` reconciles the upstream inventory by identifier: unchanged cases
87
+ keep their reviewed exact outcomes, removed cases disappear, and new or
88
+ semantically changed cases are executed directly against the upstream Codex
89
+ expectation. This means `npm run test:wg17` can test an upgraded snapshot
90
+ immediately without first copying EyeProlog's current behaviour into an
91
+ expected result. Normal `npm test` remains offline and uses only the committed
92
+ snapshot.
93
+
78
94
  Summarize conformance coverage by category:
79
95
 
80
96
  ```sh
@@ -1,12 +1,12 @@
1
1
  # WG17 syntax traceability status
2
2
 
3
3
  Source: [Conformity Testing I: Syntax](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/conformity_testing)
4
- Upstream inventory checked: 2026-08-15
4
+ Upstream inventory checked: 2026-08-16
5
5
 
6
- This ledger counts an upstream case only when its WG17 identifier, query,
7
- expected ISO disposition, and observed EyeProlog outcome are stored in the
8
- offline executable matrix. Semantically similar parser tests are not inferred
9
- as coverage.
6
+ This ledger counts an upstream case when its WG17 identifier, query, and
7
+ expected ISO disposition are stored in the offline executable matrix. Existing
8
+ cases may pin an exact reviewed EyeProlog outcome; newly upgraded cases are
9
+ executed directly against the upstream Codex expectation.
10
10
 
11
11
  ## Current standing
12
12
 
@@ -18,15 +18,15 @@ as coverage.
18
18
  | Deleted upstream identifiers | #20, #273 |
19
19
 
20
20
  The matrix runs in strict ISO stream-reader mode as part of `npm test`. The
21
- three upstream `waits` cases are checked through EyeProlog's interactive input
22
- hook; the other 363 cases are checked for their exact stored
23
- success output, bindings, failure, or ISO error category.
21
+ 3 upstream `waits` cases are checked through EyeProlog's interactive input
22
+ hook. 1 case uses the upstream Codex expectation directly; the remaining
23
+ 365 cases retain exact stored outcomes for stronger regression checking.
24
24
 
25
25
  ## Traceable evidence
26
26
 
27
27
  | Executable evidence | Referenced IDs | WG17 cases |
28
28
  | --- | ---: | --- |
29
- | [complete offline executable matrix](../run-wg17-syntax.mjs) | 366 | #1–#19, #21–#272, #274–#368 |
29
+ | [complete offline executable matrix](../run-wg17.mjs) | 366 | #1–#19, #21–#272, #274–#368 |
30
30
 
31
31
  The evidence groups overlap. Their union is **366** active cases:
32
32
  #1–#19, #21–#272, #274–#368.
@@ -37,6 +37,6 @@ None.
37
37
 
38
38
  ## Maintenance
39
39
 
40
- 1. Refresh the dated fixture when the upstream table changes.
41
- 2. Review any changed ISO expectation before updating an observed snapshot.
42
- 3. Keep this generated status page synchronized in the release gate.
40
+ 1. Run `npm run wg17:upgrade` to reconcile the dated fixture with upstream.
41
+ 2. Review every new or changed ISO expectation before adding its expected outcome.
42
+ 3. Run `npm run test:wg17` and keep this generated status page synchronized.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "source": "https://www.complang.tuwien.ac.at/ulrich/iso-prolog/conformity_testing",
3
- "checkedOn": "2026-08-15",
3
+ "checkedOn": "2026-08-16",
4
4
  "protocol": "Each query is read and executed in strict ISO mode; /**/ rows reuse the preceding setup.",
5
5
  "cases": [
6
6
  {
@@ -5551,14 +5551,13 @@
5551
5551
  },
5552
5552
  {
5553
5553
  "id": 368,
5554
- "query": "Finis ().",
5555
- "input": "Finis ().",
5554
+ "query": "writeq(?).",
5555
+ "input": "writeq(?).",
5556
5556
  "readCount": 1,
5557
- "expected": "syntax err.",
5558
- "outcome": {
5559
- "type": "error",
5560
- "formal": "syntax_error(read_term)"
5561
- }
5557
+ "expected": "?",
5558
+ "assertion": "upstream"
5562
5559
  }
5563
- ]
5560
+ ],
5561
+ "sourceRevision": "1.300",
5562
+ "sourceSha256": "d7ca05e90cfafe786d5edca4783e691a356a35508c50b62f5a99bd9057e2cd05"
5564
5563
  }
@@ -1,18 +1,21 @@
1
1
  {
2
2
  "source": "https://www.complang.tuwien.ac.at/ulrich/iso-prolog/conformity_testing",
3
- "checkedOn": "2026-08-15",
3
+ "checkedOn": "2026-08-16",
4
4
  "upstream": {
5
5
  "firstId": 1,
6
6
  "lastId": 368,
7
- "deletedIds": [20, 273],
7
+ "deletedIds": [
8
+ 20,
9
+ 273
10
+ ],
8
11
  "activeCases": 366
9
12
  },
10
13
  "evidence": [
11
14
  {
12
15
  "name": "complete offline executable matrix",
13
16
  "path": "test/conformance/wg17-syntax-cases.json",
14
- "link": "../run-wg17-syntax.mjs",
15
- "ids": "all-active"
17
+ "link": "../run-wg17.mjs",
18
+ "ids": "all-executable"
16
19
  }
17
20
  ]
18
21
  }
package/test/run-all.mjs CHANGED
@@ -9,14 +9,14 @@ import { runIsoStrict } from './run-iso-strict.mjs';
9
9
  import { runPlayground } from './run-playground.mjs';
10
10
  import { runExamples } from './run-examples.mjs';
11
11
  import { runBookExamples } from './run-book-examples.mjs';
12
- import { runWg17Syntax } from './run-wg17-syntax.mjs';
12
+ import { runWg17 } from './run-wg17.mjs';
13
13
 
14
14
  const reporter = new TestReporter();
15
15
 
16
16
  try {
17
17
  runConformance(reporter);
18
18
  runIsoStrict(reporter);
19
- runWg17Syntax(reporter);
19
+ runWg17(reporter);
20
20
  runRegression(reporter);
21
21
  await runPlayground(reporter);
22
22
  runExamples(reporter);
@@ -46,6 +46,8 @@ import { buildConformanceReport, formatConformanceReport } from './run-conforman
46
46
  import { proofExamples } from './run-examples.mjs';
47
47
  import { goalsFromSource } from './goal-metadata.mjs';
48
48
  import { renderWg17SyntaxStatus } from '../tools/report-wg17-syntax-coverage.mjs';
49
+ import { parseWg17SyntaxTable } from '../tools/upgrade-wg17.mjs';
50
+ import { matchesUpstreamExpectation } from './run-wg17.mjs';
49
51
 
50
52
  const testRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
51
53
  const packageRoot = path.resolve(testRoot, '..');
@@ -2166,6 +2168,49 @@ function documentationSyncCases() {
2166
2168
  assertEqual(fs.readFileSync(filename, 'utf8'), renderWg17SyntaxStatus(), 'WG17 syntax status');
2167
2169
  },
2168
2170
  },
2171
+ {
2172
+ name: 'WG17 upgrader accepts omitted HTML table end tags',
2173
+ run: () => {
2174
+ // HTML permits </td> and </tr> to be omitted. TU Wien uses this
2175
+ // compact form, so the upgrader must not depend on explicit closes.
2176
+ const rows = Array.from({ length: 120 }, (_, index) =>
2177
+ `<tr><td>${index + 1}<td><code>write(${index + 1}).</code><td>ok`).join('\n');
2178
+ const html = `<table><tr><th>#<th>Query<th>Codex${rows}</table>`;
2179
+ const parsed = parseWg17SyntaxTable(html);
2180
+ assertEqual(parsed.length, 120, 'parsed row count');
2181
+ assertEqual(parsed[0].id, 1, 'first id');
2182
+ assertEqual(parsed[0].query, 'write(1).', 'first query');
2183
+ assertEqual(parsed.at(-1).id, 120, 'last id');
2184
+ },
2185
+ },
2186
+ {
2187
+ name: 'WG17 upgrader normalizes presentation non-breaking spaces',
2188
+ run: () => {
2189
+ const rows = Array.from({ length: 120 }, (_, index) =>
2190
+ `<tr><td>${index + 1}<td>set_prolog_flag(&nbsp;double_quotes,chars).<td>succeeds`).join('\n');
2191
+ const html = `<table><tr><th>#<th>Query<th>Codex${rows}</table>`;
2192
+ const parsed = parseWg17SyntaxTable(html);
2193
+ assertEqual(parsed[0].query, 'set_prolog_flag( double_quotes,chars).', 'normalized query');
2194
+ },
2195
+ },
2196
+ {
2197
+ name: 'WG17 direct upstream assertions are executable without local outcomes',
2198
+ run: () => {
2199
+ assertEqual(matchesUpstreamExpectation('succeeds', { type: 'success', stages: [] }), true, 'succeeds');
2200
+ assertEqual(matchesUpstreamExpectation('fails', { type: 'failure' }), true, 'fails');
2201
+ assertEqual(matchesUpstreamExpectation('waits', { type: 'waits' }), true, 'waits');
2202
+ assertEqual(
2203
+ matchesUpstreamExpectation('syntax err.', { type: 'error', formal: 'syntax_error(read_term)' }),
2204
+ true,
2205
+ 'syntax error',
2206
+ );
2207
+ assertEqual(
2208
+ matchesUpstreamExpectation("'a b'", { type: 'success', stages: [{ output: "'a b'", variables: '[]' }] }),
2209
+ true,
2210
+ 'observable output',
2211
+ );
2212
+ },
2213
+ },
2169
2214
  {
2170
2215
  name: 'book builtins match runtime registry',
2171
2216
  run: () => assertArrayEqual(bookBuiltinNames(), registeredBuiltinNames(), 'builtins'),
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ // Aggregate all vendored WG17 conformity suites behind one stable entry point.
3
+ // The current upstream conformity table is syntax-focused; additional WG17
4
+ // suites can be added here without changing npm/CI commands.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import {
9
+ Env, Program, Solver, parseGoalText, run,
10
+ } from '../src/index.js';
11
+ import { TestReporter, isMainModule } from './test-style.mjs';
12
+
13
+ const testRoot = path.dirname(fileURLToPath(import.meta.url));
14
+ const fixturePath = path.join(testRoot, 'conformance', 'wg17-syntax-cases.json');
15
+
16
+ function runnerStage(index, maximum) {
17
+ if (index > maximum) return `write('\\n<WG17-COMPLETE>')`;
18
+ return `read_term(G${index}, [variable_names(V${index})]), ` +
19
+ `(G${index} == end_of_file -> write('\\n<WG17-COMPLETE>') ; (` +
20
+ `write('\\n<WG17-BEGIN-${index}>'), call(G${index}), ` +
21
+ `write('<WG17-VARS>'), writeq(V${index}), write('<WG17-END>'), ` +
22
+ `${runnerStage(index + 1, maximum)}))`;
23
+ }
24
+
25
+ function capturedStages(stdout) {
26
+ const complete = stdout.indexOf('<WG17-COMPLETE>');
27
+ if (complete < 0) return null;
28
+ const captured = stdout.slice(0, complete);
29
+ return [...captured.matchAll(/<WG17-BEGIN-(\d+)>([\s\S]*?)<WG17-VARS>([\s\S]*?)<WG17-END>/g)]
30
+ .map((match) => ({ output: match[2], variables: match[3] }));
31
+ }
32
+
33
+ function executeFinite(item) {
34
+ try {
35
+ const result = run('', {
36
+ isoStrict: true,
37
+ goal: runnerStage(1, item.readCount ?? 16),
38
+ ioOptions: { input: `${item.input}\n` },
39
+ });
40
+ const stages = capturedStages(result.stdout);
41
+ return stages == null ? { type: 'failure' } : { type: 'success', stages };
42
+ } catch (error) {
43
+ return { type: 'error', formal: error?.formal ?? null };
44
+ }
45
+ }
46
+
47
+ function executeWait(item) {
48
+ const program = Program.parse('', { isoStrict: true });
49
+ const solver = new Solver(program, {
50
+ isoStrict: true,
51
+ ioOptions: { input: item.input },
52
+ });
53
+ const stream = solver.io.resolve('user_input');
54
+ let requests = 0;
55
+ stream.interactiveReadTerm = () => {
56
+ requests++;
57
+ return null;
58
+ };
59
+ const goal = parseGoalText('read_term(G, [])', {
60
+ isoStrict: true,
61
+ operatorDefinitions: [...program.operators.values()],
62
+ });
63
+ try {
64
+ [...solver.solve([goal], new Env(), 0)];
65
+ } catch (_) {
66
+ // Returning null from the hook models EOF after EyeProlog has asked the
67
+ // interactive source for the continuation that the upstream case awaits.
68
+ }
69
+ return requests === 1 ? { type: 'waits' } : { type: 'did_not_wait', requests };
70
+ }
71
+
72
+ function canonicalUpstreamExpected(expected) {
73
+ return String(expected)
74
+ .replace(/[²³°]/g, '')
75
+ .replace(/\u00a0/g, ' ')
76
+ .replace(/[ \t\n]+/g, ' ')
77
+ .trim();
78
+ }
79
+
80
+ function observableOutput(actual) {
81
+ if (actual.type !== 'success') return null;
82
+ return actual.stages.map(({ output }) => output).join('');
83
+ }
84
+
85
+ export function matchesUpstreamExpectation(expectedText, actual) {
86
+ const expected = canonicalUpstreamExpected(expectedText);
87
+
88
+ if (/^waits$/i.test(expected)) return actual.type === 'waits';
89
+ if (/^succeeds(?:\b|$)/i.test(expected)) return actual.type === 'success';
90
+ if (/^fails(?:\b|$)/i.test(expected)) return actual.type === 'failure';
91
+
92
+ if (/^syntax\s*err\.?$/i.test(expected)) {
93
+ return actual.type === 'error' && /^syntax_error\(/.test(actual.formal ?? '');
94
+ }
95
+ if (/^repr\.\s*err\.?$/i.test(expected)) {
96
+ return actual.type === 'error' && /^representation_error\(/.test(actual.formal ?? '');
97
+ }
98
+ if (/^syntax\/repr\.\s*err\.?$/i.test(expected)) {
99
+ return actual.type === 'error' &&
100
+ /^(?:syntax_error|representation_error)\(/.test(actual.formal ?? '');
101
+ }
102
+ if (/^syntax\s*err\.\/waits$/i.test(expected)) {
103
+ return actual.type === 'waits' ||
104
+ (actual.type === 'error' && /^syntax_error\(/.test(actual.formal ?? ''));
105
+ }
106
+
107
+ // Most new WG17 rows are observable write/read examples. For those, the
108
+ // Codex cell itself is the expected output, so no hand-written local
109
+ // outcome is needed before the case can be executed.
110
+ const output = observableOutput(actual);
111
+ if (output != null && canonicalUpstreamExpected(output) === expected) return true;
112
+
113
+ return false;
114
+ }
115
+
116
+ function usesWaitMatcher(expectedText) {
117
+ const expected = canonicalUpstreamExpected(expectedText);
118
+ return /^waits$/i.test(expected);
119
+ }
120
+
121
+ function compactTestText(value, maximum) {
122
+ const text = String(value ?? '')
123
+ .replace(/\r/g, '\\r')
124
+ .replace(/\n/g, '\\n')
125
+ .replace(/\t/g, '\\t')
126
+ .replace(/[ ]+/g, ' ')
127
+ .trim();
128
+ if (text.length <= maximum) return text;
129
+ return `${text.slice(0, maximum - 1)}…`;
130
+ }
131
+
132
+ export function wg17TestDescription(item) {
133
+ const query = compactTestText(item.query ?? item.input, 56);
134
+ const expected = compactTestText(item.expected, 28);
135
+ return `#${item.id} ${query} -> ${expected}`;
136
+ }
137
+
138
+ function assertOutcome(item) {
139
+ if (item.outcome != null) {
140
+ const actual = item.outcome.type === 'waits' ? executeWait(item) : executeFinite(item);
141
+ if (JSON.stringify(actual) !== JSON.stringify(item.outcome)) {
142
+ throw new Error(
143
+ `WG17 #${item.id} (${item.expected})\n` +
144
+ `expected ${JSON.stringify(item.outcome)}\n` +
145
+ `actual ${JSON.stringify(actual)}`,
146
+ );
147
+ }
148
+ return;
149
+ }
150
+
151
+ const actual = usesWaitMatcher(item.expected) ? executeWait(item) : executeFinite(item);
152
+ if (!matchesUpstreamExpectation(item.expected, actual)) {
153
+ throw new Error(
154
+ `WG17 #${item.id} (${item.expected})\n` +
155
+ `upstream expectation did not match\n` +
156
+ `actual ${JSON.stringify(actual)}`,
157
+ );
158
+ }
159
+ }
160
+
161
+ function readWg17SyntaxFixture() {
162
+ const fixture = JSON.parse(fs.readFileSync(fixturePath, 'utf8'));
163
+ if (!Array.isArray(fixture.cases) || fixture.cases.length === 0) {
164
+ throw new Error('WG17 syntax fixture has no cases');
165
+ }
166
+ const ids = new Set();
167
+ for (const item of fixture.cases) {
168
+ if (!Number.isInteger(item.id) || ids.has(item.id)) {
169
+ throw new Error(`invalid or duplicate WG17 syntax id #${item.id}`);
170
+ }
171
+ ids.add(item.id);
172
+ }
173
+ return fixture;
174
+ }
175
+
176
+ function runWg17Syntax(reporter = new TestReporter()) {
177
+ const fixture = readWg17SyntaxFixture();
178
+
179
+ reporter.section('WG17 syntax');
180
+ for (const item of fixture.cases) {
181
+ reporter.test(wg17TestDescription(item), () => assertOutcome(item));
182
+ }
183
+ reporter.sectionTotal('WG17 syntax');
184
+ }
185
+
186
+ const suites = [runWg17Syntax];
187
+
188
+ export function runWg17(reporter = new TestReporter()) {
189
+ for (const runSuite of suites) runSuite(reporter);
190
+ }
191
+
192
+ if (isMainModule(import.meta.url)) {
193
+ const reporter = new TestReporter();
194
+ try {
195
+ runWg17(reporter);
196
+ reporter.totalLine();
197
+ } catch (error) {
198
+ process.stderr.write(`${error?.stack ?? error}\n`);
199
+ process.exitCode = 1;
200
+ }
201
+ }
@@ -7060,7 +7060,9 @@ syntax. Separate corpora cover expected errors, warnings, and proofs:
7060
7060
  ```sh
7061
7061
  npm run test:conformance
7062
7062
  npm run test:iso-strict
7063
- npm run test:wg17-syntax
7063
+ npm run test:wg17
7064
+ # Refresh the vendored TU Wien WG17 inventory when upstream changes:
7065
+ npm run wg17:upgrade
7064
7066
  node test/run-conformance-report.mjs
7065
7067
  ```
7066
7068
 
@@ -7069,8 +7071,9 @@ Part 1 conformance audit. It distinguishes implemented/tested families from
7069
7071
  requirements whose normative `shall` clauses, option combinations, or error
7070
7072
  precedence still need one-by-one closure. `test/conformance/ISO-MATRIX.md`
7071
7073
  maps language families to representative executable cases.
7072
- `test/conformance/WG17-SYNTAX-STATUS.md` separately traces all 366 active
7073
- upstream syntax cases to exact strict-reader outcomes, with no untraced case.
7074
+ `test/conformance/WG17-SYNTAX-STATUS.md` separately traces the vendored active
7075
+ upstream syntax cases. Reviewed cases can pin exact strict-reader outcomes, while
7076
+ newly upgraded cases execute directly against the upstream Codex expectation.
7074
7077
 
7075
7078
  The complete suite must pass before release. The file-based conformance corpus
7076
7079
  contains 791 cases, including 386 focused ISO
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
9
9
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
10
  const manifestPath = path.join(packageRoot, 'test', 'conformance', 'wg17-syntax-coverage.json');
11
11
  const statusPath = path.join(packageRoot, 'test', 'conformance', 'WG17-SYNTAX-STATUS.md');
12
+ const fixturePath = path.join(packageRoot, 'test', 'conformance', 'wg17-syntax-cases.json');
12
13
 
13
14
  export function readWg17SyntaxCoverage() {
14
15
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
@@ -25,10 +26,12 @@ export function readWg17SyntaxCoverage() {
25
26
  const evidenceEntries = [];
26
27
  for (const evidence of manifest.evidence) {
27
28
  if (!evidence.name || !evidence.path || !evidence.link ||
28
- (!Array.isArray(evidence.ids) && evidence.ids !== 'all-active')) {
29
+ (!Array.isArray(evidence.ids) && !['all-active', 'all-reviewed', 'all-executable'].includes(evidence.ids))) {
29
30
  throw new Error('invalid WG17 evidence entry');
30
31
  }
31
- const evidenceIds = evidence.ids === 'all-active' ? activeIds : evidence.ids;
32
+ const evidenceIds = evidence.ids === 'all-active' ? activeIds :
33
+ ['all-reviewed', 'all-executable'].includes(evidence.ids)
34
+ ? executableWg17Ids(evidence.path, active) : evidence.ids;
32
35
  const evidenceFilename = path.join(packageRoot, evidence.path);
33
36
  if (!fs.existsSync(evidenceFilename)) throw new Error(`missing WG17 evidence file ${evidence.path}`);
34
37
  const referenced = referencedWg17Ids(fs.readFileSync(evidenceFilename, 'utf8'));
@@ -52,6 +55,16 @@ export function readWg17SyntaxCoverage() {
52
55
  };
53
56
  }
54
57
 
58
+
59
+ function executableWg17Ids(relativePath, active) {
60
+ const filename = path.join(packageRoot, relativePath);
61
+ if (path.resolve(filename) !== path.resolve(fixturePath)) return [];
62
+ const fixture = JSON.parse(fs.readFileSync(filename, 'utf8'));
63
+ return fixture.cases
64
+ .filter((item) => typeof item.expected === 'string' && item.expected.length > 0 && active.has(item.id))
65
+ .map(({ id }) => id);
66
+ }
67
+
55
68
  function referencedWg17Ids(source) {
56
69
  const ids = new Set();
57
70
  for (const match of source.matchAll(/#(\d+)(?:-(\d+))?/g)) {
@@ -65,6 +78,10 @@ function referencedWg17Ids(source) {
65
78
 
66
79
  export function renderWg17SyntaxStatus() {
67
80
  const { manifest, evidenceEntries, activeIds, coveredIds, untracedIds } = readWg17SyntaxCoverage();
81
+ const fixture = JSON.parse(fs.readFileSync(fixturePath, 'utf8'));
82
+ const waits = fixture.cases.filter((item) =>
83
+ item.outcome?.type === 'waits' || (item.outcome == null && /^waits$/i.test(item.expected ?? ''))).length;
84
+ const direct = fixture.cases.filter((item) => item.outcome == null).length;
68
85
  const percentage = (100 * coveredIds.length / activeIds.length).toFixed(1);
69
86
  const evidenceRows = evidenceEntries.map((evidence) =>
70
87
  `| [${evidence.name}](${evidence.link}) | ${evidence.ids.length} | ${formatRanges(evidence.ids)} |`);
@@ -74,10 +91,10 @@ export function renderWg17SyntaxStatus() {
74
91
  Source: [Conformity Testing I: Syntax](${manifest.source})
75
92
  Upstream inventory checked: ${manifest.checkedOn}
76
93
 
77
- This ledger counts an upstream case only when its WG17 identifier, query,
78
- expected ISO disposition, and observed EyeProlog outcome are stored in the
79
- offline executable matrix. Semantically similar parser tests are not inferred
80
- as coverage.
94
+ This ledger counts an upstream case when its WG17 identifier, query, and
95
+ expected ISO disposition are stored in the offline executable matrix. Existing
96
+ cases may pin an exact reviewed EyeProlog outcome; newly upgraded cases are
97
+ executed directly against the upstream Codex expectation.
81
98
 
82
99
  ## Current standing
83
100
 
@@ -89,9 +106,9 @@ as coverage.
89
106
  | Deleted upstream identifiers | ${formatRanges(manifest.upstream.deletedIds)} |
90
107
 
91
108
  The matrix runs in strict ISO stream-reader mode as part of \`npm test\`. The
92
- three upstream \`waits\` cases are checked through EyeProlog's interactive input
93
- hook; the other ${activeIds.length - 3} cases are checked for their exact stored
94
- success output, bindings, failure, or ISO error category.
109
+ ${waits} upstream \`waits\` case${waits === 1 ? '' : 's'} ${waits === 1 ? 'is' : 'are'} checked through EyeProlog's interactive input
110
+ hook. ${direct} case${direct === 1 ? '' : 's'} use${direct === 1 ? 's' : ''} the upstream Codex expectation directly; the remaining
111
+ ${coveredIds.length - direct} case${coveredIds.length - direct === 1 ? '' : 's'} retain exact stored outcomes for stronger regression checking.
95
112
 
96
113
  ## Traceable evidence
97
114
 
@@ -108,9 +125,9 @@ ${untracedIds.length === 0 ? 'None.' : `${formatRanges(untracedIds)}.`}
108
125
 
109
126
  ## Maintenance
110
127
 
111
- 1. Refresh the dated fixture when the upstream table changes.
112
- 2. Review any changed ISO expectation before updating an observed snapshot.
113
- 3. Keep this generated status page synchronized in the release gate.
128
+ 1. Run \`npm run wg17:upgrade\` to reconcile the dated fixture with upstream.
129
+ 2. Review every new or changed ISO expectation before adding its expected outcome.
130
+ 3. Run \`npm run test:wg17\` and keep this generated status page synchronized.
114
131
  `;
115
132
  }
116
133
 
@@ -132,7 +149,7 @@ if (process.argv[1] != null && path.resolve(process.argv[1]) === fileURLToPath(i
132
149
  if (process.argv.includes('--check')) {
133
150
  const current = fs.readFileSync(statusPath, 'utf8');
134
151
  if (current !== rendered) {
135
- process.stderr.write('WG17 syntax status is stale; run npm run report:wg17-syntax and update the file.\n');
152
+ process.stderr.write('WG17 syntax status is stale; run npm run report:wg17 and update the file.\n');
136
153
  process.exitCode = 1;
137
154
  }
138
155
  } else {
@@ -0,0 +1,440 @@
1
+ #!/usr/bin/env node
2
+ // Refresh the vendored WG17 conformity fixtures from their public upstream
3
+ // tables. Normal test runs remain fully offline and deterministic.
4
+ import crypto from 'node:crypto';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { fileURLToPath, pathToFileURL } from 'node:url';
8
+
9
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
+ const syntaxSource = 'https://www.complang.tuwien.ac.at/ulrich/iso-prolog/conformity_testing';
11
+ const syntaxFixturePath = path.join(packageRoot, 'test', 'conformance', 'wg17-syntax-cases.json');
12
+ const syntaxCoveragePath = path.join(packageRoot, 'test', 'conformance', 'wg17-syntax-coverage.json');
13
+ const syntaxStatusPath = path.join(packageRoot, 'test', 'conformance', 'WG17-SYNTAX-STATUS.md');
14
+
15
+ const namedEntities = new Map([
16
+ ['amp', '&'], ['lt', '<'], ['gt', '>'], ['quot', '"'], ['apos', "'"],
17
+ ['nbsp', '\u00a0'], ['ndash', '–'], ['mdash', '—'], ['minus', '−'],
18
+ ['hellip', '…'], ['middot', '·'], ['times', '×'], ['laquo', '«'], ['raquo', '»'],
19
+ ]);
20
+
21
+ export function decodeHtmlEntities(text) {
22
+ return text.replace(/&(#(?:x[0-9a-f]+|\d+)|[a-z][a-z0-9]+);/gi, (whole, entity) => {
23
+ if (entity[0] === '#') {
24
+ const hex = entity[1]?.toLowerCase() === 'x';
25
+ const value = Number.parseInt(entity.slice(hex ? 2 : 1), hex ? 16 : 10);
26
+ if (Number.isInteger(value) && value >= 0 && value <= 0x10ffff) return String.fromCodePoint(value);
27
+ return whole;
28
+ }
29
+ return namedEntities.get(entity.toLowerCase()) ?? whole;
30
+ });
31
+ }
32
+
33
+ function withoutPresentationMarkup(html) {
34
+ return html
35
+ .replace(/<!--[\s\S]*?-->/g, '')
36
+ .replace(/<sup\b[^>]*>[\s\S]*?<\/sup>/gi, '')
37
+ .replace(/<br\s*\/?>/gi, '\n')
38
+ .replace(/<\/(?:p|div|pre|li|blockquote)>/gi, '\n')
39
+ .replace(/<(?:p|div|pre|li|blockquote)\b[^>]*>/gi, '')
40
+ .replace(/<[^>]+>/g, '');
41
+ }
42
+
43
+ export function htmlCellText(html) {
44
+ return decodeHtmlEntities(withoutPresentationMarkup(html))
45
+ .replace(/\r\n?/g, '\n')
46
+ .replace(/\n{3,}/g, '\n\n')
47
+ .trim();
48
+ }
49
+
50
+ function deletedRow(rowHtml, idCellHtml) {
51
+ return /<(?:del|s|strike)\b/i.test(idCellHtml) ||
52
+ /class\s*=\s*["'][^"']*\b(?:deleted|obsolete|removed)\b/i.test(rowHtml);
53
+ }
54
+
55
+ function htmlTableRows(html) {
56
+ // TU Wien intentionally serves very small, old-style HTML. In valid HTML,
57
+ // </td> and </tr> are optional, and the conformity page currently relies
58
+ // on that. Do not require explicit closing tags here: split on start tags
59
+ // and let the next cell/row start imply the end of the previous one.
60
+ const rowStarts = [...html.matchAll(/<tr\b[^>]*>/gi)];
61
+ const rows = [];
62
+ for (let rowIndex = 0; rowIndex < rowStarts.length; rowIndex++) {
63
+ const rowStart = rowStarts[rowIndex];
64
+ const bodyStart = rowStart.index + rowStart[0].length;
65
+ const nextRow = rowStarts[rowIndex + 1]?.index ?? html.length;
66
+ const explicitEnd = html.slice(bodyStart, nextRow).search(/<\/tr\s*>/i);
67
+ const rowEnd = explicitEnd < 0 ? nextRow : bodyStart + explicitEnd;
68
+ const rowHtml = html.slice(rowStart.index, rowEnd);
69
+ const body = html.slice(bodyStart, rowEnd);
70
+ const cellStarts = [...body.matchAll(/<t[dh]\b[^>]*>/gi)];
71
+ const cells = [];
72
+ for (let cellIndex = 0; cellIndex < cellStarts.length; cellIndex++) {
73
+ const cellStart = cellStarts[cellIndex];
74
+ const cellBodyStart = cellStart.index + cellStart[0].length;
75
+ const cellEnd = cellStarts[cellIndex + 1]?.index ?? body.length;
76
+ cells.push(body.slice(cellBodyStart, cellEnd));
77
+ }
78
+ rows.push({ rowHtml, cells });
79
+ }
80
+ return rows;
81
+ }
82
+
83
+ export function parseWg17SyntaxTable(html) {
84
+ const cases = [];
85
+ const seen = new Set();
86
+ for (const { rowHtml, cells } of htmlTableRows(html)) {
87
+ if (cells.length < 3 || deletedRow(rowHtml, cells[0])) continue;
88
+
89
+ const idText = htmlCellText(cells[0]).replace(/^#\s*/, '');
90
+ const idMatch = idText.match(/^(\d+)$/);
91
+ if (idMatch == null) continue;
92
+ const id = Number(idMatch[1]);
93
+ if (seen.has(id)) throw new Error(`duplicate active WG17 syntax id #${id} in upstream table`);
94
+
95
+ // TU Wien uses non-breaking spaces for table presentation/indentation.
96
+ // They are not part of the Prolog source being specified, so normalize
97
+ // them to ordinary spaces before snapshotting/comparing rows.
98
+ const query = htmlCellText(cells[1]).replace(/\u00a0/g, ' ');
99
+ const expected = htmlCellText(cells[2]).replace(/\u00a0/g, ' ').replace(/[ \t\n]+/g, ' ').trim();
100
+ if (query.length === 0 || expected.length === 0) continue;
101
+ cases.push({ id, query, expected });
102
+ seen.add(id);
103
+ }
104
+ if (cases.length < 100) {
105
+ const trCount = [...html.matchAll(/<tr\b/gi)].length;
106
+ const tdCount = [...html.matchAll(/<t[dh]\b/gi)].length;
107
+ throw new Error(
108
+ `only ${cases.length} WG17 syntax rows were found ` +
109
+ `(saw ${trCount} row starts and ${tdCount} cell starts); upstream HTML format may have changed`,
110
+ );
111
+ }
112
+ return cases;
113
+ }
114
+
115
+ function canonicalQuery(query) {
116
+ return String(query).replace(/\r\n?/g, '\n').replace(/\u00a0/g, ' ').trim();
117
+ }
118
+
119
+
120
+ function canonicalExpected(expected) {
121
+ return String(expected).replace(/\s+/g, ' ').trim();
122
+ }
123
+
124
+ function isLayoutStart(source, index) {
125
+ if (index >= source.length) return true;
126
+ const ch = source[index];
127
+ return /\s/.test(ch) || ch === '%' || (ch === '/' && source[index + 1] === '*');
128
+ }
129
+
130
+ function firstTermEnd(source) {
131
+ let quote = null;
132
+ let lineComment = false;
133
+ let blockComment = false;
134
+ let depth = 0;
135
+
136
+ for (let index = 0; index < source.length; index++) {
137
+ const ch = source[index];
138
+ const next = source[index + 1];
139
+
140
+ if (lineComment) {
141
+ if (ch === '\n') lineComment = false;
142
+ continue;
143
+ }
144
+ if (blockComment) {
145
+ if (ch === '*' && next === '/') {
146
+ blockComment = false;
147
+ index++;
148
+ }
149
+ continue;
150
+ }
151
+ if (quote != null) {
152
+ if (ch === '\\') {
153
+ index++;
154
+ continue;
155
+ }
156
+ if (ch === quote && next === quote) {
157
+ index++;
158
+ continue;
159
+ }
160
+ if (ch === quote) quote = null;
161
+ continue;
162
+ }
163
+
164
+ if (ch === '%') {
165
+ lineComment = true;
166
+ continue;
167
+ }
168
+ if (ch === '/' && next === '*') {
169
+ blockComment = true;
170
+ index++;
171
+ continue;
172
+ }
173
+ if (ch === "'" && /\d/.test(source[index - 1] ?? '')) {
174
+ // Character-code constant such as 0'. or 0'\\n: apostrophe is not a
175
+ // quoted-atom delimiter. Skip the character (or escaped character).
176
+ if (next === '\\') index += 2;
177
+ else index++;
178
+ continue;
179
+ }
180
+ if (ch === "'" || ch === '"' || ch === '`') {
181
+ quote = ch;
182
+ continue;
183
+ }
184
+ if (ch === '(' || ch === '[' || ch === '{') {
185
+ depth++;
186
+ continue;
187
+ }
188
+ if (ch === ')' || ch === ']' || ch === '}') {
189
+ if (depth > 0) depth--;
190
+ continue;
191
+ }
192
+ if (ch === '.' && depth === 0 && isLayoutStart(source, index + 1)) return index + 1;
193
+ }
194
+ return -1;
195
+ }
196
+
197
+ function skipLayoutAndComments(source, start = 0) {
198
+ let index = start;
199
+ while (index < source.length) {
200
+ if (/\s/.test(source[index])) {
201
+ index++;
202
+ continue;
203
+ }
204
+ if (source[index] === '%') {
205
+ const newline = source.indexOf('\n', index + 1);
206
+ if (newline < 0) return source.length;
207
+ index = newline + 1;
208
+ continue;
209
+ }
210
+ if (source[index] === '/' && source[index + 1] === '*') {
211
+ const end = source.indexOf('*/', index + 2);
212
+ if (end < 0) return index;
213
+ index = end + 2;
214
+ continue;
215
+ }
216
+ break;
217
+ }
218
+ return index;
219
+ }
220
+
221
+ // Return the first complete Prolog term, without its terminating full stop.
222
+ // This is used only to reconstruct the setup denoted by upstream /**/ rows.
223
+ export function firstPrologTerm(source) {
224
+ const start = skipLayoutAndComments(source, 0);
225
+ const end = firstTermEnd(source.slice(start));
226
+ if (end < 0) return source.slice(start).trim().replace(/\.$/, '').trim();
227
+ return source.slice(start, start + end - 1).trim();
228
+ }
229
+
230
+ export function countTopLevelTerms(source) {
231
+ let index = 0;
232
+ let count = 0;
233
+ while (true) {
234
+ index = skipLayoutAndComments(source, index);
235
+ if (index >= source.length) break;
236
+ const end = firstTermEnd(source.slice(index));
237
+ count++;
238
+ if (end < 0) break;
239
+ index += end;
240
+ }
241
+ return Math.max(1, count);
242
+ }
243
+
244
+ export function setupInput(query, precedingBaseQuery) {
245
+ if (!query.includes('/**/')) return query;
246
+ if (precedingBaseQuery == null) throw new Error(`WG17 query uses /**/ without a preceding setup: ${query}`);
247
+ const setup = firstPrologTerm(precedingBaseQuery);
248
+ if (setup.length === 0) throw new Error(`cannot derive WG17 setup from: ${precedingBaseQuery}`);
249
+ const tail = query.replace('/**/', '');
250
+ return `(catch((${setup}), _, true) -> true ; true).\n${tail}`;
251
+ }
252
+
253
+ function reconcileSyntaxCases(upstream, previous) {
254
+ const previousById = new Map(previous.cases.map((item) => [item.id, item]));
255
+ const nextCases = [];
256
+ const added = [];
257
+ const changed = [];
258
+ let precedingBaseQuery = null;
259
+
260
+ for (const row of upstream) {
261
+ const old = previousById.get(row.id);
262
+ const same = old != null &&
263
+ canonicalQuery(old.query) === canonicalQuery(row.query) &&
264
+ canonicalExpected(old.expected) === canonicalExpected(row.expected);
265
+
266
+ if (same) {
267
+ nextCases.push(old);
268
+ } else {
269
+ const input = setupInput(row.query, precedingBaseQuery);
270
+ const item = {
271
+ id: row.id,
272
+ query: row.query,
273
+ input,
274
+ readCount: countTopLevelTerms(input),
275
+ expected: row.expected,
276
+ assertion: 'upstream',
277
+ };
278
+ nextCases.push(item);
279
+ if (old == null) added.push(row.id);
280
+ else changed.push(row.id);
281
+ }
282
+
283
+ if (!row.query.includes('/**/')) precedingBaseQuery = row.query;
284
+ previousById.delete(row.id);
285
+ }
286
+
287
+ return {
288
+ cases: nextCases,
289
+ added,
290
+ changed,
291
+ removed: [...previousById.keys()].sort((a, b) => a - b),
292
+ };
293
+ }
294
+
295
+ function inventoryFromCases(cases) {
296
+ const ids = cases.map(({ id }) => id);
297
+ if (ids.length === 0) throw new Error('WG17 syntax inventory is empty');
298
+ const firstId = Math.min(...ids);
299
+ const lastId = Math.max(...ids);
300
+ const active = new Set(ids);
301
+ const deletedIds = [];
302
+ for (let id = firstId; id <= lastId; id++) if (!active.has(id)) deletedIds.push(id);
303
+ return { firstId, lastId, deletedIds, activeCases: cases.length };
304
+ }
305
+
306
+ function sourceRevision(html) {
307
+ const text = htmlCellText(html);
308
+ const revisions = [...text.matchAll(/\brevision\s+([0-9]+(?:\.[0-9]+)*)/gi)];
309
+ return revisions.at(-1)?.[1] ?? null;
310
+ }
311
+
312
+ function dateStamp() {
313
+ return new Date().toISOString().slice(0, 10);
314
+ }
315
+
316
+ function formatIdList(ids) {
317
+ return ids.length === 0 ? 'none' : ids.map((id) => `#${id}`).join(', ');
318
+ }
319
+
320
+ async function readSource(source) {
321
+ if (/^https?:\/\//i.test(source)) {
322
+ const response = await fetch(source, {
323
+ headers: { 'user-agent': 'EyeProlog-WG17-upgrader/1' },
324
+ redirect: 'follow',
325
+ });
326
+ if (!response.ok) throw new Error(`WG17 fetch failed: ${response.status} ${response.statusText}`);
327
+ const bytes = new Uint8Array(await response.arrayBuffer());
328
+ const header = response.headers.get('content-type') ?? '';
329
+ return { bytes, html: decodeDocument(bytes, header) };
330
+ }
331
+ const filename = path.resolve(source);
332
+ const bytes = new Uint8Array(fs.readFileSync(filename));
333
+ return { bytes, html: decodeDocument(bytes, '') };
334
+ }
335
+
336
+ export function decodeDocument(bytes, contentType = '') {
337
+ const prefix = Buffer.from(bytes.subarray(0, Math.min(bytes.length, 8192))).toString('latin1');
338
+ const headerCharset = contentType.match(/charset\s*=\s*["']?([^;"'\s]+)/i)?.[1];
339
+ const metaCharset = prefix.match(/<meta[^>]+charset\s*=\s*["']?([^"'\s/>;]+)/i)?.[1] ??
340
+ prefix.match(/<meta[^>]+content\s*=\s*["'][^"']*charset\s*=\s*([^;"'\s>]+)/i)?.[1];
341
+ const label = headerCharset ?? metaCharset ?? 'utf-8';
342
+ try {
343
+ return new TextDecoder(label).decode(bytes);
344
+ } catch (_) {
345
+ return new TextDecoder('windows-1252').decode(bytes);
346
+ }
347
+ }
348
+
349
+ function parseArgs(argv) {
350
+ const options = { check: false, source: syntaxSource };
351
+ for (let index = 0; index < argv.length; index++) {
352
+ const arg = argv[index];
353
+ if (arg === '--check') options.check = true;
354
+ else if (arg === '--source') {
355
+ if (argv[index + 1] == null) throw new Error('--source requires a URL or filename');
356
+ options.source = argv[++index];
357
+ } else if (arg === '--help' || arg === '-h') options.help = true;
358
+ else throw new Error(`unknown option ${arg}`);
359
+ }
360
+ return options;
361
+ }
362
+
363
+ function printHelp() {
364
+ process.stdout.write(`Usage: npm run wg17:upgrade -- [--check] [--source URL_OR_FILE]\n\n` +
365
+ `Refreshes the vendored WG17 conformity tests from the TU Wien table.\n` +
366
+ `New or changed rows are executable immediately against the upstream\n` +
367
+ `Codex expectation; existing reviewed exact outcomes remain pinned.\n`);
368
+ }
369
+
370
+ export async function upgradeWg17({ check = false, source = syntaxSource } = {}) {
371
+ const previous = JSON.parse(fs.readFileSync(syntaxFixturePath, 'utf8'));
372
+ const { bytes, html } = await readSource(source);
373
+ const upstream = parseWg17SyntaxTable(html);
374
+ const reconciliation = reconcileSyntaxCases(upstream, previous);
375
+ const semanticChanges = reconciliation.added.length + reconciliation.changed.length + reconciliation.removed.length;
376
+
377
+ process.stdout.write(`WG17 syntax: ${upstream.length} active upstream cases\n`);
378
+ process.stdout.write(` added: ${formatIdList(reconciliation.added)}\n`);
379
+ process.stdout.write(` changed: ${formatIdList(reconciliation.changed)}\n`);
380
+ process.stdout.write(` removed: ${formatIdList(reconciliation.removed)}\n`);
381
+
382
+ if (check) {
383
+ if (semanticChanges > 0) {
384
+ process.stderr.write('WG17 snapshot is stale; run npm run wg17:upgrade.\n');
385
+ process.exitCode = 1;
386
+ return { changed: true, ...reconciliation };
387
+ }
388
+ process.stdout.write('WG17 snapshot matches the upstream test inventory.\n');
389
+ return { changed: false, ...reconciliation };
390
+ }
391
+
392
+ const checkedOn = dateStamp();
393
+ const fixture = {
394
+ ...previous,
395
+ source: syntaxSource,
396
+ checkedOn,
397
+ sourceRevision: sourceRevision(html),
398
+ sourceSha256: crypto.createHash('sha256').update(bytes).digest('hex'),
399
+ protocol: 'Each query is read and executed in strict ISO mode; /**/ rows reuse the preceding setup.',
400
+ cases: reconciliation.cases,
401
+ };
402
+ fs.writeFileSync(syntaxFixturePath, `${JSON.stringify(fixture, null, 2)}\n`);
403
+
404
+ const coverage = JSON.parse(fs.readFileSync(syntaxCoveragePath, 'utf8'));
405
+ coverage.source = syntaxSource;
406
+ coverage.checkedOn = checkedOn;
407
+ coverage.upstream = inventoryFromCases(fixture.cases);
408
+ for (const evidence of coverage.evidence ?? []) {
409
+ if (['all-active', 'all-reviewed'].includes(evidence.ids)) evidence.ids = 'all-executable';
410
+ if (evidence.path === 'test/conformance/wg17-syntax-cases.json') evidence.link = '../run-wg17.mjs';
411
+ }
412
+ fs.writeFileSync(syntaxCoveragePath, `${JSON.stringify(coverage, null, 2)}\n`);
413
+
414
+ // Generate status after the fixture/manifest are synchronized.
415
+ const { renderWg17SyntaxStatus } = await import('./report-wg17-syntax-coverage.mjs');
416
+ fs.writeFileSync(syntaxStatusPath, renderWg17SyntaxStatus());
417
+
418
+ const upstreamAssertions = fixture.cases
419
+ .filter((item) => item.outcome == null)
420
+ .map(({ id }) => id);
421
+ process.stdout.write(`Updated WG17 snapshot (${fixture.cases.length} cases).\n`);
422
+ if (upstreamAssertions.length > 0) {
423
+ process.stdout.write(
424
+ `Direct upstream assertions used by test:wg17: ${formatIdList(upstreamAssertions)}\n`,
425
+ );
426
+ }
427
+ return { changed: semanticChanges > 0, upstreamAssertions, ...reconciliation };
428
+ }
429
+
430
+ const isMain = process.argv[1] != null && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url;
431
+ if (isMain) {
432
+ try {
433
+ const options = parseArgs(process.argv.slice(2));
434
+ if (options.help) printHelp();
435
+ else await upgradeWg17(options);
436
+ } catch (error) {
437
+ process.stderr.write(`${error?.stack ?? error}\n`);
438
+ process.exitCode = 1;
439
+ }
440
+ }
package/why-eyeprolog.md CHANGED
@@ -20,7 +20,7 @@ syntax.
20
20
  EyeProlog implements the Part 1 core together with Technical Corrigenda 1, 2,
21
21
  and 3, Part 2 modules, and the Part 3 definite clause grammar specification.
22
22
  Its executable conformance matrix and tests document the supported
23
- behavior, including a complete executable trace of the 366 active WG17 syntax
23
+ behavior, including an executable trace of the vendored active WG17 syntax
24
24
  cases. This is extensive implementation evidence, not certification by an
25
25
  independent standards body.
26
26