eyemind 0.1.24 → 0.1.26
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 +5 -4
- package/SPEC.md +118 -19
- package/package.json +1 -1
- package/src/cli.js +23 -8
- package/src/engine.js +9 -4
- package/src/goals.js +7 -4
- package/src/lang.js +8 -0
- package/src/program.js +10 -6
- package/src/proof.js +30 -4
- package/src/templates.js +91 -0
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -15,10 +15,10 @@ JavaScript library, and a browser application, with no dependencies.
|
|
|
15
15
|
write rules, check proofs, use the library, and develop eyemind.
|
|
16
16
|
- **[Specification](https://eyereasoner.github.io/eyemind/SPEC)**: the
|
|
17
17
|
language, its canonical text, the proof format, and the C1–C8 checks.
|
|
18
|
-
- **[Examples](examples/)**:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- **[Conformance suite](conformance/)**:
|
|
18
|
+
- **[Examples](https://eyereasoner.github.io/eyemind/examples/)**:
|
|
19
|
+
93 runnable programs, each with a card deck and saved conclusions, proofs
|
|
20
|
+
and check reports.
|
|
21
|
+
- **[Conformance suite](conformance/)**: 372 cases that test any
|
|
22
22
|
implementation of the specification through its command line.
|
|
23
23
|
|
|
24
24
|
```js
|
|
@@ -30,6 +30,7 @@ query(type(X, Y))
|
|
|
30
30
|
|
|
31
31
|
```sh
|
|
32
32
|
node bin/eyemind.js examples/socrates.js
|
|
33
|
+
node bin/eyemind.js -c examples/socrates.js
|
|
33
34
|
node bin/eyemind.js --proof examples/socrates.js > /tmp/proof.js
|
|
34
35
|
node bin/eyemind.js --check-proof /tmp/proof.js examples/socrates.js
|
|
35
36
|
```
|
package/SPEC.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
```text
|
|
4
4
|
Title: The eyemind Rule Language and Proof Format
|
|
5
|
-
Version: eyemind 0.1.
|
|
5
|
+
Version: eyemind 0.1.26
|
|
6
6
|
Status: Informational
|
|
7
7
|
Author: Jos De Roo, KNoWS office of IDLab, Ghent University - imec
|
|
8
8
|
Repository: https://github.com/eyereasoner/eyemind
|
|
@@ -22,7 +22,7 @@ command line.
|
|
|
22
22
|
## Status of This Memo
|
|
23
23
|
|
|
24
24
|
This document is not an Internet Standards Track specification. It describes
|
|
25
|
-
the language and formats as implemented by eyemind 0.1.
|
|
25
|
+
the language and formats as implemented by eyemind 0.1.26, for readers who want
|
|
26
26
|
to write programs, produce or consume proofs and reports, or implement a
|
|
27
27
|
compatible reasoner or checker. Where this document and the implementation
|
|
28
28
|
disagree, that is a defect in one of them.
|
|
@@ -145,7 +145,7 @@ loaded as one program; their clauses are then numbered in the order the
|
|
|
145
145
|
scripts are run.
|
|
146
146
|
|
|
147
147
|
A program is given exactly the names needed to state it: the statements of
|
|
148
|
-
Section 4.2, `preds`, `vars`, `struct`, the controls and primitives of
|
|
148
|
+
Section 4.2, `forward`, `backward`, `preds`, `vars`, `struct`, the controls and primitives of
|
|
149
149
|
Section 5, the atom `fail`, and `eyemind`, the library, with which a program
|
|
150
150
|
can build and run another program and read and write documents
|
|
151
151
|
(`eyemind.build`, `eyemind.run`, `eyemind.checkProof`, `eyemind.checkReport`,
|
|
@@ -180,7 +180,8 @@ statements) would run as JavaScript code.
|
|
|
180
180
|
A clause MUST be rejected when:
|
|
181
181
|
|
|
182
182
|
- a head is not an atom or a compound;
|
|
183
|
-
- a head is `step/4` or `clause/2`, which are reserved for proof documents
|
|
183
|
+
- a head is `step/4` or `clause/2`, which are reserved for proof documents,
|
|
184
|
+
or explicitly states `activated/2`, reserved for template activations;
|
|
184
185
|
- a head is a primitive (Section 5.2) or a control (Section 5.1), except the
|
|
185
186
|
head `'true'` of a query and `'false'` of a contradiction; in particular a
|
|
186
187
|
conclusion of `implies` is never `'true'` or `'false'`;
|
|
@@ -230,7 +231,8 @@ Section 5.1 treats as anonymous.
|
|
|
230
231
|
|
|
231
232
|
In a goal position, an array is the conjunction of its items: a statement's
|
|
232
233
|
goal, the argument of `and`, `or`, `not`, `call` and `once`, and the goal of
|
|
233
|
-
`findall
|
|
234
|
+
`findall`, and either part of a rule template (Section 4.6). Anywhere else an
|
|
235
|
+
array is a list.
|
|
234
236
|
|
|
235
237
|
Operators do not build terms. Variables, compounds and predicates have no
|
|
236
238
|
JavaScript primitive value, so an operator applied to one, such as `X + 1` or
|
|
@@ -258,6 +260,55 @@ A program MUST be rejected when:
|
|
|
258
260
|
Because dependencies compare whole terms, two relations sharing a predicate
|
|
259
261
|
name MAY have different ranks.
|
|
260
262
|
|
|
263
|
+
### 4.6 Inline rule templates
|
|
264
|
+
|
|
265
|
+
`forward(Body, Head)` and `backward(Head, Body)` construct rule templates.
|
|
266
|
+
They MUST occur as top-level conjuncts of an `implies` conclusion. The
|
|
267
|
+
conclusion MAY combine ordinary heads and several templates. Each template's
|
|
268
|
+
body and head MAY themselves be conjunctions, written as arrays. A backward
|
|
269
|
+
template with several heads defines one backward clause per head, sharing
|
|
270
|
+
the same activation and body. Empty and open lists are not goals or heads.
|
|
271
|
+
|
|
272
|
+
A template variable is **captured** when it also occurs in the generating
|
|
273
|
+
clause's body or in an ordinary sibling head. The other variables are local
|
|
274
|
+
to that template: they are universally quantified within the generated rule,
|
|
275
|
+
except for head-only variables of a forward template, which receive witnesses
|
|
276
|
+
as Section 7.2 describes. Variables shared only between sibling templates are
|
|
277
|
+
local to each. Every call in a template body, including calls inside controls,
|
|
278
|
+
MUST be statically named; a variable in a term's argument is permitted, but a
|
|
279
|
+
variable used as a goal is not. A forward template MAY conclude further inline
|
|
280
|
+
templates. A backward template MUST NOT conclude templates.
|
|
281
|
+
|
|
282
|
+
Templates are lowered before validation, indexing and stratification. Starting
|
|
283
|
+
with all stated clauses, in clause order, replace each template conjunct,
|
|
284
|
+
left to right, with `activated(N, Captures)`. `Captures` is a list of its
|
|
285
|
+
captured variables in order of first occurrence in the template as written;
|
|
286
|
+
`N` is the number of the first clause appended for it. Append a forward or
|
|
287
|
+
backward clause as specified by the template, with the activation as its first
|
|
288
|
+
body goal, followed by the template's body goals. A forward template keeps its
|
|
289
|
+
head conjunction; a backward template appends one clause per head in order.
|
|
290
|
+
Continue processing the appended clauses in order, lowering any nested
|
|
291
|
+
templates the same way. Original clause numbers are kept; appended clauses
|
|
292
|
+
are numbered consecutively after them and inherit the generator's source line.
|
|
293
|
+
Capture metadata belongs to each appended clause, including one whose head
|
|
294
|
+
is subsequently lowered again.
|
|
295
|
+
|
|
296
|
+
For example, `implies(cat(X), forward(dog(Y), test('yes')))` as the only
|
|
297
|
+
stated clause becomes these two clauses:
|
|
298
|
+
|
|
299
|
+
```js
|
|
300
|
+
implies(cat(X), activated(2, []))
|
|
301
|
+
implies([activated(2, []), dog(Y)], test('yes'))
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`Y` stays local to the second clause. Its activation is required even though
|
|
305
|
+
the template does not capture `X`. A generator with several solutions but
|
|
306
|
+
identical captures enables the same rule instance. No arbitrary rule term is
|
|
307
|
+
installed during reasoning. All potential generated clauses and their
|
|
308
|
+
activation dependencies participate in Section 4.5, including clauses whose
|
|
309
|
+
generators never fire. A closed dependency cycle MUST therefore be rejected
|
|
310
|
+
even if its template would remain inactive.
|
|
311
|
+
|
|
261
312
|
## 5. Goals, Controls and Primitives
|
|
262
313
|
|
|
263
314
|
### 5.1 Controls
|
|
@@ -416,6 +467,12 @@ concluded:
|
|
|
416
467
|
When the run asks its own goals (Section 7.4), rules whose heads are all
|
|
417
468
|
`'true'` are not run.
|
|
418
469
|
|
|
470
|
+
For an appended forward clause of Section 4.6, prepend one element to
|
|
471
|
+
`Arguments`: the grounded capture list of its activation. This distinguishes
|
|
472
|
+
witnesses of different captured contexts even when a head does not mention
|
|
473
|
+
those values. An empty capture list still contributes one element, `[]`.
|
|
474
|
+
Original clauses, including generators, use the ordinary witness convention.
|
|
475
|
+
|
|
419
476
|
### 7.3 Bounds
|
|
420
477
|
|
|
421
478
|
A run is bounded by `max_depth`, the depth of nested rule bodies (default
|
|
@@ -442,12 +499,16 @@ A run that concluded `'false'` has halt code 65; otherwise it has none.
|
|
|
442
499
|
|
|
443
500
|
The conclusions of a run are, in order:
|
|
444
501
|
|
|
445
|
-
1. when it halted: every derived fact, in the order derived, ending with
|
|
502
|
+
1. when it halted: every derived fact except template activations, in the order derived, ending with
|
|
446
503
|
`'false'`;
|
|
447
504
|
2. otherwise, when it was given goals: the reported instances of its goals;
|
|
448
505
|
3. otherwise, when the program has a forward rule with a head `'true'`: the
|
|
449
506
|
reported instances, in order of first report, which MAY be none;
|
|
450
|
-
4. otherwise: every derived fact, in the order derived.
|
|
507
|
+
4. otherwise: every derived fact except template activations, in the order derived.
|
|
508
|
+
|
|
509
|
+
An explicit goal MAY ask for an activation. Activations remain available as
|
|
510
|
+
premises and proof steps; the library's `inferred` list omits them. The
|
|
511
|
+
`derived` statistic counts them as facts added during reasoning.
|
|
451
512
|
|
|
452
513
|
The output of a run is the canonical text (Section 8) of each conclusion,
|
|
453
514
|
one per line.
|
|
@@ -575,6 +636,13 @@ array of its body goals when it has several, `Head` likewise the head or the
|
|
|
575
636
|
array of heads, and `G1, ...` are the body goals themselves. Displays are not
|
|
576
637
|
authority: a checker MUST compare each with the program it checks against.
|
|
577
638
|
|
|
639
|
+
For a program with templates, displays show the deterministically lowered
|
|
640
|
+
clauses of Section 4.6, including activation heads and premises. The program
|
|
641
|
+
supplied to the checker MUST be lowered from the source as Section 4.6
|
|
642
|
+
requires; displays cannot authorize extra generated rules. Each use of a
|
|
643
|
+
generated clause requires its activation as the first use, with a justified
|
|
644
|
+
step of its own. All existing checks apply.
|
|
645
|
+
|
|
578
646
|
### 10.3 Steps
|
|
579
647
|
|
|
580
648
|
`Goal` is the justified goal as it stands once the whole solution is found:
|
|
@@ -679,7 +747,7 @@ A boundary that is not refuted remains an obligation (Section 12).
|
|
|
679
747
|
|
|
680
748
|
When goals were given and no claim is `'false'`, the questions are the goals.
|
|
681
749
|
Otherwise the questions are, for each forward rule, each head other than
|
|
682
|
-
`'true'
|
|
750
|
+
`'true'` or a template activation, and the rule's body as a conjunction when one of its heads is
|
|
683
751
|
`'true'` and no claim is `'false'`. Each claim MUST be an instance of a
|
|
684
752
|
question. Each step MUST be reachable from a conjunct of a claim through the
|
|
685
753
|
conjuncts of uses.
|
|
@@ -700,6 +768,11 @@ bound to `skolem(N, Name, Args)`, where:
|
|
|
700
768
|
the heads, that are ground and contain no Skolem term `skolem(N, _, Args)`:
|
|
701
769
|
the values that do contain one hold a witness of this firing.
|
|
702
770
|
|
|
771
|
+
For an appended forward clause of Section 4.6, prepend the capture list,
|
|
772
|
+
instantiated under the step's substitution, as one element of `Args`, as
|
|
773
|
+
required by Section 7.2. The checker MUST compute this from the source
|
|
774
|
+
template's capture metadata and the substitution, not from the witness term.
|
|
775
|
+
|
|
703
776
|
A variable of the head that also occurs in the body is not checked by C8,
|
|
704
777
|
even when the body leaves it unbound; its value comes from the uses, which
|
|
705
778
|
other conditions check. A step citing a rule without such variables is not
|
|
@@ -736,8 +809,9 @@ leave conclusions out, and a run's own proof holds exactly its conclusions.
|
|
|
736
809
|
|
|
737
810
|
## 12. Check Reports
|
|
738
811
|
|
|
739
|
-
A check report is a document (Section 9)
|
|
740
|
-
line
|
|
812
|
+
A check report is a document (Section 9) beginning with the proof's claims,
|
|
813
|
+
in proof order, in canonical text, one per line, followed by a blank line and
|
|
814
|
+
the following report facts, one per line:
|
|
741
815
|
|
|
742
816
|
1. `condition(Id, Name, Outcome, Covered)` for C1 to C8, where `Outcome` is
|
|
743
817
|
`'ok'` or `failed(N)`, `N` the number of that condition's failures;
|
|
@@ -751,6 +825,15 @@ line:
|
|
|
751
825
|
otherwise `'checked_with_obligations'` when there are obligations,
|
|
752
826
|
otherwise `'checked'`.
|
|
753
827
|
|
|
828
|
+
Comments MUST identify these initial terms as checked answers only when the
|
|
829
|
+
report is valid with no obligations. A valid report with obligations MUST
|
|
830
|
+
label them as conditional on those obligations. A failed report MUST label
|
|
831
|
+
them as unverified claims; their inclusion does not validate them. A valid
|
|
832
|
+
report with no claims MUST say it establishes no conclusions. Comments also
|
|
833
|
+
explain the C1-C8 order, failures and obligations when present, the statistics,
|
|
834
|
+
and that the verdict does not establish completeness. Comments are informative
|
|
835
|
+
and MAY be worded differently. They are ignored when the document is read.
|
|
836
|
+
|
|
754
837
|
Failures are listed in the order they are found: those of reading the
|
|
755
838
|
document (Section 11.1), in document order; C4 for each claim, in claim
|
|
756
839
|
order; then for each step, in document order, C4 for its uses and the C1, C3,
|
|
@@ -772,11 +855,13 @@ it differently. eyemind's details are, by condition:
|
|
|
772
855
|
| C7 | `claim answers no goal: ...`, `step serves no claim: ...` |
|
|
773
856
|
| C8 | `not the witness of this firing of clause N: ...` |
|
|
774
857
|
|
|
775
|
-
The
|
|
858
|
+
The initial answers keep the variable names of the proof. The variables of
|
|
859
|
+
each subsequent report fact are renamed `A`, `B`, ..., `Z`, `A1`, ... in order
|
|
776
860
|
of first occurrence. A report is ordinary data: `facts_from` (Section 4.2)
|
|
777
861
|
states it as facts.
|
|
778
862
|
|
|
779
|
-
The JSON form
|
|
863
|
+
The JSON form includes `answers`, an array of the initial claims' canonical
|
|
864
|
+
texts, including unverified claims when `valid` is false. It also has `valid` (boolean), `steps`,
|
|
780
865
|
`claims`, `verified`, `redecided` (the count `recomputed`), `composed`,
|
|
781
866
|
`uses` (the number of uses of all steps, integers), `trusted`
|
|
782
867
|
(a list of `{kind, conclusion}`), `failures` (a list of
|
|
@@ -815,18 +900,22 @@ eyemind [OPTION ...] [FILE ...]
|
|
|
815
900
|
| --- | --- |
|
|
816
901
|
| (none) | Load the files, or standard input, as one program and print its conclusions. |
|
|
817
902
|
| `-p`, `--proof` | Print the proof document instead. |
|
|
818
|
-
| `-c
|
|
819
|
-
| `--
|
|
903
|
+
| `-c`, `--prove-and-check` | Generate a proof, check the rendered proof against the input program, and print its answers and check report. |
|
|
904
|
+
| `--check-proof FILE` | Check the proof in `FILE` (`-` for standard input) against the program and print its answers and report. |
|
|
905
|
+
| `--json` | With either checking mode, print the report, including `answers`, as JSON. |
|
|
820
906
|
| `--goal GOAL` | Ask `GOAL`, read as in Section 9, where an array is a conjunction; MAY be repeated. With `--check-proof`, the goals the proof answers. |
|
|
821
|
-
| `--strict-proof` | With
|
|
907
|
+
| `--strict-proof` | With either checking mode, forbid trusted boundaries. |
|
|
822
908
|
| `--unused` | Print the unused clauses (Section 13). |
|
|
823
909
|
| `--stats` | Print reasoning statistics as JSON to standard error: `inferences`, the goals started, which `--max-inferences` bounds; `rounds`, the forward rounds run over all strata; and `derived`, the facts forward rules added, not counting a conclusion the program states as a fact, a query's answers or `'false'`. |
|
|
824
910
|
| `--max-depth N`, `--max-iterations N`, `--max-inferences N` | The bounds of Section 7.3; `N` MUST be a positive safe integer, at most 9007199254740991. |
|
|
825
911
|
| `-v`, `--version` | Print the version. |
|
|
826
912
|
| `-h`, `--help` | Print the usage, including every option and its short alias where one exists. |
|
|
827
913
|
|
|
828
|
-
`--
|
|
829
|
-
|
|
914
|
+
`--proof`, `--prove-and-check`, `--check-proof` and `--unused` are mutually
|
|
915
|
+
exclusive. `--json` and `--strict-proof` need a checking mode. With
|
|
916
|
+
`--prove-and-check`, reasoning bounds and `--stats` apply to proof generation;
|
|
917
|
+
an invalid check returns 2, otherwise the run's halt code is retained
|
|
918
|
+
(65 for a contradiction, 0 otherwise). A program
|
|
830
919
|
text that has a line starting with `step(` is a proof document; given one as
|
|
831
920
|
a program, the command line MUST say how to check it instead.
|
|
832
921
|
|
|
@@ -868,8 +957,8 @@ conclusions and proof documents in the canonical text, and checks every proof
|
|
|
868
957
|
it produces as Section 11 describes. A conforming **checker** implements
|
|
869
958
|
Sections 9, 11 and 12 and does not depend on a reasoner. For the same
|
|
870
959
|
program, a conforming reasoner and checker MUST produce the same
|
|
871
|
-
conclusions, proof documents and reports as eyemind 0.1.
|
|
872
|
-
except for the `Detail` texts of failures (Section 12), the internal names of
|
|
960
|
+
conclusions, proof documents and reports as eyemind 0.1.26, byte for byte,
|
|
961
|
+
except for comments and the `Detail` texts of failures (Section 12), the internal names of
|
|
873
962
|
variables where Section 3 says so, and where the host's floating-point
|
|
874
963
|
library functions (`exp`, `log`, the trigonometric and hyperbolic functions,
|
|
875
964
|
`pow` of floats) differ in the last digit.
|
|
@@ -945,6 +1034,12 @@ step(subclass_of('human', 'mortal'), clause(2), {}, [])
|
|
|
945
1034
|
Its check report (Section 12):
|
|
946
1035
|
|
|
947
1036
|
```js
|
|
1037
|
+
// Checked answers: these follow from the input program.
|
|
1038
|
+
type('socrates', 'mortal')
|
|
1039
|
+
type('socrates', 'human')
|
|
1040
|
+
|
|
1041
|
+
// C1-C8: resolution, acyclicity, justification, coverage,
|
|
1042
|
+
// re-decision, boundary consistency, relevance and witness integrity.
|
|
948
1043
|
condition('C1', 'resolution', 'ok', 3)
|
|
949
1044
|
condition('C2', 'well_founded', 'ok', 3)
|
|
950
1045
|
condition('C3', 'justification', 'ok', 3)
|
|
@@ -953,11 +1048,15 @@ condition('C5', 're_decision', 'ok', 0)
|
|
|
953
1048
|
condition('C6', 'boundary_consistency', 'ok', 0)
|
|
954
1049
|
condition('C7', 'relevance', 'ok', 5)
|
|
955
1050
|
condition('C8', 'witness_integrity', 'ok', 0)
|
|
1051
|
+
|
|
1052
|
+
// Proof statistics: recorded steps and how they were checked.
|
|
956
1053
|
steps(3)
|
|
957
1054
|
verified(3)
|
|
958
1055
|
recomputed(0)
|
|
959
1056
|
composed(0)
|
|
960
1057
|
trusted(0)
|
|
961
1058
|
claims(2)
|
|
1059
|
+
|
|
1060
|
+
// This verdict applies to the claims above; it does not establish completeness.
|
|
962
1061
|
verdict('checked')
|
|
963
1062
|
```
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -8,7 +8,8 @@ import { VERSION } from './version.js';
|
|
|
8
8
|
export const HELP = `Usage: eyemind [OPTION ...] [FILE ...]
|
|
9
9
|
Programs are JavaScript; forward rules run to a fixpoint.
|
|
10
10
|
-p, --proof Print the claims with clause(...) and step(...) records
|
|
11
|
-
-c, --check
|
|
11
|
+
-c, --prove-and-check Generate a proof and check it against the input program
|
|
12
|
+
--check-proof FILE Print checked answers and a C1-C8 report (- for stdin)
|
|
12
13
|
--json Print the check report as JSON instead
|
|
13
14
|
--goal GOAL Ask a backward goal after forward reasoning; with
|
|
14
15
|
--check-proof, the goal the proof answers
|
|
@@ -46,6 +47,7 @@ export function main(argv, io) {
|
|
|
46
47
|
let strictProof = false;
|
|
47
48
|
let asJson = false;
|
|
48
49
|
let unused = false;
|
|
50
|
+
let proveAndCheck = false;
|
|
49
51
|
for (let i = 0; i < argv.length; i++) {
|
|
50
52
|
const arg = argv[i];
|
|
51
53
|
if (arg === '--help' || arg === '-h') {
|
|
@@ -58,6 +60,8 @@ export function main(argv, io) {
|
|
|
58
60
|
}
|
|
59
61
|
if (arg === '--proof' || arg === '-p') {
|
|
60
62
|
options.proof = true;
|
|
63
|
+
} else if (arg === '--prove-and-check' || arg === '-c') {
|
|
64
|
+
proveAndCheck = true;
|
|
61
65
|
} else if (arg === '--stats') {
|
|
62
66
|
stats = true;
|
|
63
67
|
} else if (arg === '--strict-proof') {
|
|
@@ -66,11 +70,11 @@ export function main(argv, io) {
|
|
|
66
70
|
asJson = true;
|
|
67
71
|
} else if (arg === '--unused') {
|
|
68
72
|
unused = true;
|
|
69
|
-
} else if (arg === '--check-proof' || arg === '
|
|
73
|
+
} else if (arg === '--check-proof' || arg === '--goal' || Object.hasOwn(LIMITS, arg)) {
|
|
70
74
|
i++;
|
|
71
75
|
if (i >= argv.length) throw new UsageError(`${arg} needs a value`);
|
|
72
76
|
const value = argv[i];
|
|
73
|
-
if (arg === '--check-proof'
|
|
77
|
+
if (arg === '--check-proof') {
|
|
74
78
|
proofFile = value;
|
|
75
79
|
} else if (arg === '--goal') {
|
|
76
80
|
goals.push(value);
|
|
@@ -85,11 +89,14 @@ export function main(argv, io) {
|
|
|
85
89
|
files.push(arg);
|
|
86
90
|
}
|
|
87
91
|
}
|
|
88
|
-
if (asJson && proofFile === null) throw new UsageError('--json requires --check-proof');
|
|
89
|
-
if (strictProof && proofFile === null) throw new UsageError('--strict-proof requires --check-proof');
|
|
92
|
+
if (asJson && proofFile === null && !proveAndCheck) throw new UsageError('--json requires --check-proof or --prove-and-check');
|
|
93
|
+
if (strictProof && proofFile === null && !proveAndCheck) throw new UsageError('--strict-proof requires --check-proof or --prove-and-check');
|
|
94
|
+
if (proveAndCheck && (proofFile !== null || options.proof)) {
|
|
95
|
+
throw new UsageError('--prove-and-check cannot be combined with --proof or --check-proof');
|
|
96
|
+
}
|
|
90
97
|
if (proofFile !== null && options.proof) throw new UsageError('--check-proof cannot be combined with --proof');
|
|
91
|
-
if (unused && (proofFile !== null || options.proof)) {
|
|
92
|
-
throw new UsageError('--unused cannot be combined with --proof or --check
|
|
98
|
+
if (unused && (proofFile !== null || options.proof || proveAndCheck)) {
|
|
99
|
+
throw new UsageError('--unused cannot be combined with --proof, --check-proof or --prove-and-check');
|
|
93
100
|
}
|
|
94
101
|
if (proofFile === '-' && (!files.length || files.includes('-'))) {
|
|
95
102
|
throw new UsageError('stdin holds the proof; name the program as files');
|
|
@@ -108,7 +115,15 @@ export function main(argv, io) {
|
|
|
108
115
|
stdout(unusedClauses(program, { goals, ...options }));
|
|
109
116
|
return 0;
|
|
110
117
|
}
|
|
111
|
-
const result = run(program, { goals, ...options });
|
|
118
|
+
const result = run(program, { goals, ...options, proof: options.proof || proveAndCheck });
|
|
119
|
+
if (proveAndCheck) {
|
|
120
|
+
// Check the rendered certificate against the input, just as a saved
|
|
121
|
+
// proof is checked; no second reasoning run supplies missing evidence.
|
|
122
|
+
const report = checkProof(program, result.proof, { goals, allowTrusted: !strictProof });
|
|
123
|
+
stdout(asJson ? `${JSON.stringify(publicReport(report), null, 2)}\n` : checkReport(report));
|
|
124
|
+
if (stats) stderr(`${JSON.stringify(result.stats)}\n`);
|
|
125
|
+
return report.valid ? result.haltCode || 0 : 2;
|
|
126
|
+
}
|
|
112
127
|
stdout(result.stdout);
|
|
113
128
|
if (stats) stderr(`${JSON.stringify(result.stats)}\n`);
|
|
114
129
|
return result.haltCode || 0;
|
package/src/engine.js
CHANGED
|
@@ -4,6 +4,7 @@ import { freshClause, matchHead, renaming, text, variant } from './common.js';
|
|
|
4
4
|
import { readGoal } from './goals.js';
|
|
5
5
|
import { checkRendered, clauseDisplay, renderProof } from './proof.js';
|
|
6
6
|
import { CONTROL_KEYS, Program } from './program.js';
|
|
7
|
+
import { isActivation } from './templates.js';
|
|
7
8
|
import {
|
|
8
9
|
Env, EyemindError, Struct, Var, conjunction, copyResolved, deref, flattenConjunction, freshTerm,
|
|
9
10
|
identity, isCallable, isGround, isGroundButAnonymous, isTerm, key, keyArity, listFromItems, unify,
|
|
@@ -630,7 +631,10 @@ export class Solver {
|
|
|
630
631
|
const resolved = copyResolved(head, answerEnv);
|
|
631
632
|
const unresolved = variables(resolved);
|
|
632
633
|
if (unresolved.size) {
|
|
633
|
-
|
|
634
|
+
// A generated rule also depends on its captures when its head omits
|
|
635
|
+
// them; otherwise two enabled instances could share a witness.
|
|
636
|
+
const context = clause.captures === null ? null : copyResolved(body[0].args[1], answerEnv);
|
|
637
|
+
const witnesses = skolemTerms(clause, head, unresolved, answerEnv, context);
|
|
634
638
|
[...unresolved.values()].forEach((value, i) => unify(value, witnesses[i], answerEnv));
|
|
635
639
|
}
|
|
636
640
|
const conclusions = unresolved.size
|
|
@@ -704,8 +708,8 @@ export class Solver {
|
|
|
704
708
|
// variable and the values the solution gave the heads' other variables. The
|
|
705
709
|
// same activation thus always gives the same terms, and a different one
|
|
706
710
|
// different terms.
|
|
707
|
-
export function skolemTerms(clause, head, unresolved, env) {
|
|
708
|
-
const values = [];
|
|
711
|
+
export function skolemTerms(clause, head, unresolved, env, context = null) {
|
|
712
|
+
const values = context === null ? [] : [context];
|
|
709
713
|
for (const variable of variables(head).values()) {
|
|
710
714
|
const value = copyResolved(variable, env);
|
|
711
715
|
if (isGround(value)) values.push(value);
|
|
@@ -741,7 +745,7 @@ export class Result {
|
|
|
741
745
|
|
|
742
746
|
// The derived facts as text, written only when asked for.
|
|
743
747
|
get inferred() {
|
|
744
|
-
return this.derived.map((node) => text(node.goal));
|
|
748
|
+
return this.derived.filter((node) => !isActivation(node.goal)).map((node) => text(node.goal));
|
|
745
749
|
}
|
|
746
750
|
}
|
|
747
751
|
|
|
@@ -808,6 +812,7 @@ function reason(program, goalInputs, options) {
|
|
|
808
812
|
}
|
|
809
813
|
} else {
|
|
810
814
|
for (const node of solver.derived) {
|
|
815
|
+
if (isActivation(node.goal)) continue;
|
|
811
816
|
roots.push(node);
|
|
812
817
|
claims.push(node.goal);
|
|
813
818
|
}
|
package/src/goals.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
//
|
|
3
3
|
// An array in a goal position is the conjunction of its items: [p(X), q(X)]
|
|
4
4
|
// is and(p(X), q(X)). A goal position is a statement's goal, an argument of
|
|
5
|
-
// and, or, not, call or once,
|
|
6
|
-
// is a list.
|
|
7
|
-
import { EMPTY, Struct, conjunction, properListItems } from './terms.js';
|
|
5
|
+
// and, or, not, call or once, either part of a rule template, or the goal of
|
|
6
|
+
// findall. Anywhere else an array is a list.
|
|
7
|
+
import { EMPTY, Env, Struct, conjunction, properListItems } from './terms.js';
|
|
8
8
|
import { readTerm } from './reader.js';
|
|
9
9
|
|
|
10
|
-
const EMPTY_ENV =
|
|
10
|
+
const EMPTY_ENV = new Env();
|
|
11
11
|
|
|
12
12
|
function isList(term) {
|
|
13
13
|
return term === EMPTY || (term instanceof Struct && term.name === '.' && term.args.length === 2);
|
|
@@ -25,6 +25,9 @@ export function normalizeGoal(term) {
|
|
|
25
25
|
if ((name === 'and' || name === 'or') && args.length === 2) {
|
|
26
26
|
return rebuild(term, args.map(normalizeGoal));
|
|
27
27
|
}
|
|
28
|
+
if ((name === 'forward' || name === 'backward') && args.length === 2) {
|
|
29
|
+
return rebuild(term, args.map(normalizeGoal));
|
|
30
|
+
}
|
|
28
31
|
if ((name === 'not' || name === 'call' || name === 'once') && args.length === 1) {
|
|
29
32
|
return rebuild(term, [normalizeGoal(args[0])]);
|
|
30
33
|
}
|
package/src/lang.js
CHANGED
|
@@ -256,6 +256,14 @@ function exportsFor(io) {
|
|
|
256
256
|
},
|
|
257
257
|
implies: (premise, conclusion) => builder.add('forward', goalOf(conclusion), [goalOf(premise)]),
|
|
258
258
|
implied_by: (conclusion, premise) => builder.add('backward', toTerm(conclusion), [goalOf(premise)]),
|
|
259
|
+
forward: (...args) => {
|
|
260
|
+
if (args.length !== 2) throw new EyemindError(`forward takes 2 arguments, got ${args.length}`);
|
|
261
|
+
return new Struct('forward', args.map(goalOf));
|
|
262
|
+
},
|
|
263
|
+
backward: (...args) => {
|
|
264
|
+
if (args.length !== 2) throw new EyemindError(`backward takes 2 arguments, got ${args.length}`);
|
|
265
|
+
return new Struct('backward', args.map(goalOf));
|
|
266
|
+
},
|
|
259
267
|
query: (...body) => builder.add('query', TRUE_ATOM, body.map(goalOf)),
|
|
260
268
|
contradiction: (...body) => builder.add('contradiction', 'false', body.map(goalOf)),
|
|
261
269
|
facts_from: (source) => {
|
package/src/program.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
// A program: its clauses, validated, indexed and stratified.
|
|
2
2
|
import { PRIMITIVE_KEYS } from './builtins.js';
|
|
3
|
+
import { ACTIVATION_KEY, expandTemplates } from './templates.js';
|
|
3
4
|
import {
|
|
4
5
|
Env, EyemindError, Struct, Var, deref, flattenConjunction, freshTerm, identity, isCallable,
|
|
5
6
|
isGround, isTerm, key, keyArity, unify,
|
|
6
7
|
} from './terms.js';
|
|
7
8
|
|
|
8
9
|
export const CONTROL_KEYS = new Set(['and/2', 'or/2', 'not/1', 'call/1', 'once/1', 'findall/3']);
|
|
9
|
-
export const RESERVED_KEYS = new Set(['step/4', 'clause/2']);
|
|
10
|
+
export const RESERVED_KEYS = new Set(['step/4', 'clause/2', ACTIVATION_KEY, 'forward/2', 'backward/2']);
|
|
10
11
|
|
|
11
12
|
// The statements that state a forward rule, and how an error names them.
|
|
12
13
|
const FORWARD_KINDS = { forward: 'a forward rule', query: 'query()', contradiction: 'contradiction()' };
|
|
13
14
|
const isIndexable = (value) => typeof value === 'string' || typeof value === 'bigint' || typeof value === 'number';
|
|
14
15
|
|
|
15
16
|
export class Clause {
|
|
16
|
-
constructor(id, head, heads, body, forward, line) {
|
|
17
|
+
constructor(id, head, heads, body, forward, line, captures = null) {
|
|
17
18
|
this.id = id;
|
|
18
19
|
this.head = head;
|
|
19
20
|
// Only a forward rule can have several heads, so only it stores them.
|
|
@@ -21,6 +22,7 @@ export class Clause {
|
|
|
21
22
|
this.body = body;
|
|
22
23
|
this.forward = forward;
|
|
23
24
|
this.line = line;
|
|
25
|
+
this.captures = captures;
|
|
24
26
|
this.renaming = null; // how to rename it apart, worked out once it is used often
|
|
25
27
|
this.uses = 0;
|
|
26
28
|
}
|
|
@@ -68,6 +70,8 @@ function pushGoals(goal, out) {
|
|
|
68
70
|
export class Program {
|
|
69
71
|
constructor(sources, without = null) {
|
|
70
72
|
this.sources = [...sources];
|
|
73
|
+
const expanded = expandTemplates(this.sources);
|
|
74
|
+
this.maxClauseId = expanded.length;
|
|
71
75
|
this.clauses = [];
|
|
72
76
|
this.byId = new Map();
|
|
73
77
|
this.groups = new Map();
|
|
@@ -77,9 +81,9 @@ export class Program {
|
|
|
77
81
|
// made once, and a key once allowed as a head is not checked again.
|
|
78
82
|
const keys = new KeyCache();
|
|
79
83
|
const allowed = new Set();
|
|
80
|
-
const count =
|
|
84
|
+
const count = expanded.length;
|
|
81
85
|
for (let index = 0; index < count; index++) {
|
|
82
|
-
const source =
|
|
86
|
+
const source = expanded[index];
|
|
83
87
|
const ordinal = index + 1;
|
|
84
88
|
if (ordinal === without) continue;
|
|
85
89
|
const forward = Object.hasOwn(FORWARD_KINDS, source.kind);
|
|
@@ -104,14 +108,14 @@ export class Program {
|
|
|
104
108
|
const itemKey = keys.of(item);
|
|
105
109
|
headKey = itemKey;
|
|
106
110
|
if (allowed.has(itemKey)) continue;
|
|
107
|
-
if (RESERVED_KEYS.has(itemKey) ||
|
|
111
|
+
if ((RESERVED_KEYS.has(itemKey) && itemKey !== ACTIVATION_KEY) ||
|
|
108
112
|
((PRIMITIVE_KEYS.has(itemKey) || CONTROL_KEYS.has(itemKey)) &&
|
|
109
113
|
!(forward && (item === 'true' || item === 'false')))) {
|
|
110
114
|
throw new EyemindError(`line ${source.line}: unsupported or reserved head ${itemKey}`);
|
|
111
115
|
}
|
|
112
116
|
allowed.add(itemKey);
|
|
113
117
|
}
|
|
114
|
-
const clause = new Clause(ordinal, source.head, forward ? heads : null, body, forward, source.line);
|
|
118
|
+
const clause = new Clause(ordinal, source.head, forward ? heads : null, body, forward, source.line, source.captures ?? null);
|
|
115
119
|
this.clauses.push(clause);
|
|
116
120
|
this.byId.set(ordinal, clause);
|
|
117
121
|
if (forward) {
|
package/src/proof.js
CHANGED
|
@@ -13,6 +13,7 @@ import { freshClause, text } from './common.js';
|
|
|
13
13
|
import { readGoal } from './goals.js';
|
|
14
14
|
import { CONTROL_KEYS, Program } from './program.js';
|
|
15
15
|
import { readTerms } from './reader.js';
|
|
16
|
+
import { isActivation } from './templates.js';
|
|
16
17
|
import {
|
|
17
18
|
Env, EyemindError, Struct, Var, conjunction, copyResolved, deref, flattenConjunction, freshTerm,
|
|
18
19
|
isCallable, isGround, isTerm, key, listFromItems, properListItems, unify, variables,
|
|
@@ -215,6 +216,7 @@ function check(program, claims, steps, goals, allowTrusted, fail, spellings, nam
|
|
|
215
216
|
valid: !fail.items.length,
|
|
216
217
|
steps: steps.size,
|
|
217
218
|
claims: claims.length,
|
|
219
|
+
answers: claims.map((claim) => name(claim)),
|
|
218
220
|
verified: tally.verified,
|
|
219
221
|
redecided: tally.redecided,
|
|
220
222
|
composed: tally.composed,
|
|
@@ -284,7 +286,7 @@ function readDocument(program, document, fail, spellings) {
|
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
function clauseNamed(program, id) {
|
|
287
|
-
if (typeof id !== 'bigint' || id < 1n || id > BigInt(program.
|
|
289
|
+
if (typeof id !== 'bigint' || id < 1n || id > BigInt(program.maxClauseId)) return null;
|
|
288
290
|
return program.byId.get(Number(id)) ?? null;
|
|
289
291
|
}
|
|
290
292
|
|
|
@@ -509,7 +511,11 @@ function checkWitnesses(clause, head, names, env) {
|
|
|
509
511
|
}
|
|
510
512
|
return false;
|
|
511
513
|
};
|
|
512
|
-
|
|
514
|
+
// Recover captures from the source clause and substitution, independently
|
|
515
|
+
// of the proposed witness, including captures the head does not mention.
|
|
516
|
+
const expected = clause.captures === null ? [] : [listFromItems(
|
|
517
|
+
clause.captures.map((name) => copyResolved(names.get(name), env)),
|
|
518
|
+
)];
|
|
513
519
|
for (const variable of variables(head).values()) {
|
|
514
520
|
const value = copyResolved(variable, env);
|
|
515
521
|
if (isGround(value) && !ofThisFiring(value)) expected.push(value);
|
|
@@ -625,7 +631,7 @@ function checkRelevance(program, claims, steps, goals, fail, spellings) {
|
|
|
625
631
|
if (heads.some((head) => head === 'true')) {
|
|
626
632
|
if (!halted && clause.body.length) questions.push(conjunction(clause.body));
|
|
627
633
|
}
|
|
628
|
-
for (const head of heads) if (head !== 'true') questions.push(head);
|
|
634
|
+
for (const head of heads) if (head !== 'true' && !isActivation(head)) questions.push(head);
|
|
629
635
|
}
|
|
630
636
|
}
|
|
631
637
|
for (const claim of claims) {
|
|
@@ -727,7 +733,27 @@ export function checkReport(report) {
|
|
|
727
733
|
facts.push(new Struct(name, [BigInt(count)]));
|
|
728
734
|
}
|
|
729
735
|
facts.push(new Struct('verdict', [verdict(report)]));
|
|
730
|
-
|
|
736
|
+
const heading = !report.valid ? '// Unverified claims: proof checking failed.'
|
|
737
|
+
: !report.answers.length ? '// No answers claimed; this check establishes no conclusions.'
|
|
738
|
+
: report.trusted.length ? '// Answers checked subject to the obligations below.'
|
|
739
|
+
: '// Checked answers: these follow from the input program.';
|
|
740
|
+
const lines = [heading, ...report.answers, '',
|
|
741
|
+
'// C1-C8: resolution, acyclicity, justification, coverage,',
|
|
742
|
+
'// re-decision, boundary consistency, relevance and witness integrity.'];
|
|
743
|
+
let previous = null;
|
|
744
|
+
for (const fact of facts) {
|
|
745
|
+
if (fact.name === 'failure' && previous !== 'failure') {
|
|
746
|
+
lines.push('', '// Failures: the claims above have not passed every check.');
|
|
747
|
+
}
|
|
748
|
+
if (fact.name === 'obligation' && previous !== 'obligation') {
|
|
749
|
+
lines.push('', '// Obligations: absence and collection completeness are taken on trust.');
|
|
750
|
+
}
|
|
751
|
+
if (fact.name === 'steps') lines.push('', '// Proof statistics: recorded steps and how they were checked.');
|
|
752
|
+
if (fact.name === 'verdict') lines.push('', '// This verdict applies to the claims above; it does not establish completeness.');
|
|
753
|
+
lines.push(text(lettered(fact)));
|
|
754
|
+
previous = fact.name;
|
|
755
|
+
}
|
|
756
|
+
return `${lines.join('\n')}\n`;
|
|
731
757
|
}
|
|
732
758
|
|
|
733
759
|
export function verdictText(report) {
|
package/src/templates.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Inline rule templates become statically visible clauses whose first
|
|
2
|
+
// premise records the generating activation. No rule is installed at run time.
|
|
3
|
+
import { normalizeGoal } from './goals.js';
|
|
4
|
+
import {
|
|
5
|
+
EMPTY, EyemindError, Struct, Var, conjunction, flattenConjunction, isCallable, isTerm, listFromItems, variables,
|
|
6
|
+
} from './terms.js';
|
|
7
|
+
|
|
8
|
+
export const ACTIVATION_KEY = 'activated/2';
|
|
9
|
+
export const isActivation = (term) => isTerm(term, 'activated', 2);
|
|
10
|
+
const isTemplate = (term) => isTerm(term, 'forward', 2) || isTerm(term, 'backward', 2);
|
|
11
|
+
|
|
12
|
+
function containsTemplate(term) {
|
|
13
|
+
const pending = [term];
|
|
14
|
+
while (pending.length) {
|
|
15
|
+
const item = pending.pop();
|
|
16
|
+
if (isTemplate(item)) return true;
|
|
17
|
+
if (item instanceof Struct) pending.push(...item.args);
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function checkGoal(goal, line) {
|
|
23
|
+
if (goal instanceof Var) throw new EyemindError(`line ${line}: generated rule bodies require statically named calls`);
|
|
24
|
+
if (!isCallable(goal) || goal === EMPTY || isTerm(goal, '.', 2)) {
|
|
25
|
+
throw new EyemindError(`line ${line}: a generated rule goal must be an atom or a compound, not a number or list`);
|
|
26
|
+
}
|
|
27
|
+
if (!(goal instanceof Struct)) return;
|
|
28
|
+
if (isTemplate(goal)) throw new EyemindError(`line ${line}: rule templates belong in an implies() conclusion`);
|
|
29
|
+
if (isTerm(goal, 'and', 2) || isTerm(goal, 'or', 2)) {
|
|
30
|
+
for (const part of goal.args) checkGoal(part, line);
|
|
31
|
+
} else if (['not', 'call', 'once'].includes(goal.name) && goal.args.length === 1) {
|
|
32
|
+
checkGoal(goal.args[0], line);
|
|
33
|
+
} else if (isTerm(goal, 'findall', 3)) checkGoal(goal.args[1], line);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function expandTemplates(sources) {
|
|
37
|
+
const expanded = [...sources];
|
|
38
|
+
for (let index = 0; index < expanded.length; index++) {
|
|
39
|
+
const source = expanded[index];
|
|
40
|
+
const heads = flattenConjunction(source.head);
|
|
41
|
+
for (const head of heads) {
|
|
42
|
+
if (isActivation(head)) throw new EyemindError(`line ${source.line}: activated/2 is reserved for generated rules`);
|
|
43
|
+
}
|
|
44
|
+
const templates = heads.filter(isTemplate);
|
|
45
|
+
if (!templates.length) {
|
|
46
|
+
if (containsTemplate(source.head) || source.body.some(containsTemplate)) {
|
|
47
|
+
throw new EyemindError(`line ${source.line}: rule templates must be top-level conjuncts of an implies() conclusion`);
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (source.kind !== 'forward') {
|
|
52
|
+
throw new EyemindError(`line ${source.line}: rule templates belong in an implies() conclusion`);
|
|
53
|
+
}
|
|
54
|
+
if (source.body.some(containsTemplate)) {
|
|
55
|
+
throw new EyemindError(`line ${source.line}: rule templates belong in an implies() conclusion`);
|
|
56
|
+
}
|
|
57
|
+
// Variables occurring outside a template are captured. Other variables
|
|
58
|
+
// belong to that generated rule and are freshened by ordinary resolution.
|
|
59
|
+
const outer = new Map();
|
|
60
|
+
for (const goal of source.body) variables(goal, outer);
|
|
61
|
+
for (const head of heads) if (!isTemplate(head)) variables(head, outer);
|
|
62
|
+
const lowered = heads.map((head) => {
|
|
63
|
+
if (!isTemplate(head)) {
|
|
64
|
+
if (containsTemplate(head)) throw new EyemindError(`line ${source.line}: a rule template cannot be nested inside an ordinary term`);
|
|
65
|
+
return head;
|
|
66
|
+
}
|
|
67
|
+
const forward = head.name === 'forward';
|
|
68
|
+
const premise = normalizeGoal(head.args[forward ? 0 : 1]);
|
|
69
|
+
const conclusion = normalizeGoal(head.args[forward ? 1 : 0]);
|
|
70
|
+
for (const item of flattenConjunction(conclusion)) {
|
|
71
|
+
if (item === EMPTY || isTerm(item, '.', 2)) {
|
|
72
|
+
throw new EyemindError(`line ${source.line}: a generated rule conclusion cannot be an empty or open list`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const body = flattenConjunction(premise);
|
|
76
|
+
for (const goal of body) checkGoal(goal, source.line);
|
|
77
|
+
const captured = [...variables(head).values()].filter((variable) => outer.has(variable.name));
|
|
78
|
+
const activation = new Struct('activated', [BigInt(expanded.length + 1), listFromItems(captured)]);
|
|
79
|
+
const conclusions = forward ? [conclusion] : flattenConjunction(conclusion);
|
|
80
|
+
for (const generated of conclusions) {
|
|
81
|
+
expanded.push({
|
|
82
|
+
kind: forward ? 'forward' : 'backward', head: generated, body: [activation, ...body],
|
|
83
|
+
line: source.line, file: source.file, captures: captured.map((variable) => variable.name),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return activation;
|
|
87
|
+
});
|
|
88
|
+
expanded[index] = { ...source, head: conjunction(lowered) };
|
|
89
|
+
}
|
|
90
|
+
return expanded;
|
|
91
|
+
}
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// The release this checkout is; ./release keeps it in step with package.json.
|
|
2
|
-
export const VERSION = '0.1.
|
|
2
|
+
export const VERSION = '0.1.26';
|