eyeprolog 1.4.0 → 1.4.2

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
@@ -68,7 +68,7 @@ printf 'human(socrates).\nmortal(X) :- human(X).\n' |
68
68
  - [Symbiotic Knowledge Graphs](examples/deck/symbiotic-knowledge-graphs.md) — RDF ↔ Prolog heatwave-response demo for human/AI/KG co-evolution
69
69
  - [rdf-prolog-roundtrip](https://github.com/eyereasoner/rdf-prolog-roundtrip) — standalone RDF 1.2 ↔ ISO Prolog bridge used by the RDF examples
70
70
  - [ISO conformance audit](test/conformance/ISO-COMPLIANCE.md) — supported Part 1 profile
71
- - [Conformance report](conformance-report.md) — generated public corpus summary
71
+ - [Conformance report](conformance-report.md) — generated executable conformance status and corpus summary
72
72
  - [OpenRuleBench](openrulebench/README.md) — portable benchmark profile
73
73
 
74
74
  ## RDF, Prolog, and symbiotic knowledge graphs
@@ -1,6 +1,20 @@
1
1
  # EyeProlog conformance report
2
2
 
3
- This report summarizes the file-based conformance corpus under `test/conformance/`.
3
+ This report combines an executable external conformance gate with the file-based
4
+ conformance corpus under `test/conformance/`. The executable result is measured
5
+ when this report is generated; it is not inferred from fixture counts.
6
+
7
+ ## Executable conformance status
8
+
9
+ | Gate | Passed | Total | Status |
10
+ |---|---:|---:|---|
11
+ | WG17 syntax | 372 | 372 | pass |
12
+
13
+ The WG17 syntax row executes the vendored 372-case conformity-testing matrix
14
+ against EyeProlog's strict ISO reader/writer. A behavior fix such as operator-token
15
+ spelling therefore changes this report even when no corpus file is added or removed.
16
+
17
+ ## File-based corpus inventory
4
18
 
5
19
  | Category | Positive | Errors | Warnings | Proofs | Total |
6
20
  |---|---:|---:|---:|---:|---:|
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.4.0",
6
+ "version": "1.4.2",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/write.js CHANGED
@@ -99,17 +99,30 @@ function compactPrefixOperator(token, argument) {
99
99
  return `${token}${needsLayout ? ' ' : ''}${argument}`;
100
100
  }
101
101
 
102
+ function quotedOperatorAfterNumericNeedsSpace(left, token) {
103
+ if (!token.startsWith("'") || !/^[0-9]+$/.test(left)) return false;
104
+ const value = Number(left);
105
+ // `0'X` starts character-code notation and bases 2..36 start based-number
106
+ // notation. Base 1 and values above 36 do not, so they need no layout
107
+ // before a quoted operator (WG17 #208, #355).
108
+ return value === 0 || (value >= 2 && value <= 36);
109
+ }
110
+
102
111
  function compactPostfixOperator(argument, token) {
103
- if (isWordOperatorToken(token)) return `${argument} ${token}`;
104
- return `${argument}${compactBoundaryNeedsSpace(argument, token) ? ' ' : ''}${token}`;
112
+ const needsLayout = compactBoundaryNeedsSpace(argument, token) ||
113
+ quotedOperatorAfterNumericNeedsSpace(argument, token);
114
+ return `${argument}${needsLayout ? ' ' : ''}${token}`;
105
115
  }
106
116
 
107
117
  function compactInfixOperator(left, token, right) {
108
118
  // Solo punctuation operators already delimit themselves.
109
119
  if (token === ',' || token === ';') return `${left}${token}${right}`;
110
- if (isWordOperatorToken(token)) return `${left} ${token} ${right}`;
111
- const before = compactBoundaryNeedsSpace(left, token) ? ' ' : '';
112
- const after = compactBoundaryNeedsSpace(token, right) ? ' ' : '';
120
+ const before = (compactBoundaryNeedsSpace(left, token) ||
121
+ quotedOperatorAfterNumericNeedsSpace(left, token)) ? ' ' : '';
122
+ let after = compactBoundaryNeedsSpace(token, right) ? ' ' : '';
123
+ // A word operator immediately followed by `(` would look like functional
124
+ // notation for that atom rather than operator notation.
125
+ if (isWordOperatorToken(token) && right.startsWith('(')) after = ' ';
113
126
  return `${left}${before}${token}${after}${right}`;
114
127
  }
115
128
 
@@ -384,7 +397,17 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
384
397
  ['fx', 'fy', 'xfx', 'xfy', 'yfx'].includes(childDefinition.specifier)) {
385
398
  argumentPriority = priority - 1;
386
399
  }
387
- const argument = format(resolved.args[0], env, options, table, argumentPriority);
400
+ const child = deref(resolved.args[0], env);
401
+ let argument = format(resolved.args[0], env, options, table, argumentPriority);
402
+ // If both the operand atom and postfix operator require quoted names,
403
+ // plain adjacency would form quoted-atom escape syntax and layout would
404
+ // not match the ISO writer form. Parenthesize the operator atom instead
405
+ // (WG17 #169). Do not apply this to ordinary word postfix operators.
406
+ if (options.minimalOperatorSpacing && child.type === ATOM && table.has(child.name) &&
407
+ writeAtom(child.name).startsWith("'") && token.startsWith("'") &&
408
+ compactBoundaryNeedsSpace(argument, token)) {
409
+ argument = `(${argument})`;
410
+ }
388
411
  text = options.minimalOperatorSpacing ? compactPostfixOperator(argument, token) : `${argument} ${token}`;
389
412
  } else {
390
413
  let leftPriority = specifier === 'yfx' ? priority : priority - 1;
@@ -6,7 +6,7 @@ together with Technical Corrigenda 1:2007, 2:2012, and 3:2017. Detailed
6
6
  row-level evidence lives only where it adds information: the built-in, processor,
7
7
  term-semantics, Prolog-text/execution, evaluable-functor, implementation-defined,
8
8
  WG17-syntax, and STC draft ledgers in this directory. The generated
9
- [`conformance-report.md`](../../conformance-report.md) gives current case totals.
9
+ [`conformance-report.md`](../../conformance-report.md) gives the current executable WG17 syntax result together with file-based case totals.
10
10
 
11
11
  The ledger deliberately does **not** claim independent certification. A row
12
12
  marked `covered` means EyeProlog has implementation and executable tests for
@@ -19,7 +19,7 @@ relevant implementation-dependent overlap choice to an explicit audit outcome.
19
19
 
20
20
  | Requirement | Status | EyeProlog evidence / remaining work |
21
21
  | --- | --- | --- |
22
- | 5.1(a) prepare conforming Prolog text | covered | Clause 6 token/term production and rejection families are mapped by the strict production gate and the complete 366-case vendored WG17 syntax matrix; 7.4 preparation/directive behavior is separately closed. |
22
+ | 5.1(a) prepare conforming Prolog text | covered | Clause 6 token/term production and rejection families are mapped by the strict production gate and the complete 372-case vendored WG17 syntax matrix; 7.4 preparation/directive behavior is separately closed. |
23
23
  | 5.1(b) execute conforming Prolog goals | covered | Clause 7 term/control/execution semantics, the higher-level 7.10 stream model, 7.11 flags, the 7.12 error envelope, all 8.2-8.17 built-in rows, and Clause 9 arithmetic have explicit executable dispositions. |
24
24
  | 5.1(c) reject nonconforming text/read-terms | covered | WG17 negative syntax cases, focused malformed-production/escape/comment/operator cases, read-term syntax errors, and strict rejection of implementation-specific syntax provide explicit rejection evidence across the Clause 6 families. |
25
25
  | 5.1(d) document permitted variations | covered | The clause-by-clause [ISO 5.4 decision index](ISO-IMPLEMENTATION-DEFINED.md) records every explicit implementation-defined decision found in the Part 1 + Corrigenda baseline and separately inventories implementation-specific extension families. The index retains an `audit gap` category for future discoveries; no current release-facing row depends on one. |
@@ -31,7 +31,7 @@ relevant implementation-dependent overlap choice to an explicit audit outcome.
31
31
 
32
32
  | Standard area | Status | Current evidence |
33
33
  | --- | --- | --- |
34
- | Clause 6 — tokens, terms, lists, operators, quoted text | covered | The strict Clause 6 gate maps atomic/variable/compound/operator/list/curly/double-quoted term forms, layout/comments, quoted escapes, integer bases/character codes, floating tokens, and solo/meta token boundaries, with malformed counterparts rejected. The complete 366-case WG17 syntax matrix provides independent production-level coverage, and a cross-profile gate verifies that normal syntax extensions preserve every strict-accepted standard observation. The implementation-defined 6.5/6.6 PCS/collation choices are documented separately. |
34
+ | Clause 6 — tokens, terms, lists, operators, quoted text | covered | The strict Clause 6 gate maps atomic/variable/compound/operator/list/curly/double-quoted term forms, layout/comments, quoted escapes, integer bases/character codes, floating tokens, and solo/meta token boundaries, with malformed counterparts rejected. The complete 372-case WG17 syntax matrix provides independent production-level coverage, and a cross-profile gate verifies that normal syntax extensions preserve every strict-accepted standard observation. The implementation-defined 6.5/6.6 PCS/collation choices are documented separately. |
35
35
  | 7.1-7.3 — term types, term order, unification | covered | [ISO-TERM-SEMANTICS-MATRIX.md](ISO-TERM-SEMANTICS-MATRIX.md) records the five mutually exclusive strict term types, derived variable/compound/list notions, the complete standard-order classes and implementation-dependent variable-order choice, NSTO MGU/failure behavior, and EyeProlog's permitted consistent occurs-check failure for Part 1's undefined STO ordinary-unification cases. The normal JavaScript API string term is documented as a 5.5.4 extension and rejected at strict program/goal entry. |
36
36
  | 7.4 — Prolog text and directives | covered | [ISO-PROLOG-TEXT-EXECUTION-MATRIX.md](ISO-PROLOG-TEXT-EXECUTION-MATRIX.md) closes the preparation/directive rows: declaration semantics, source clauses, cross-text operators/character conversion/flags, initialization order/lifetime, `include/1`, and one-time `ensure_loaded/1`, with implementation-defined cross-text choices indexed under 5.4. |
37
37
  | 7.5-7.6 — database and term/clause conversion | covered | [ISO-PROLOG-TEXT-EXECUTION-MATRIX.md](ISO-PROLOG-TEXT-EXECUTION-MATRIX.md) closes static/dynamic and private/public procedure semantics, clause order, logical-update visibility, empty/unknown lifetime, and term/body/clause conversion. Source and runtime assertion conversion recurse through `,/2`, `;/2`, and `->/2`, preserve variable identity, and protect standardized static/control procedures. |
@@ -286,7 +286,7 @@ post-N289 STC drafts remain review input until standardized.
286
286
  | Criterion | Status | Evidence |
287
287
  | --- | --- | --- |
288
288
  | Clause 5 processor obligations have explicit dispositions | covered | `ISO-PROCESSOR-REQUIREMENTS.md` gives explicit outcomes for 5.1-5.5, including the strict/normal extension boundary. |
289
- | Clause 6 lexical/syntactic requirements have explicit dispositions | covered | the strict Clause 6 production/rejection gate and the 366-case vendored WG17 matrix cover the standard families; cross-profile preservation verifies that normal extensions do not reinterpret strict-accepted text. |
289
+ | Clause 6 lexical/syntactic requirements have explicit dispositions | covered | the strict Clause 6 production/rejection gate and the 372-case vendored WG17 matrix cover the standard families; cross-profile preservation verifies that normal extensions do not reinterpret strict-accepted text. |
290
290
  | Clause 7 semantic requirements have explicit dispositions | covered | 7.1-7.12 now have explicit semantic, implementation-defined, or error-envelope dispositions backed by strict tests and the specialized matrices. |
291
291
  | Clause 8 built-in modes/errors have explicit dispositions | covered | `ISO-BUILTIN-MODE-ERROR-MATRIX.md` closes 8.2-8.17 row by row |
292
292
  | Clause 9 evaluable-functor requirements have explicit dispositions | covered | `ISO-EVALUABLE-FUNCTOR-MATRIX.md` plus the strict arithmetic regression closes the published Part 1 + Corrigenda arithmetic rows |
@@ -294,7 +294,7 @@ post-N289 STC drafts remain review input until standardized.
294
294
  | 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. |
295
295
  | Published Corrigenda 1-3 are incorporated | covered | dedicated conformance/strict tests and this ledger track all three published Corrigenda |
296
296
  | 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 |
297
- | Independent external syntax corpus is an offline release gate | covered | the vendored WG17 syntax matrix has 366 executable dispositions checked against its upstream expectations |
297
+ | Independent external syntax corpus is an offline release gate | covered | the vendored WG17 syntax matrix has 372 executable dispositions checked against its upstream expectations |
298
298
  | 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` |
299
299
  | 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. |
300
300
 
@@ -109,7 +109,7 @@ terminating newline, so stream-sensitive cases such as #270 and #271 exercise
109
109
  the characters left after `read/1`. Normal `npm test` remains offline and uses
110
110
  only the committed upstream snapshot.
111
111
 
112
- Summarize conformance coverage by category:
112
+ Regenerate the public conformance report, including the executable WG17 syntax status and the file-based category inventory:
113
113
 
114
114
  ```sh
115
115
  node test/run-conformance-report.mjs
@@ -145,9 +145,9 @@ are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
145
145
 
146
146
  The corpus has 386 cases in `iso/` and 802 file-based conformance cases in
147
147
  total. Of those, 11 cases in `stc/` are explicitly labelled working-draft
148
- review evidence rather than normative ISO claims. The separate strict-reader WG17 matrix has 366 executable dispositions.
149
- The generated `conformance-report.md` is the authoritative source for current
150
- category totals. Together with regression, documentation-sync, API, example,
148
+ review evidence rather than normative ISO claims. The separate strict-reader WG17 matrix has 372 executable dispositions.
149
+ The generated `conformance-report.md` is the authoritative source for the current
150
+ executable WG17 syntax result and file-based category totals. Together with regression, documentation-sync, API, example,
151
151
  and book-example checks, `npm test` is the release gate.
152
152
 
153
153
  ## Updating expected output
@@ -1,16 +1,16 @@
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-16
4
+ Upstream inventory checked: 2026-08-28
5
5
 
6
6
  This ledger counts an upstream case when its WG17 identifier, query, and
7
7
  expected ISO disposition are stored in the offline executable matrix. Every
8
8
  case is executed against the upstream Codex expectation. Reviewed exact
9
9
  EyeProlog outcomes are additional regression locks and can never override the
10
- upstream assertion. Writer expectations also preserve ISO-significant token
11
- spelling: in particular, a bare bar used as an operator is not treated as
12
- equivalent to a quoted atom, and a bare semicolon name token is not silently
13
- accepted in quoted form.
10
+ upstream assertion. Concrete writer expectations are matched lexically: layout,
11
+ parentheses, and quoting are significant. Only Codex expectations explicitly
12
+ marked e.g. use controlled flexibility for representation choices such as
13
+ generated variable names or equivalent numeric spellings.
14
14
 
15
15
  ## Current standing
16
16
 
@@ -20,25 +20,25 @@ A represented case still has to pass the executable upstream assertion in
20
20
 
21
21
  | Measure | Count |
22
22
  | --- | ---: |
23
- | Active upstream cases | 366 |
24
- | Upstream cases represented in the executable matrix | 366 (100.0%) |
23
+ | Active upstream cases | 372 |
24
+ | Upstream cases represented in the executable matrix | 372 (100.0%) |
25
25
  | Not yet traced one-by-one | 0 |
26
26
  | Deleted upstream identifiers | #20, #273 |
27
27
 
28
28
  The matrix runs in strict ISO stream-reader mode as part of `npm test`. The
29
29
  3 upstream `waits` cases are checked through EyeProlog's interactive input
30
- hook. All 366 executable cases are independently checked against the
31
- upstream Codex expectation. 366 cases additionally retain exact reviewed
30
+ hook. All 372 executable cases are independently checked against the
31
+ upstream Codex expectation. 372 cases additionally retain exact reviewed
32
32
  outcomes for stronger regression checking; 0 cases currently rely on the upstream assertion alone.
33
33
 
34
34
  ## Traceable evidence
35
35
 
36
36
  | Executable evidence | Referenced IDs | WG17 cases |
37
37
  | --- | ---: | --- |
38
- | [complete offline executable matrix](../run-wg17.mjs) | 366 | #1–#19, #21–#272, #274–#368 |
38
+ | [complete offline executable matrix](../run-wg17.mjs) | 372 | #1–#19, #21–#272, #274–#374 |
39
39
 
40
- The evidence groups overlap. Their union is **366** active cases:
41
- #1–#19, #21–#272, #274–#368.
40
+ The evidence groups overlap. Their union is **372** active cases:
41
+ #1–#19, #21–#272, #274–#374.
42
42
 
43
43
  ## Untraced upstream identifiers
44
44
 
@@ -2486,7 +2486,7 @@
2486
2486
  "variables": "[]"
2487
2487
  },
2488
2488
  {
2489
- "output": "0''",
2489
+ "output": "0 ''",
2490
2490
  "variables": "[]"
2491
2491
  }
2492
2492
  ]
@@ -2506,7 +2506,7 @@
2506
2506
  "variables": "[]"
2507
2507
  },
2508
2508
  {
2509
- "output": "0''",
2509
+ "output": "0 ''",
2510
2510
  "variables": "[]"
2511
2511
  }
2512
2512
  ]
@@ -2653,7 +2653,7 @@
2653
2653
  "variables": "[]"
2654
2654
  },
2655
2655
  {
2656
- "output": "0'f '",
2656
+ "output": "0 'f '",
2657
2657
  "variables": "[]"
2658
2658
  }
2659
2659
  ]
@@ -3313,7 +3313,7 @@
3313
3313
  "variables": "[]"
3314
3314
  },
3315
3315
  {
3316
- "output": "(fy 1) yf",
3316
+ "output": "(fy 1)yf",
3317
3317
  "variables": "[]"
3318
3318
  }
3319
3319
  ]
@@ -3333,7 +3333,7 @@
3333
3333
  "variables": "[]"
3334
3334
  },
3335
3335
  {
3336
- "output": "(fy (fy 1) yf) yf",
3336
+ "output": "(fy (fy 1)yf)yf",
3337
3337
  "variables": "[]"
3338
3338
  }
3339
3339
  ]
@@ -3393,7 +3393,7 @@
3393
3393
  "variables": "[]"
3394
3394
  },
3395
3395
  {
3396
- "output": "(fy 1) yfx 2",
3396
+ "output": "(fy 1)yfx 2",
3397
3397
  "variables": "[]"
3398
3398
  }
3399
3399
  ]
@@ -3453,7 +3453,7 @@
3453
3453
  "variables": "[]"
3454
3454
  },
3455
3455
  {
3456
- "output": "(1 xfy 2) yf",
3456
+ "output": "(1 xfy 2)yf",
3457
3457
  "variables": "[]"
3458
3458
  }
3459
3459
  ]
@@ -3587,7 +3587,7 @@
3587
3587
  "query": "op(9,fy,p),op(9,xfy,p).\nwrite_canonical(1 p p p 2).",
3588
3588
  "input": "op(9,fy,p),op(9,xfy,p).\nwrite_canonical(1 p p p 2).",
3589
3589
  "readCount": 2,
3590
- "expected": "p(1, p(p(2)))",
3590
+ "expected": "p(1,p(p(2)))",
3591
3591
  "outcome": {
3592
3592
  "type": "success",
3593
3593
  "stages": [
@@ -3607,7 +3607,7 @@
3607
3607
  "query": "op(7,fy,p),op(9,yfx,p).\nwrite_canonical(1 p p p 2).",
3608
3608
  "input": "op(7,fy,p),op(9,yfx,p).\nwrite_canonical(1 p p p 2).",
3609
3609
  "readCount": 2,
3610
- "expected": "p(1, p(p(2)))",
3610
+ "expected": "p(1,p(p(2)))",
3611
3611
  "outcome": {
3612
3612
  "type": "success",
3613
3613
  "stages": [
@@ -3695,7 +3695,7 @@
3695
3695
  "variables": "[]"
3696
3696
  },
3697
3697
  {
3698
- "output": "'.' '.'",
3698
+ "output": "('.')'.'",
3699
3699
  "variables": "[]"
3700
3700
  }
3701
3701
  ]
@@ -5565,6 +5565,97 @@
5565
5565
  }
5566
5566
  ]
5567
5567
  }
5568
+ },
5569
+ {
5570
+ "id": 369,
5571
+ "query": "writeq(?-).",
5572
+ "input": "writeq(?-).",
5573
+ "readCount": 1,
5574
+ "expected": "?-",
5575
+ "outcome": {
5576
+ "type": "success",
5577
+ "stages": [
5578
+ {
5579
+ "output": "?-",
5580
+ "variables": "[]"
5581
+ }
5582
+ ]
5583
+ }
5584
+ },
5585
+ {
5586
+ "id": 370,
5587
+ "query": "writeq(./*).",
5588
+ "input": "writeq(./*).",
5589
+ "readCount": 1,
5590
+ "expected": "./*",
5591
+ "outcome": {
5592
+ "type": "success",
5593
+ "stages": [
5594
+ {
5595
+ "output": "./*",
5596
+ "variables": "[]"
5597
+ }
5598
+ ]
5599
+ }
5600
+ },
5601
+ {
5602
+ "id": 371,
5603
+ "query": "writeq({*}).",
5604
+ "input": "writeq({*}).",
5605
+ "readCount": 1,
5606
+ "expected": "{*}",
5607
+ "outcome": {
5608
+ "type": "success",
5609
+ "stages": [
5610
+ {
5611
+ "output": "{*}",
5612
+ "variables": "[]"
5613
+ }
5614
+ ]
5615
+ }
5616
+ },
5617
+ {
5618
+ "id": 372,
5619
+ "query": "writeq(.*).",
5620
+ "input": "writeq(.*).",
5621
+ "readCount": 1,
5622
+ "expected": ".*",
5623
+ "outcome": {
5624
+ "type": "success",
5625
+ "stages": [
5626
+ {
5627
+ "output": ".*",
5628
+ "variables": "[]"
5629
+ }
5630
+ ]
5631
+ }
5632
+ },
5633
+ {
5634
+ "id": 373,
5635
+ "query": "writeq(...*).",
5636
+ "input": "writeq(...*).",
5637
+ "readCount": 1,
5638
+ "expected": "...*",
5639
+ "outcome": {
5640
+ "type": "success",
5641
+ "stages": [
5642
+ {
5643
+ "output": "...*",
5644
+ "variables": "[]"
5645
+ }
5646
+ ]
5647
+ }
5648
+ },
5649
+ {
5650
+ "id": 374,
5651
+ "query": "Finis ().",
5652
+ "input": "Finis ().",
5653
+ "readCount": 1,
5654
+ "expected": "syntax err.",
5655
+ "outcome": {
5656
+ "type": "error",
5657
+ "formal": "syntax_error(read_term)"
5658
+ }
5568
5659
  }
5569
5660
  ],
5570
5661
  "sourceRevision": "1.300",
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "source": "https://www.complang.tuwien.ac.at/ulrich/iso-prolog/conformity_testing",
3
- "checkedOn": "2026-08-16",
3
+ "checkedOn": "2026-08-28",
4
4
  "upstream": {
5
5
  "firstId": 1,
6
- "lastId": 368,
6
+ "lastId": 374,
7
7
  "deletedIds": [
8
8
  20,
9
9
  273
10
10
  ],
11
- "activeCases": 366
11
+ "activeCases": 372
12
12
  },
13
13
  "evidence": [
14
14
  {
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // Static conformance corpus report.
3
- // This complements the executable runner with a category summary that makes
4
- // coverage growth visible without running every case.
2
+ // Executable conformance status plus static corpus inventory.
3
+ // The WG17 row is run when this report is generated, so a syntax-conformance
4
+ // regression changes the public report even when the fixture inventory itself
5
+ // has not changed.
5
6
  import fs from 'node:fs';
6
7
  import path from 'node:path';
7
8
  import { fileURLToPath } from 'node:url';
9
+ import { runWg17 } from './run-wg17.mjs';
8
10
  import { listPrologFiles } from './test-support.mjs';
9
11
 
10
12
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
@@ -18,9 +20,9 @@ const KINDS = [
18
20
  { kind: 'proofs', expectedKind: 'expected-proofs', expectedExt: '.pl', column: 'proofs' },
19
21
  ];
20
22
 
21
- export function buildConformanceReport() {
23
+ export function buildConformanceReport({ wg17Suite = runWg17 } = {}) {
22
24
  const categories = new Map();
23
- const issues = [];
25
+ const corpusIssues = [];
24
26
 
25
27
  for (const { kind, expectedKind, expectedExt, column } of KINDS) {
26
28
  const base = path.join(conformanceRoot, kind);
@@ -33,10 +35,10 @@ export function buildConformanceReport() {
33
35
 
34
36
  const stem = file.slice(0, -3);
35
37
  const expected = path.join(conformanceRoot, expectedKind, `${stem}${expectedExt}`);
36
- if (!fs.existsSync(expected)) issues.push(`missing ${expectedKind}/${stem}${expectedExt}`);
38
+ if (!fs.existsSync(expected)) corpusIssues.push(`missing ${expectedKind}/${stem}${expectedExt}`);
37
39
  if (kind === 'warnings') {
38
40
  const expectedStderr = path.join(conformanceRoot, expectedKind, `${stem}.txt`);
39
- if (!fs.existsSync(expectedStderr)) issues.push(`missing ${expectedKind}/${stem}.txt`);
41
+ if (!fs.existsSync(expectedStderr)) corpusIssues.push(`missing ${expectedKind}/${stem}.txt`);
40
42
  }
41
43
  }
42
44
  }
@@ -52,32 +54,99 @@ export function buildConformanceReport() {
52
54
  total: acc.total + row.total,
53
55
  }), { positive: 0, errors: 0, warnings: 0, proofs: 0, total: 0 });
54
56
 
55
- return { rows, total, issues: issues.sort() };
57
+ const executable = [executeGate('WG17 syntax', wg17Suite)];
58
+ const executionIssues = executable.flatMap((gate) => gate.failures.map((failure) =>
59
+ `${gate.name}: ${failure.name}: ${failure.message}`));
60
+
61
+ return {
62
+ rows,
63
+ total,
64
+ executable,
65
+ corpusIssues: corpusIssues.sort(),
66
+ executionIssues,
67
+ issues: [...corpusIssues.sort(), ...executionIssues],
68
+ };
56
69
  }
57
70
 
58
71
  export function formatConformanceReport(report = buildConformanceReport()) {
72
+ const wg17 = report.executable.find((gate) => gate.name === 'WG17 syntax');
73
+ const wg17Total = wg17?.total ?? 0;
59
74
  const lines = [
60
75
  '# EyeProlog conformance report',
61
76
  '',
62
- 'This report summarizes the file-based conformance corpus under `test/conformance/`.',
77
+ 'This report combines an executable external conformance gate with the file-based',
78
+ 'conformance corpus under `test/conformance/`. The executable result is measured',
79
+ 'when this report is generated; it is not inferred from fixture counts.',
80
+ '',
81
+ '## Executable conformance status',
82
+ '',
83
+ '| Gate | Passed | Total | Status |',
84
+ '|---|---:|---:|---|',
85
+ ];
86
+
87
+ for (const gate of report.executable) {
88
+ const status = gate.passed === gate.total ? 'pass' : 'fail';
89
+ lines.push(`| ${gate.name} | ${gate.passed} | ${gate.total} | ${status} |`);
90
+ }
91
+
92
+ lines.push(
93
+ '',
94
+ `The WG17 syntax row executes the vendored ${wg17Total}-case conformity-testing matrix`,
95
+ 'against EyeProlog\'s strict ISO reader/writer. A behavior fix such as operator-token',
96
+ 'spelling therefore changes this report even when no corpus file is added or removed.',
97
+ '',
98
+ '## File-based corpus inventory',
63
99
  '',
64
100
  '| Category | Positive | Errors | Warnings | Proofs | Total |',
65
101
  '|---|---:|---:|---:|---:|---:|',
66
- ];
102
+ );
67
103
 
68
104
  for (const row of report.rows) {
69
105
  lines.push(`| ${row.category} | ${row.positive} | ${row.errors} | ${row.warnings} | ${row.proofs} | ${row.total} |`);
70
106
  }
71
107
  lines.push(`| **Total** | **${report.total.positive}** | **${report.total.errors}** | **${report.total.warnings}** | **${report.total.proofs}** | **${report.total.total}** |`);
72
108
 
73
- if (report.issues.length > 0) {
109
+ if (report.corpusIssues.length > 0) {
74
110
  lines.push('', '## Corpus issues', '');
75
- for (const issue of report.issues) lines.push(`- ${issue}`);
111
+ for (const issue of report.corpusIssues) lines.push(`- ${issue}`);
112
+ }
113
+ if (report.executionIssues.length > 0) {
114
+ lines.push('', '## Executable conformance failures', '');
115
+ for (const issue of report.executionIssues) lines.push(`- ${issue}`);
76
116
  }
77
117
 
78
118
  return `${lines.join('\n')}\n`;
79
119
  }
80
120
 
121
+ function executeGate(name, runSuite) {
122
+ const failures = [];
123
+ const reporter = {
124
+ passed: 0,
125
+ total: 0,
126
+ section() {},
127
+ sectionTotal() {},
128
+ test(testName, run) {
129
+ this.total++;
130
+ try {
131
+ run();
132
+ this.passed++;
133
+ } catch (error) {
134
+ failures.push({
135
+ name: testName,
136
+ message: String(error?.message ?? error).split('\n', 1)[0],
137
+ });
138
+ }
139
+ },
140
+ };
141
+
142
+ try {
143
+ runSuite(reporter);
144
+ } catch (error) {
145
+ failures.push({ name: 'suite setup', message: String(error?.message ?? error).split('\n', 1)[0] });
146
+ }
147
+ return { name, passed: reporter.passed, total: reporter.total, failures };
148
+ }
149
+
81
150
  function categoryOf(file) {
82
151
  const parts = file.split('/');
83
152
  return parts.length > 1 ? parts[0] : 'legacy-numbered';
@@ -4726,6 +4726,54 @@ function documentationSyncCases() {
4726
4726
  false,
4727
4727
  'quoted semicolon is not accepted when the ISO name token is bare',
4728
4728
  );
4729
+ const quotedPostfixWriter = {
4730
+ id: 208,
4731
+ query: "op(100,xf,'f ').\nwriteq(0 'f ').",
4732
+ input: "op(100,xf,'f ').\nwriteq(0 'f ').",
4733
+ };
4734
+ assertEqual(
4735
+ matchesUpstreamExpectation(
4736
+ "0 'f '",
4737
+ { type: 'success', stages: [{ output: "0'f '", variables: '[]' }] },
4738
+ quotedPostfixWriter,
4739
+ ),
4740
+ false,
4741
+ 'mandatory layout before quoted postfix operator is not erased',
4742
+ );
4743
+ const dottedPostfixWriter = {
4744
+ id: 169,
4745
+ query: 'writeq(.(.)).',
4746
+ input: "op(0,xfy,.),op(9,yf,.).\nwriteq(.(.)).",
4747
+ };
4748
+ assertEqual(
4749
+ matchesUpstreamExpectation(
4750
+ "('.')'.'",
4751
+ { type: 'success', stages: [{ output: "'.' '.'", variables: '[]' }] },
4752
+ dottedPostfixWriter,
4753
+ ),
4754
+ false,
4755
+ 'mandatory writer parentheses are not replaced by layout',
4756
+ );
4757
+ const wordPostfixWriter = { id: 150, query: 'writeq(yf(fy(1))).', input: 'writeq(yf(fy(1))).' };
4758
+ assertEqual(
4759
+ matchesUpstreamExpectation(
4760
+ '(fy 1)yf',
4761
+ { type: 'success', stages: [{ output: '(fy 1) yf', variables: '[]' }] },
4762
+ wordPostfixWriter,
4763
+ ),
4764
+ false,
4765
+ 'concrete writer layout is matched exactly',
4766
+ );
4767
+ const canonicalWriter = { id: 163, query: 'write_canonical(1 p p p 2).', input: 'write_canonical(1 p p p 2).' };
4768
+ assertEqual(
4769
+ matchesUpstreamExpectation(
4770
+ 'p(1,p(p(2)))',
4771
+ { type: 'success', stages: [{ output: 'p(1, p(p(2)))', variables: '[]' }] },
4772
+ canonicalWriter,
4773
+ ),
4774
+ false,
4775
+ 'concrete canonical writer layout is matched exactly',
4776
+ );
4729
4777
  const repeated = {
4730
4778
  id: 227, input: 'write_canonical(B+B).',
4731
4779
  outcome: { type: 'success', stages: [{ output: '+(_A,_A)', variables: "['B' = B]" }] },
@@ -5029,10 +5077,27 @@ function documentationSyncCases() {
5029
5077
  assertEqual(report.total.total >= 475, true, 'conformance case count');
5030
5078
  assertEqual(report.total.positive + report.total.errors + report.total.warnings + report.total.proofs, report.total.total, 'conformance total');
5031
5079
  assertEqual(report.rows.some((row) => row.category === 'legacy-numbered'), false, 'legacy-numbered category');
5080
+ const wg17 = report.executable.find((gate) => gate.name === 'WG17 syntax');
5081
+ assertEqual(wg17?.total, 372, 'WG17 executable total');
5082
+ assertEqual(wg17?.passed, 372, 'WG17 executable passed');
5083
+ assertArrayEqual(wg17?.failures ?? [], [], 'WG17 executable failures');
5032
5084
  const text = formatConformanceReport(report);
5085
+ assertIncludes(text, '| WG17 syntax | 372 | 372 | pass |', 'report');
5033
5086
  assertIncludes(text, '| variables |', 'report');
5034
5087
  assertIncludes(text, '| Proofs |', 'report');
5035
5088
  assertIncludes(text, '| **Total** |', 'report');
5089
+
5090
+ const failing = buildConformanceReport({
5091
+ wg17Suite: (reporter) => {
5092
+ reporter.section('WG17 syntax');
5093
+ reporter.test('synthetic syntax failure', () => { throw new Error('synthetic mismatch'); });
5094
+ reporter.sectionTotal('WG17 syntax');
5095
+ },
5096
+ });
5097
+ assertEqual(failing.executable[0].passed, 0, 'failing WG17 executable passed');
5098
+ assertEqual(failing.executable[0].total, 1, 'failing WG17 executable total');
5099
+ assertEqual(failing.executionIssues.length, 1, 'failing WG17 report issue count');
5100
+ assertIncludes(formatConformanceReport(failing), '| WG17 syntax | 0 | 1 | fail |', 'failing report');
5036
5101
  },
5037
5102
  },
5038
5103
 
package/test/run-wg17.mjs CHANGED
@@ -306,12 +306,26 @@ function abbreviatedErrorMatches(expected, actual) {
306
306
  return patterns.some(([pattern, prefix]) => pattern.test(expected) && formal.startsWith(prefix));
307
307
  }
308
308
 
309
+ function isWriterCase(item) {
310
+ return /\bwrite(?:q|_canonical|_term)?\s*\(/.test(item?.query ?? '');
311
+ }
312
+
309
313
  function textExpectationMatches(expectedText, actual, item, example = false) {
310
314
  const candidates = observableCandidates(actual);
311
315
  if (candidates.length === 0) return false;
312
316
  let expected = presentationText(expectedText);
313
317
  if (example) expected = expected.replace(/^e\.g\.\s*/i, '');
314
318
 
319
+ // A concrete WG17 writer expectation describes output syntax, not merely
320
+ // the term denoted by that output. Layout, quoting and parentheses are
321
+ // therefore significant. Only expectations explicitly marked `e.g.` use
322
+ // the controlled semantic/layout fallbacks below.
323
+ const hasBindings = actual.type === 'success' &&
324
+ actual.stages.some(({ variables }) => bindingAnswer(variables).length > 0);
325
+ if (!example && isWriterCase(item) && !hasBindings) {
326
+ return candidates.some((candidate) => presentationText(candidate) === expected);
327
+ }
328
+
315
329
  for (const candidate of candidates) {
316
330
  let left = expected;
317
331
  let right = candidate;
@@ -7784,14 +7784,14 @@ their declared goals are executed. The eight-case
7784
7784
  playground contract suite imports the production worker, sends real reasoning
7785
7785
  requests through its message protocol, and crawls the served module graph for
7786
7786
  missing assets, bad MIME types, and static Node-only imports. The generated
7787
- `conformance-report.md` is the authoritative source for current conformance
7788
- category totals.
7787
+ `conformance-report.md` is the authoritative source for the current executable
7788
+ WG17 syntax result and file-based conformance category totals.
7789
7789
 
7790
7790
  ### Generated and checked repository material
7791
7791
 
7792
7792
  Repository artifacts have distinct roles:
7793
7793
 
7794
- - `conformance-report.md` is generated from the file-based conformance corpus;
7794
+ - `conformance-report.md` executes the vendored WG17 syntax gate and also inventories the file-based conformance corpus;
7795
7795
  - `examples/book/` is extracted from executable code blocks in this book and
7796
7796
  should be rebuilt with `npm run generate` rather than edited directly;
7797
7797
  - `examples/output/` and `examples/proof/` contain reviewed exact-output
@@ -95,10 +95,10 @@ This ledger counts an upstream case when its WG17 identifier, query, and
95
95
  expected ISO disposition are stored in the offline executable matrix. Every
96
96
  case is executed against the upstream Codex expectation. Reviewed exact
97
97
  EyeProlog outcomes are additional regression locks and can never override the
98
- upstream assertion. Writer expectations also preserve ISO-significant token
99
- spelling: in particular, a bare bar used as an operator is not treated as
100
- equivalent to a quoted atom, and a bare semicolon name token is not silently
101
- accepted in quoted form.
98
+ upstream assertion. Concrete writer expectations are matched lexically: layout,
99
+ parentheses, and quoting are significant. Only Codex expectations explicitly
100
+ marked e.g. use controlled flexibility for representation choices such as
101
+ generated variable names or equivalent numeric spellings.
102
102
 
103
103
  ## Current standing
104
104