eyeprolog 1.5.21 → 1.5.23

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.
@@ -47,6 +47,9 @@ import {
47
47
  unify,
48
48
  variantTerms,
49
49
  parseProgramText,
50
+ proofCertificate,
51
+ proofCertificatesFromText,
52
+ verifyProof,
50
53
  } from '../src/index.js';
51
54
  import { ISO_OPERATOR_DEFINITIONS, parseGoalText, parseNumberTokenText, tryParseClausesFastInto } from '../src/parser.js';
52
55
  import { PrologError, formalErrorTerm } from '../src/iso.js';
@@ -73,6 +76,10 @@ const testRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
73
76
  const packageRoot = path.resolve(testRoot, '..');
74
77
  const bin = path.join(packageRoot, 'bin', 'eyeprolog.js');
75
78
  const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
79
+ // The issue #49 hand-off checks assert demand-driven completion and bounded memory,
80
+ // not a machine-speed threshold. Leave enough wall-clock headroom for slower or
81
+ // contended CI hosts while still terminating a genuinely stuck child process.
82
+ const DCG_HANDOFF_TEST_TIMEOUT_MS = 20_000;
76
83
  let tmp = null;
77
84
  let tmpCounter = 0;
78
85
 
@@ -2863,7 +2870,7 @@ c4 ?- call((!;1)).
2863
2870
  const result = spawnSync(process.execPath, ['--input-type=module', '--eval', script], {
2864
2871
  cwd: packageRoot,
2865
2872
  encoding: 'utf8',
2866
- timeout: 5000,
2873
+ timeout: DCG_HANDOFF_TEST_TIMEOUT_MS,
2867
2874
  });
2868
2875
  if (result.error) throw result.error;
2869
2876
  assertEqual(result.status, 0, `65536-cell hand-off status; stderr=${result.stderr}`);
@@ -2906,7 +2913,7 @@ c4 ?- call((!;1)).
2906
2913
  'length(_,E),E>12,N is 2^E,\\+ \\+ (length(L,N),time(phrase(a,L))).\n' +
2907
2914
  ';\n;\n;\n\n' +
2908
2915
  'halt.\n',
2909
- timeout: 5000,
2916
+ timeout: DCG_HANDOFF_TEST_TIMEOUT_MS,
2910
2917
  });
2911
2918
  if (result.error) throw result.error;
2912
2919
  assertEqual(result.status, 0, `deep REPL hand-off status; stderr=${result.stderr}`);
@@ -3842,6 +3849,39 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
3842
3849
  assertEqual(result.stderr, '', 'stderr');
3843
3850
  },
3844
3851
  },
3852
+
3853
+ {
3854
+ name: '--proof-detail expanded exposes bundled Prolog library clauses',
3855
+ run: () => {
3856
+ const input = '%% goal: q(a)\n:- use_module(library(lists)).\nq(X) :- member(X, [a,b]).\n';
3857
+ const result = runCli(['--proof-detail', 'expanded', '-'], { input });
3858
+ assertEqual(result.status, 0, 'exit status');
3859
+ assertIncludes(result.stdout, 'by(fact("src/lib/lists.pl"', 'expanded library source');
3860
+ assertNotIncludes(result.stdout, 'by(library(member, 2))', 'abstract library boundary');
3861
+ },
3862
+ },
3863
+ {
3864
+ name: '--verify-proof accepts saved why/2 proof output and rejects tampering',
3865
+ run: () => {
3866
+ const programFile = path.join(tmp, `proof-program-${++tmpCounter}.pl`);
3867
+ const proofFile = path.join(tmp, `proof-certificate-${++tmpCounter}.pl`);
3868
+ fs.writeFileSync(programFile, '%% goal: q(a)\np(a).\nq(X) :- p(X).\n');
3869
+ const generated = runCli(['--proof', programFile]);
3870
+ assertEqual(generated.status, 0, 'proof generation status');
3871
+ fs.writeFileSync(proofFile, generated.stdout);
3872
+ const verified = runCli(['--verify-proof', proofFile, programFile]);
3873
+ assertEqual(verified.status, 0, 'verification status');
3874
+ assertEqual(verified.stdout, 'verified 1 proof certificate.\n', 'verification stdout');
3875
+ const strictVerified = runCli(['--iso-strict', '--verify-proof', proofFile, programFile]);
3876
+ assertEqual(strictVerified.status, 0, 'strict verification status');
3877
+ assertEqual(strictVerified.stdout, 'verified 1 proof certificate.\n', 'strict verification stdout');
3878
+ const tamperedFile = path.join(tmp, `proof-certificate-bad-${++tmpCounter}.pl`);
3879
+ fs.writeFileSync(tamperedFile, generated.stdout.replace('goal(p(a))', 'goal(p(b))'));
3880
+ const rejected = runCli(['--verify-proof', tamperedFile, programFile]);
3881
+ assertEqual(rejected.status, 1, 'tampered verification status');
3882
+ assertIncludes(rejected.stderr, 'proof certificate 1 failed verification', 'tampered verification stderr');
3883
+ },
3884
+ },
3845
3885
  {
3846
3886
  name: '-pw combines proof and warning flags',
3847
3887
  run: () => {
@@ -5347,11 +5387,12 @@ ${profile}`;
5347
5387
  assertEqual(report.total.positive + report.total.errors + report.total.warnings + report.total.proofs, report.total.total, 'conformance total');
5348
5388
  assertEqual(report.rows.some((row) => row.category === 'legacy-numbered'), false, 'legacy-numbered category');
5349
5389
  const wg17 = report.executable.find((gate) => gate.name === 'WG17 syntax');
5350
- assertEqual(wg17?.total, 372, 'WG17 executable total');
5351
- assertEqual(wg17?.passed, 372, 'WG17 executable passed');
5390
+ const wg17FixtureTotal = readWg17SyntaxFixture().cases.length;
5391
+ assertEqual(wg17?.total, wg17FixtureTotal, 'WG17 executable total');
5392
+ assertEqual(wg17?.passed, wg17FixtureTotal, 'WG17 executable passed');
5352
5393
  assertArrayEqual(wg17?.failures ?? [], [], 'WG17 executable failures');
5353
5394
  const text = formatConformanceReport(report);
5354
- assertIncludes(text, '| WG17 syntax | 372 | 372 | pass |', 'report');
5395
+ assertIncludes(text, `| WG17 syntax | ${wg17FixtureTotal} | ${wg17FixtureTotal} | pass |`, 'report');
5355
5396
  assertIncludes(text, '| variables |', 'report');
5356
5397
  assertIncludes(text, '| Proofs |', 'report');
5357
5398
  assertIncludes(text, '| **Total** |', 'report');
@@ -5624,6 +5665,74 @@ true :+ ready.
5624
5665
  },
5625
5666
  },
5626
5667
 
5668
+ {
5669
+ name: 'proof certificates verify source derivations without proof search',
5670
+ run: () => {
5671
+ const program = Program.parse('p(a, b).\nq(X, Y) :- p(X, Y).\n', { sourceMetadata: true });
5672
+ const result = proofCertificate(program, parseGoalText('q(a, b)'));
5673
+ assertEqual(result.ok, true, 'certificate generated');
5674
+ assertEqual(result.certificate.version, 1, 'certificate version');
5675
+ assertEqual(result.certificate.answer, 'q(a, b)', 'certificate answer');
5676
+ const checked = verifyProof(program, result);
5677
+ assertEqual(checked.ok, true, 'certificate verifies');
5678
+ assertEqual(checked.trusted.length, 0, 'source-only certificate has no trusted boundaries');
5679
+ },
5680
+ },
5681
+ {
5682
+ name: 'proof verification rejects a tampered child goal',
5683
+ run: () => {
5684
+ const program = Program.parse('p(a, b).\nq(X, Y) :- p(X, Y).\n', { sourceMetadata: true });
5685
+ const result = proofCertificate(program, parseGoalText('q(a, b)'));
5686
+ const tampered = structuredClone(result.certificate);
5687
+ tampered.proof.children[0].goal = 'p(a, c)';
5688
+ assertEqual(verifyProof(program, tampered).ok, false, 'tampered child is rejected');
5689
+ const bindingTamper = structuredClone(result.certificate);
5690
+ bindingTamper.proof.bindings[1].value = 'c';
5691
+ assertEqual(verifyProof(program, bindingTamper).ok, false, 'tampered binding is rejected');
5692
+ const changedProgram = Program.parse('p(a, c).\nq(X, Y) :- p(X, Y).\n', { sourceMetadata: true });
5693
+ assertEqual(verifyProof(changedProgram, result).ok, false, 'changed source program is rejected');
5694
+ },
5695
+ },
5696
+ {
5697
+ name: 'why/2 text round-trips into a verifiable proof certificate',
5698
+ run: () => {
5699
+ const program = Program.parse('p(a).\nq(X) :- p(X).\n', { sourceMetadata: true });
5700
+ const result = proofCertificate(program, parseGoalText('q(a)'));
5701
+ const parsed = proofCertificatesFromText(result.text, program);
5702
+ assertEqual(parsed.length, 1, 'certificate count');
5703
+ assertEqual(verifyProof(program, parsed[0]).ok, true, 'round-tripped certificate verifies');
5704
+ },
5705
+ },
5706
+ {
5707
+ name: 'proof certificate text round-trips under every double_quotes mode',
5708
+ run: () => {
5709
+ for (const doubleQuotes of ['chars', 'codes', 'atom']) {
5710
+ const program = Program.parse('p(a).\nq(X) :- p(X).\n', { sourceMetadata: true, doubleQuotes });
5711
+ const result = proofCertificate(program, parseGoalText('q(a)', { doubleQuotes }));
5712
+ const parsed = proofCertificatesFromText(result.text, program);
5713
+ assertEqual(parsed.length, 1, `${doubleQuotes} certificate count`);
5714
+ assertEqual(verifyProof(program, parsed[0]).ok, true, `${doubleQuotes} certificate verifies`);
5715
+ }
5716
+ },
5717
+ },
5718
+ {
5719
+ name: 'expanded proof detail opens bundled Prolog library clauses',
5720
+ run: () => {
5721
+ const program = Program.parse(':- use_module(library(lists)).\nq(X) :- member(X, [a,b]).\n', { sourceMetadata: true });
5722
+ const abstract = proofCertificate(program, parseGoalText('q(a)'));
5723
+ const expanded = proofCertificate(program, parseGoalText('q(a)'), { proofDetail: 'expanded' });
5724
+ assertEqual(abstract.certificate.proof.children[0].method.type, 'library', 'abstract library boundary');
5725
+ assertEqual(expanded.certificate.proof.children[0].method.type, 'source', 'expanded library source');
5726
+ const abstractChecked = verifyProof(program, abstract);
5727
+ const expandedChecked = verifyProof(program, expanded);
5728
+ assertEqual(abstractChecked.ok, true, 'abstract certificate verifies');
5729
+ assertEqual(abstractChecked.trusted.length, 1, 'abstract certificate reports library trust');
5730
+ assertEqual(abstractChecked.trusted[0].type, 'library', 'reported trust boundary type');
5731
+ assertEqual(expandedChecked.ok, true, 'expanded certificate verifies');
5732
+ assertEqual(expandedChecked.trusted.length, 0, 'pure-Prolog expanded certificate removes library trust');
5733
+ },
5734
+ },
5735
+
5627
5736
  {
5628
5737
  name: 'run accepts Program instances',
5629
5738
  run: () => {
@@ -7171,6 +7280,92 @@ function whiteBoxCases() {
7171
7280
  'double_quotes(codes) representation');
7172
7281
  },
7173
7282
  },
7283
+ {
7284
+ name: 'double-quoted character and code suffixes are recognized inside proper lists (issue #88 suffix follow-up)',
7285
+ run: () => {
7286
+ const repl = runCli([], {
7287
+ input: 'T = [A,b,c,d,e,f].\nhalt.\n',
7288
+ });
7289
+ assertEqual(repl.status, 0, 'REPL exit status');
7290
+ assertIncludes(repl.stdout, 'T = [A|"bcdef"].', 'Scryer-compatible character suffix');
7291
+ assertNotIncludes(repl.stdout, 'T = [A, b, c, d, e, f].', 'expanded suffix suppressed');
7292
+
7293
+ const chars = parseGoalText('p([foo,b,c])').args[0];
7294
+ assertEqual(
7295
+ formatTermForWrite(chars, new Env(), {
7296
+ quoted: true,
7297
+ doubleQuotes: 'chars',
7298
+ doubleBar: true,
7299
+ compact: true,
7300
+ operators: ISO_OPERATOR_DEFINITIONS,
7301
+ }),
7302
+ '[foo|"bc"]',
7303
+ 'ordinary list notation uses a quoted suffix',
7304
+ );
7305
+ assertEqual(
7306
+ formatTermForWrite(chars, new Env(), {
7307
+ quoted: true,
7308
+ ignoreOps: true,
7309
+ doubleQuotes: 'chars',
7310
+ doubleBar: true,
7311
+ compact: true,
7312
+ operators: ISO_OPERATOR_DEFINITIONS,
7313
+ }),
7314
+ `'.'(foo,"bc")`,
7315
+ 'ignore_ops remains orthogonal to suffix quoting',
7316
+ );
7317
+ assertEqual(
7318
+ formatTermForWrite(chars, new Env(), {
7319
+ quoted: true,
7320
+ doubleQuotes: null,
7321
+ doubleBar: true,
7322
+ compact: true,
7323
+ operators: ISO_OPERATOR_DEFINITIONS,
7324
+ }),
7325
+ '[foo,b,c]',
7326
+ 'disabled double_quotes keeps expanded list notation',
7327
+ );
7328
+ const improper = parseGoalText('p([foo,b,c|tail])').args[0];
7329
+ assertEqual(
7330
+ formatTermForWrite(improper, new Env(), {
7331
+ quoted: true,
7332
+ doubleQuotes: 'chars',
7333
+ doubleBar: true,
7334
+ compact: true,
7335
+ operators: ISO_OPERATOR_DEFINITIONS,
7336
+ }),
7337
+ '[foo,b,c|tail]',
7338
+ 'improper suffix is not mistaken for a proper string suffix',
7339
+ );
7340
+
7341
+ const astral = parseGoalText("p([foo,'😀',x])").args[0];
7342
+ assertEqual(
7343
+ formatTermForWrite(astral, new Env(), {
7344
+ quoted: true,
7345
+ doubleQuotes: 'chars',
7346
+ doubleBar: true,
7347
+ compact: true,
7348
+ operators: ISO_OPERATOR_DEFINITIONS,
7349
+ }),
7350
+ '[foo|"😀x"]',
7351
+ 'Unicode scalar suffix',
7352
+ );
7353
+
7354
+ const codes = parseGoalText('p([foo,98,128512])', { doubleQuotes: 'codes' }).args[0];
7355
+ assertEqual(
7356
+ formatTermForWrite(codes, new Env(), {
7357
+ quoted: true,
7358
+ doubleQuotes: 'codes',
7359
+ doubleBar: true,
7360
+ compact: true,
7361
+ operators: ISO_OPERATOR_DEFINITIONS,
7362
+ }),
7363
+ '[foo|"b😀"]',
7364
+ 'code-list suffix',
7365
+ );
7366
+ },
7367
+ },
7368
+
7174
7369
  {
7175
7370
  name: 'charsio write_term_to_chars composes double_quotes with ignore_ops',
7176
7371
  run: () => {
@@ -8148,6 +8343,7 @@ function documentedConformanceMetricIssues() {
8148
8343
  const report = buildConformanceReport();
8149
8344
  const iso = report.rows.find((row) => row.category === 'iso')?.total;
8150
8345
  const total = report.total.total;
8346
+ const wg17 = report.executable.find((gate) => gate.name === 'WG17 syntax')?.total;
8151
8347
  const checks = [
8152
8348
  {
8153
8349
  file: path.join(packageRoot, 'the-art-of-eyeprolog.md'),
@@ -8177,6 +8373,21 @@ function documentedConformanceMetricIssues() {
8177
8373
  }
8178
8374
  }
8179
8375
  }
8376
+ for (const file of [
8377
+ path.join(packageRoot, 'test', 'conformance', 'README.md'),
8378
+ path.join(packageRoot, 'test', 'conformance', 'ISO-COMPLIANCE.md'),
8379
+ ]) {
8380
+ const relative = path.relative(packageRoot, file);
8381
+ const text = fs.readFileSync(file, 'utf8');
8382
+ const claims = [
8383
+ ...[...text.matchAll(/\b(\d+)-case[^\n|]*WG17/g)].map((match) => Number(match[1])),
8384
+ ...[...text.matchAll(/WG17[^\n|]*?\b(\d+) executable/g)].map((match) => Number(match[1])),
8385
+ ];
8386
+ if (claims.length === 0) issues.push(`${relative}: WG17 total not found`);
8387
+ for (const claim of claims) {
8388
+ if (claim !== wg17) issues.push(`${relative}: WG17 count ${claim} != ${wg17}`);
8389
+ }
8390
+ }
8180
8391
  return issues.sort();
8181
8392
  }
8182
8393
 
@@ -1467,25 +1467,42 @@ why(
1467
1467
  ).
1468
1468
  ```
1469
1469
 
1470
- Proof output is valid EyeProlog input:
1470
+ Proof output is valid EyeProlog input and can be kept as a proof certificate:
1471
1471
 
1472
1472
  ```sh
1473
1473
  eyeprolog --proof examples/socrates.pl > socrates.why.pl
1474
+ eyeprolog --verify-proof socrates.why.pl examples/socrates.pl
1474
1475
  ```
1475
1476
 
1477
+ The second command checks the supplied `why/2` derivation against the program;
1478
+ it does not search again for a proof. A changed source clause, child goal, source
1479
+ location, or recorded substitution makes the certificate fail verification.
1480
+ Certificate input is parsed as Prolog data rather than loaded as a program, so
1481
+ its terms cannot trigger directives while being checked.
1482
+
1476
1483
  A normal answer is one resolved ground term followed by a period. Strings,
1477
1484
  quoted atoms, lists, and compounds are rendered in supported source syntax so
1478
1485
  the output can be read back. Enabling `--proof`, `--warnings`, or `--stats`
1479
1486
  must not change which answers are found.
1480
1487
 
1481
- The second argument of `why/2` is an abstract proof term of the general shape
1488
+ The second argument of `why/2` is a proof term of the general shape
1482
1489
  `proof(goal(G), by(Method), bindings(Bindings), uses(Proofs))`. User clauses
1483
1490
  are identified as `fact(Filename, clause(N))` or
1484
1491
  `rule(Filename, clause(N))`, with one-based source clause numbers. Built-ins
1485
- are identified as `builtin(Name, Arity)`. Explanation data is outside the
1492
+ are identified as `builtin(Name, Arity)`. By default, bundled Prolog-library
1493
+ predicates appear as `library(Name, Arity)` trusted boundaries. Use
1494
+ `--proof-detail expanded` to replace those boundaries with the library source
1495
+ clauses and any trusted built-ins they call. Explanation data is outside the
1486
1496
  logical semantics of the input program: it describes the derivation but does
1487
1497
  not participate in finding it.
1488
1498
 
1499
+ Verification checks source steps structurally: the named clause must exist, its
1500
+ head must unify with the certified goal, its body must correspond to the child
1501
+ proofs, and recorded clause-variable bindings must agree with that derivation.
1502
+ Built-ins and abstract library nodes are deliberately trusted boundaries. This
1503
+ separates proof discovery from proof checking without pretending that host
1504
+ operations can be justified by Prolog source that does not exist.
1505
+
1489
1506
  A second program can query `why/2`. Read a proof as an argument. If it contains
1490
1507
  irrelevant detours, improve the helpers. If a key premise is hidden inside an
1491
1508
  opaque value, model it as a fact. Designing for a good explanation often
@@ -1862,10 +1879,42 @@ prints numeric work counters; those counters describe this run rather than an
1862
1879
  additional logical answer.
1863
1880
 
1864
1881
  `run/2` accepts source text or an already parsed `Program`. Its options include
1865
- `proof` (with `why` and `explain` as aliases), `maxDepth`, `maxInferences`,
1866
- `maxMemoryBytes`, `solutionLimit`, a custom `registry`, and `strictNegation` or
1867
- `analyzeNegation`. It returns `stdout`, the solver's numeric `stats`, and a
1868
- nullable `haltCode`; it does not write to the process streams.
1882
+ `proof` (with `why` and `explain` as aliases), `proofDetail` (`abstract` or
1883
+ `expanded`), `maxDepth`, `maxInferences`, `maxMemoryBytes`, `solutionLimit`, a
1884
+ custom `registry`, and `strictNegation` or `analyzeNegation`. It returns
1885
+ `stdout`, the solver's numeric `stats`, and a nullable `haltCode`; it does not
1886
+ write to the process streams.
1887
+
1888
+ For applications that exchange proofs independently of answer output, the same
1889
+ module exposes `proofCertificate(program, goal, options)`,
1890
+ `proofCertificatesFromText(text, program)`, and
1891
+ `verifyProof(program, certificate, options)`:
1892
+
1893
+ ```js
1894
+ import {
1895
+ Program, parseGoalText,
1896
+ proofCertificate, proofCertificatesFromText, verifyProof
1897
+ } from 'eyeprolog';
1898
+
1899
+ const program = Program.parse(`
1900
+ p(a).
1901
+ q(X) :- p(X).
1902
+ `, { sourceMetadata: true });
1903
+
1904
+ const made = proofCertificate(program, parseGoalText('q(a)'));
1905
+ console.log(verifyProof(program, made).ok); // true
1906
+
1907
+ const received = proofCertificatesFromText(made.text, program)[0];
1908
+ console.log(verifyProof(program, received).ok); // true
1909
+ ```
1910
+
1911
+ `proofCertificate` returns both the ordinary `why/2` text and a JSON-serializable
1912
+ certificate object. `verifyProof` walks the supplied certificate rather than
1913
+ asking the solver to find another proof; its `trusted` array lists every builtin
1914
+ or abstract-library boundary that was assumed while checking it. Passing
1915
+ `proofDetail: 'expanded'` exposes bundled Prolog-library clauses in the
1916
+ certificate and therefore reduces library-level trust boundaries to the
1917
+ built-ins those clauses ultimately use.
1869
1918
 
1870
1919
  When `run` receives an already parsed `Program`, bundled-library imports
1871
1920
  needed only by its host-supplied goals are added to that Program before solving,
@@ -5480,9 +5529,11 @@ still retained where adjacent graphic tokens would otherwise merge, as in
5480
5529
  `write_term/2-3` supports `quoted/1`, `ignore_ops/1`, `numbervars/1`, and
5481
5530
  `variable_names/1`. Normal mode additionally accepts the EyeProlog extension
5482
5531
  `double_quotes(true|false)`: `true` lets eligible character/code lists use the
5483
- current `double_quotes` representation. Proper lists can therefore be written
5484
- as `"text"`, while a partial list such as `[a,b|Tail]` is written as
5485
- `"ab"||Tail`. This representation choice is independent of `ignore_ops/1`: with
5532
+ current `double_quotes` representation. Proper character/code lists can therefore be written
5533
+ as `"text"`. A proper list whose final segment contains at least two characters/codes
5534
+ uses that representation for the suffix, so `[A,b,c,d,e,f]` is written as
5535
+ `[A|"bcdef"]`. A partial list such as `[a,b|Tail]` is written as `"ab"||Tail`.
5536
+ This representation choice is independent of `ignore_ops/1`: with
5486
5537
  `ignore_ops(true)`, operator terms use functional notation while an explicitly
5487
5538
  requested character/code list remains double quoted. Thus
5488
5539
  `write_term(f("ab",a+b),[quoted(true),ignore_ops(true),double_quotes(true)])`
@@ -9334,6 +9385,8 @@ make the observed question explicit.
9334
9385
  | --- | --- |
9335
9386
  | `-h`, `--help` | Show usage |
9336
9387
  | `-p`, `--proof` | Print `why/2` explanations |
9388
+ | `--proof-detail abstract|expanded` | Select library abstraction for proof output; implies `--proof` |
9389
+ | `--verify-proof File` | Verify saved `why/2` proof certificates against the input program without proof search |
9337
9390
  | `-q`, `--quads` | Run embedded quad tests and fail if any do not hold |
9338
9391
  | `--iso-strict` | Restrict parsing and execution to ISO/IEC 13211-1:1995 + Corrigenda 1–3; reject EyeProlog language extensions (including `table` and `:+`) and disable bundled-library autoloading |
9339
9392
  | `--portable` | Enforce the conservative EyeProlog/Trealla/Scryer interoperability profile |
@@ -9378,7 +9431,9 @@ Work in a fixed sequence:
9378
9431
 
9379
9432
  1. predict the ground answers before running the program;
9380
9433
  2. run without observation flags and compare stdout with that prediction;
9381
- 3. add `--proof` when the support for an answer is the question;
9434
+ 3. add `--proof` when the support for an answer is the question; save the
9435
+ output and use `--verify-proof` when the derivation itself must cross a
9436
+ process or review boundary;
9382
9437
  4. add `--warnings` when portability or negative dependencies are the
9383
9438
  question; use `--portable` when non-profile dependencies must fail CI;
9384
9439
  5. add `--stats` only when comparing two executions of the same semantic case.
@@ -9388,6 +9443,8 @@ For example:
9388
9443
  ```sh
9389
9444
  eyeprolog --goal 'ancestor(X, Y)' examples/ancestor.pl
9390
9445
  eyeprolog --proof --goal 'type(X, Y)' examples/socrates.pl
9446
+ eyeprolog --proof examples/socrates.pl > socrates.why.pl
9447
+ eyeprolog --verify-proof socrates.why.pl examples/socrates.pl
9391
9448
  eyeprolog --warnings --goal 'answer(X)' test/conformance/warnings/negation/unstratified_mutual.pl
9392
9449
  eyeprolog --portable --goal 'sudoku9_solution(S)' examples/clpz-sudoku-9x9.pl
9393
9450
  eyeprolog --stats --goal 'path(a, X)' examples/path-discovery.pl > answers.pl 2> run.stats
@@ -11,6 +11,11 @@ const syntaxSource = 'https://www.complang.tuwien.ac.at/ulrich/iso-prolog/confor
11
11
  const syntaxFixturePath = path.join(packageRoot, 'test', 'conformance', 'wg17-syntax-cases.json');
12
12
  const syntaxCoveragePath = path.join(packageRoot, 'test', 'conformance', 'wg17-syntax-coverage.json');
13
13
  const syntaxStatusPath = path.join(packageRoot, 'test', 'conformance', 'WG17-SYNTAX-STATUS.md');
14
+ const conformanceReportPath = path.join(packageRoot, 'conformance-report.md');
15
+ const countDocumentationPaths = [
16
+ path.join(packageRoot, 'test', 'conformance', 'README.md'),
17
+ path.join(packageRoot, 'test', 'conformance', 'ISO-COMPLIANCE.md'),
18
+ ];
14
19
 
15
20
  const namedEntities = new Map([
16
21
  ['amp', '&'], ['lt', '<'], ['gt', '>'], ['quot', '"'], ['apos', "'"],
@@ -318,6 +323,20 @@ function formatIdList(ids) {
318
323
  return ids.length === 0 ? 'none' : ids.map((id) => `#${id}`).join(', ');
319
324
  }
320
325
 
326
+ function updateDocumentedInventoryCount(previousCount, nextCount) {
327
+ if (previousCount === nextCount) return;
328
+ for (const filename of countDocumentationPaths) {
329
+ const original = fs.readFileSync(filename, 'utf8');
330
+ const updated = original
331
+ .replaceAll(`${previousCount}-case`, `${nextCount}-case`)
332
+ .replaceAll(`WG17 matrix has ${previousCount} executable`, `WG17 matrix has ${nextCount} executable`);
333
+ if (updated === original) {
334
+ throw new Error(`WG17 inventory count was not found in ${path.relative(packageRoot, filename)}`);
335
+ }
336
+ fs.writeFileSync(filename, updated);
337
+ }
338
+ }
339
+
321
340
  async function readSource(source) {
322
341
  if (/^https?:\/\//i.test(source)) {
323
342
  const response = await fetch(source, {
@@ -415,6 +434,13 @@ export async function upgradeWg17({ check = false, source = syntaxSource } = {})
415
434
  // Generate status after the fixture/manifest are synchronized.
416
435
  const { renderWg17SyntaxStatus } = await import('./report-wg17-syntax-coverage.mjs');
417
436
  fs.writeFileSync(syntaxStatusPath, renderWg17SyntaxStatus());
437
+ updateDocumentedInventoryCount(previous.cases.length, fixture.cases.length);
438
+
439
+ // The public report executes the refreshed fixture, so keep its measured row
440
+ // synchronized as part of the same explicit upgrade operation.
441
+ const { buildConformanceReport, formatConformanceReport } = await import('../test/run-conformance-report.mjs');
442
+ const conformanceReport = buildConformanceReport();
443
+ fs.writeFileSync(conformanceReportPath, formatConformanceReport(conformanceReport));
418
444
 
419
445
  const upstreamAssertions = fixture.cases
420
446
  .filter((item) => item.outcome == null)
package/why-eyeprolog.md CHANGED
@@ -203,9 +203,20 @@ make decisions, combine data from several sources, or need to be reviewed by
203
203
  someone who did not write them.
204
204
 
205
205
  Proofs make successful reasoning easier to inspect, test, teach, and discuss.
206
- They do not authenticate source data or replace application security. Embedders
207
- remain responsible for validating inputs and imposing suitable time, memory,
208
- depth, and solution limits.
206
+ EyeProlog also treats a successful `why/2` term as a portable proof certificate.
207
+ A certificate can be saved, transmitted as ordinary Prolog data, and checked later
208
+ against the program without repeating the search that found the answer. Source
209
+ steps are checked against the named clauses and their substitutions. Built-ins
210
+ and abstract library steps remain explicit trusted boundaries; expanded proof
211
+ detail opens bundled Prolog-library clauses so that more of the derivation can
212
+ be checked from source. The verification result enumerates the remaining trusted
213
+ boundaries rather than folding them into an undifferentiated success result.
214
+
215
+ Verification and discovery therefore have different jobs: solving searches for
216
+ a derivation, while certificate verification checks a supplied derivation. The
217
+ certificate does not authenticate source data or replace application security.
218
+ Embedders remain responsible for validating inputs and imposing suitable time,
219
+ memory, depth, and solution limits.
209
220
 
210
221
  ## One engine across JavaScript environments
211
222