create-arxo 0.2.0 → 0.2.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 +35 -2
- package/index.js +12 -4
- package/package.json +1 -1
- package/template/README.md +39 -0
- package/template/_gitignore +1 -0
- package/template/package.json +15 -3
- package/template/src/ask.ts +121 -0
- package/template/test/{frist.test.mjs → frist.test.ts} +17 -7
- package/template/tsconfig.json +12 -0
package/README.md
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
# create-arxo
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
```
|
|
4
|
+
npm create arxo@latest my-app
|
|
5
|
+
cd my-app && npm install && npm start
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
de.bgb.fristen@0.1.0 opened (local)
|
|
10
|
+
|
|
11
|
+
Does a 14-day period from 2026-03-06 end on 2026-03-20?
|
|
12
|
+
evaluationStatus COMPUTED
|
|
13
|
+
truthStatus TRUE_ONLY (established: yes)
|
|
14
|
+
rules applied TagesfristEnde
|
|
15
|
+
hashes
|
|
16
|
+
program sha256:…
|
|
17
|
+
result sha256:…
|
|
18
|
+
document … bytes, canonical evaluation document
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The project is TypeScript (`tsx`, no build step): `src/ask.ts` opens the
|
|
22
|
+
pinned canon `de.bgb.fristen@0.1.0` (§§ 187, 188 BGB, installed with
|
|
23
|
+
`@arxo/canon-bgb-fristen`), asks one `truth` question and prints the
|
|
24
|
+
answer — statuses, the rule applied, sources, hashes, the attached
|
|
25
|
+
evaluation document. It then asks the same question **without** the
|
|
26
|
+
duration and shows how `whyNot` turns into a request for the missing fact.
|
|
27
|
+
|
|
28
|
+
Scripts of the created project:
|
|
29
|
+
|
|
30
|
+
| Command | What it does |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `npm start` | runs `src/ask.ts` |
|
|
33
|
+
| `npm test` | the same question as a `node --test` test |
|
|
34
|
+
| `npm run typecheck` | checks predicate names against the canon's declarations |
|
|
35
|
+
|
|
36
|
+
Next: https://docs.arxo.io/guide/quickstart/ and
|
|
37
|
+
https://docs.arxo.io/guide/reading-an-answer/.
|
package/index.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { cpSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
6
6
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
const dest = process.argv[2] ?? 'arxo-app';
|
|
8
8
|
mkdirSync(dest, { recursive: true });
|
|
9
9
|
cpSync(join(HERE, 'template'), dest, { recursive: true });
|
|
10
|
+
// npm renames .gitignore inside a published package, so the template ships _gitignore.
|
|
11
|
+
if (existsSync(join(dest, '_gitignore'))) renameSync(join(dest, '_gitignore'), join(dest, '.gitignore'));
|
|
10
12
|
const pkg = JSON.parse(readFileSync(join(dest, 'package.json'), 'utf8'));
|
|
11
|
-
pkg.name = dest.replace(/[^a-z0-9-._]/gi, '-').toLowerCase();
|
|
13
|
+
pkg.name = basename(resolve(dest)).replace(/[^a-z0-9-._]/gi, '-').toLowerCase();
|
|
12
14
|
writeFileSync(join(dest, 'package.json'), `${JSON.stringify(pkg, null, 2)}\n`);
|
|
13
|
-
console.log(`created ${dest}
|
|
15
|
+
console.log(`created ${dest}
|
|
16
|
+
|
|
17
|
+
Next:
|
|
18
|
+
cd ${dest} && npm install && npm start
|
|
19
|
+
|
|
20
|
+
npm start — asks the canon one question and prints the answer with its proof
|
|
21
|
+
npm test — the same question as a test`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# arxo-app
|
|
2
|
+
|
|
3
|
+
One canon, one question, answered by the Arxo engine with a proof.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm install
|
|
7
|
+
npm start
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`src/ask.ts` opens the pinned canon `de.bgb.fristen@0.1.0` (§§ 186–213 BGB,
|
|
11
|
+
installed with `@arxo/canon-bgb-fristen`) and asks: an event on 6 March 2026
|
|
12
|
+
starts a period of 14 days — does it end on 20 March 2026?
|
|
13
|
+
|
|
14
|
+
You will see:
|
|
15
|
+
|
|
16
|
+
- `evaluationStatus COMPUTED` and `truthStatus TRUE_ONLY` — the engine
|
|
17
|
+
established the answer; a refusal would be a status, not an exception;
|
|
18
|
+
- the rule applied and the sources behind it; the hashes that make the
|
|
19
|
+
answer reproducible; the size of the attached evaluation document;
|
|
20
|
+
- the same question asked again **without the duration**: the answer is
|
|
21
|
+
`NEITHER`, `whyNot` names the premises the rule needs, and the program
|
|
22
|
+
asks you to add `frist_dauer_tage`.
|
|
23
|
+
|
|
24
|
+
Facts and their meaning:
|
|
25
|
+
|
|
26
|
+
| Fact | Meaning |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `frist_ereignis(frist, 2026-03-06)` | the event that starts the period (§ 187 (1) BGB) |
|
|
29
|
+
| `frist_dauer_tage(frist, 14 calendar_day)` | the length of the period in days (§ 188 (1) BGB) |
|
|
30
|
+
| `frist_ende(frist, 2026-03-20)` | the question: does the period end on this date? |
|
|
31
|
+
|
|
32
|
+
`npm test` runs the same question as a test; `npm run typecheck` checks
|
|
33
|
+
the predicate names against the canon's declarations.
|
|
34
|
+
|
|
35
|
+
Next:
|
|
36
|
+
|
|
37
|
+
- Quickstart: https://docs.arxo.io/guide/quickstart/
|
|
38
|
+
- Reading an answer (statuses, `whyNot`, proof, hashes): https://docs.arxo.io/guide/reading-an-answer/
|
|
39
|
+
- More canons and questions: `npm view @arxo/law readme`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
node_modules/
|
package/template/package.json
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arxo-app",
|
|
3
3
|
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
4
5
|
"type": "module",
|
|
6
|
+
"description": "One canon, one question: §§ 187, 188 BGB with @arxo/law",
|
|
5
7
|
"scripts": {
|
|
6
|
-
"
|
|
8
|
+
"start": "tsx src/ask.ts",
|
|
9
|
+
"test": "node --import tsx --test test/frist.test.ts",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
7
11
|
},
|
|
8
12
|
"dependencies": {
|
|
9
|
-
"@arxo/law": "0.
|
|
10
|
-
"@arxo/canon-bgb-fristen": "0.1.
|
|
13
|
+
"@arxo/law": "0.3.0",
|
|
14
|
+
"@arxo/canon-bgb-fristen": "0.1.3"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^20.0.0",
|
|
18
|
+
"tsx": "^4.19.0",
|
|
19
|
+
"typescript": "^5.6.0"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20"
|
|
11
23
|
}
|
|
12
24
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One canon, one question.
|
|
3
|
+
*
|
|
4
|
+
* §§ 187, 188 BGB (German Civil Code): an event on 6 March 2026 starts a
|
|
5
|
+
* period of 14 days. Does the period end on 20 March 2026?
|
|
6
|
+
*
|
|
7
|
+
* The answer is computed by the Arxo engine from the pinned canon
|
|
8
|
+
* `de.bgb.fristen@0.1.0` (installed with `@arxo/canon-bgb-fristen`); the
|
|
9
|
+
* canonical evaluation document that proves it is attached to the answer.
|
|
10
|
+
*
|
|
11
|
+
* Run: npm start
|
|
12
|
+
*/
|
|
13
|
+
import { open, type Answer, type CaseInput, type Fact } from '@arxo/law';
|
|
14
|
+
// Type-only import: the canon registers its relation names in
|
|
15
|
+
// `CanonRegistry`, so a misspelled predicate is an editor error.
|
|
16
|
+
import type { RelationName } from '@arxo/canon-bgb-fristen';
|
|
17
|
+
|
|
18
|
+
// `frist` names the period we ask about; the values are bare and typed by
|
|
19
|
+
// the canon's declarations (a Date, a Quantity of days).
|
|
20
|
+
const EVENT_DATE = '2026-03-06';
|
|
21
|
+
const CANDIDATE_END = '2026-03-20';
|
|
22
|
+
|
|
23
|
+
// frist_ereignis — the event that starts the period (§ 187 (1) BGB:
|
|
24
|
+
// the day of the event itself is not counted).
|
|
25
|
+
const eventFact: Fact = { predicate: 'frist_ereignis', args: ['frist', EVENT_DATE] };
|
|
26
|
+
// frist_dauer_tage — the length of the period in days (§ 188 (1) BGB:
|
|
27
|
+
// a period stated in days ends with the expiry of its last day).
|
|
28
|
+
const durationFact: Fact = { predicate: 'frist_dauer_tage', args: ['frist', '14 calendar_day'] };
|
|
29
|
+
|
|
30
|
+
// frist_ende(frist, date) — does the period end on this date?
|
|
31
|
+
const question: RelationName = 'frist_ende';
|
|
32
|
+
|
|
33
|
+
function caseInput(facts: Fact[]): CaseInput {
|
|
34
|
+
return {
|
|
35
|
+
// The date the question is asked on the legal axis; never "today".
|
|
36
|
+
legalTime: '2026-09-17',
|
|
37
|
+
timezone: 'Europe/Berlin',
|
|
38
|
+
// How days are counted is a policy of the canon, named explicitly.
|
|
39
|
+
deadlinePolicy: 'urn:de:corpus:clir:bgb-fristen#BGB_FRISTEN_TAG',
|
|
40
|
+
answers: facts,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const TRUTH: Record<string, string> = {
|
|
45
|
+
TRUE_ONLY: 'established: yes',
|
|
46
|
+
FALSE_ONLY: 'established: no',
|
|
47
|
+
NEITHER: 'not established either way',
|
|
48
|
+
BOTH: 'contradictory',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const localName = (id: unknown): string => String(id).split('#').pop() ?? String(id);
|
|
52
|
+
|
|
53
|
+
function describe(label: string, r: Answer): void {
|
|
54
|
+
console.log(`\n${label}`);
|
|
55
|
+
console.log(` evaluationStatus ${r.evaluationStatus}`);
|
|
56
|
+
console.log(` truthStatus ${r.truthStatus ?? '—'} (${TRUTH[r.truthStatus ?? ''] ?? 'see the document'})`);
|
|
57
|
+
|
|
58
|
+
// The proof graph: which rules of the canon were applied.
|
|
59
|
+
const rules = (r.proof?.nodes ?? [])
|
|
60
|
+
.filter((n) => n.kind === 'rule_application' && n.rule)
|
|
61
|
+
.map((n) => localName(n.rule));
|
|
62
|
+
console.log(` rules applied ${rules.length ? [...new Set(rules)].join(', ') : '—'}`);
|
|
63
|
+
|
|
64
|
+
// The norms behind the answer, with their pinned text, when the canon
|
|
65
|
+
// anchors its rules to the source (§ 187, § 188 BGB here).
|
|
66
|
+
const sources = Array.isArray(r.sources) ? r.sources : [];
|
|
67
|
+
if (sources.length) {
|
|
68
|
+
for (const s of sources) console.log(` source ${JSON.stringify(s)}`);
|
|
69
|
+
} else {
|
|
70
|
+
console.log(' sources none anchored in this canon version — see the rule names above');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Reproducibility: the same canon, case and semantics give the same hashes.
|
|
74
|
+
console.log(' hashes');
|
|
75
|
+
for (const [k, v] of Object.entries(r.hashes)) console.log(` ${k.padEnd(8)} ${v}`);
|
|
76
|
+
|
|
77
|
+
if (r.via === 'local') {
|
|
78
|
+
console.log(` document ${r.document.byteLength} bytes, canonical evaluation document`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const fristen = await open('de.bgb.fristen@0.1.0', { offline: true });
|
|
83
|
+
console.log(`${fristen.name}@${fristen.version} opened (${fristen.via})`);
|
|
84
|
+
|
|
85
|
+
// 1. Both facts given: is 20 March 2026 the end of the period?
|
|
86
|
+
const answer = await fristen.truth(question, ['frist', CANDIDATE_END], caseInput([eventFact, durationFact]));
|
|
87
|
+
describe(`Does a 14-day period from ${EVENT_DATE} end on ${CANDIDATE_END}?`, answer);
|
|
88
|
+
|
|
89
|
+
// 2. The same question without the duration: the engine does not guess.
|
|
90
|
+
// `whyNot` returns the blocker graph — the premises each rule needs —
|
|
91
|
+
// and the application turns it into a request for the missing facts.
|
|
92
|
+
const partial = await fristen.whyNot(question, ['frist', CANDIDATE_END], caseInput([eventFact]));
|
|
93
|
+
describe(`The same question without frist_dauer_tage (why not?)`, partial);
|
|
94
|
+
|
|
95
|
+
console.log(` whyNot ${partial.whyNot && Array.isArray(partial.whyNot) && partial.whyNot.length
|
|
96
|
+
? partial.whyNot.map((w) => `${w.kind}: ${w.description}`).join('; ')
|
|
97
|
+
: 'no input requirement raised by the engine — the rule stays undetermined'}`);
|
|
98
|
+
|
|
99
|
+
type Blocker = { rule?: string; conjuncts?: Array<{ literal?: { predicate?: string }; trigger?: string }> };
|
|
100
|
+
const blockers: Blocker[] =
|
|
101
|
+
partial.value && typeof partial.value === 'object' && 'value' in partial.value
|
|
102
|
+
? ((partial.value as { value?: { blockers?: Blocker[] } }).value?.blockers ?? [])
|
|
103
|
+
: [];
|
|
104
|
+
const given = new Set([eventFact].map((f) => f.predicate));
|
|
105
|
+
// Ask only for what the rules your facts already speak to still need; a
|
|
106
|
+
// rule none of your facts touch is another route to the same conclusion.
|
|
107
|
+
const missing = new Set<string>();
|
|
108
|
+
for (const b of blockers) {
|
|
109
|
+
const needs = (b.conjuncts ?? []).map((c) => localName(c.literal?.predicate)).filter(Boolean);
|
|
110
|
+
const touched = needs.some((p) => given.has(p));
|
|
111
|
+
console.log(` rule ${localName(b.rule).padEnd(17)} needs ${needs.join(', ')}${touched ? '' : ' (another route)'}`);
|
|
112
|
+
if (touched) for (const p of needs) if (!given.has(p)) missing.add(p);
|
|
113
|
+
}
|
|
114
|
+
const HINT: Record<string, string> = {
|
|
115
|
+
frist_ereignis: 'the event that starts the period, e.g. "2026-03-06"',
|
|
116
|
+
frist_dauer_tage: 'the length of the period in days, e.g. "14 calendar_day"',
|
|
117
|
+
};
|
|
118
|
+
if (missing.size) {
|
|
119
|
+
console.log('\nTo answer, please add:');
|
|
120
|
+
for (const p of missing) console.log(` - ${p}: ${HINT[p] ?? 'see `fristen.questions()` for the declared type'}`);
|
|
121
|
+
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
3
|
import { open } from '@arxo/law';
|
|
4
|
+
import type { RelationName } from '@arxo/canon-bgb-fristen';
|
|
4
5
|
|
|
5
6
|
test('opens the bundled canon and asks one question', async () => {
|
|
6
|
-
const
|
|
7
|
-
const fristen = await open('de.bgb.fristen@0.1.0', {
|
|
8
|
-
offline: true,
|
|
9
|
-
...(wasmPath ? { wasmPath } : {}),
|
|
10
|
-
});
|
|
7
|
+
const fristen = await open('de.bgb.fristen@0.1.0', { offline: true });
|
|
11
8
|
assert.equal(fristen.name, 'de.bgb.fristen');
|
|
12
9
|
// 14 days from 6 March 2026, § 187 (1) and § 188 (1) BGB: does the period end on 20 March?
|
|
13
|
-
const
|
|
10
|
+
const question: RelationName = 'frist_ende';
|
|
11
|
+
const r = await fristen.truth(question, ['frist', '2026-03-20'], {
|
|
14
12
|
legalTime: '2026-09-17',
|
|
15
13
|
timezone: 'Europe/Berlin',
|
|
16
14
|
deadlinePolicy: 'urn:de:corpus:clir:bgb-fristen#BGB_FRISTEN_TAG',
|
|
@@ -22,5 +20,17 @@ test('opens the bundled canon and asks one question', async () => {
|
|
|
22
20
|
assert.equal(r.via, 'local');
|
|
23
21
|
assert.equal(r.evaluationStatus, 'COMPUTED');
|
|
24
22
|
assert.equal(r.truthStatus, 'TRUE_ONLY');
|
|
25
|
-
assert.ok(r.document.byteLength > 0, 'the evaluation document is attached');
|
|
23
|
+
assert.ok(r.via === 'local' && r.document.byteLength > 0, 'the evaluation document is attached');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('without the duration the answer is NEITHER, not a guess', async () => {
|
|
27
|
+
const fristen = await open('de.bgb.fristen@0.1.0', { offline: true });
|
|
28
|
+
const r = await fristen.truth('frist_ende', ['frist', '2026-03-20'], {
|
|
29
|
+
legalTime: '2026-09-17',
|
|
30
|
+
timezone: 'Europe/Berlin',
|
|
31
|
+
deadlinePolicy: 'urn:de:corpus:clir:bgb-fristen#BGB_FRISTEN_TAG',
|
|
32
|
+
answers: [{ predicate: 'frist_ereignis', args: ['frist', '2026-03-06'] }],
|
|
33
|
+
});
|
|
34
|
+
assert.equal(r.evaluationStatus, 'COMPUTED');
|
|
35
|
+
assert.equal(r.truthStatus, 'NEITHER');
|
|
26
36
|
});
|