eyelang 0.1.5 → 0.1.6
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/docs/guide.md +3 -5
- package/docs/language-reference.md +1 -3
- package/index.d.ts +167 -29
- package/package.json +1 -1
- package/test/run-regression.mjs +168 -38
package/docs/guide.md
CHANGED
|
@@ -238,7 +238,7 @@ The playground has matching `--stats` and `--proof` checkboxes, so browser runs
|
|
|
238
238
|
|
|
239
239
|
### Builtins
|
|
240
240
|
|
|
241
|
-
Eyelang builtins are registered by name and arity in small modules under [`src/builtins`](../src/builtins). This keeps the runtime portable to Node.js and the browser while giving each builtin family a clear boundary. Built-ins are called as ordinary Eyelang predicates. See the [Eyelang language reference](
|
|
241
|
+
Eyelang builtins are registered by name and arity in small modules under [`src/builtins`](../src/builtins). This keeps the runtime portable to Node.js and the browser while giving each builtin family a clear boundary. Built-ins are called as ordinary Eyelang predicates. See the [Eyelang language reference](language-reference.md#9-standard-built-in-predicates) for the portable profile. The bundled implementation currently registers 80 name/arity entries across 78 predicate names:
|
|
242
242
|
|
|
243
243
|
| Family | Count | Built-ins |
|
|
244
244
|
|---|---:|---|
|
|
@@ -289,8 +289,6 @@ Use `holds/2` when you want to match the member term directly, for example `name
|
|
|
289
289
|
|
|
290
290
|
`matches/3` can create context data from named regular-expression captures, which is useful when text logs or messages need to become facts before later rules inspect them with `holds/2` or `holds/3`. See [`observability-log-correlation.pl`](../examples/observability-log-correlation.pl) for a complete log-correlation example.
|
|
291
291
|
|
|
292
|
-
The N3 counterpart of the context schema audit lives at [`examples/context-schema-audit.n3`](../examples/context-schema-audit.n3) with golden output in [`examples/output/context-schema-audit.md`](../examples/output/context-schema-audit.md).
|
|
293
|
-
|
|
294
292
|
|
|
295
293
|
## Example catalog
|
|
296
294
|
|
|
@@ -467,8 +465,8 @@ The conformance suite lives in [`test/conformance/`](../test/conformance/) as on
|
|
|
467
465
|
Common commands:
|
|
468
466
|
|
|
469
467
|
```sh
|
|
470
|
-
npm run test:eyelang #
|
|
471
|
-
npm
|
|
468
|
+
npm run test:eyelang # alias for npm test
|
|
469
|
+
npm test # full conformance, regression/API/white-box, examples, and proof examples
|
|
472
470
|
node test/run-conformance.mjs
|
|
473
471
|
node test/run-regression.mjs
|
|
474
472
|
node test/run-examples.mjs
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
- [9.6 Strings and atom constants](#96-strings-and-atom-constants)
|
|
40
40
|
- [9.7 Lists](#97-lists)
|
|
41
41
|
- [9.8 Aggregation and ordering](#98-aggregation-and-ordering)
|
|
42
|
-
- [9.9 Context
|
|
42
|
+
- [9.9 Context and term inspection](#99-context-and-term-inspection)
|
|
43
43
|
- [9.10 Search control](#910-search-control)
|
|
44
44
|
- [10. Implementation-specific built-ins](#10-implementation-specific-built-ins)
|
|
45
45
|
- [11. Declarations](#11-declarations)
|
|
@@ -475,8 +475,6 @@ The first goal can yield `holds((name(alice, "Alice"), knows(alice, bob)), name(
|
|
|
475
475
|
|
|
476
476
|
`holds/3` is the appropriate form for schema-style introspection because it exposes the predicate name and all arguments without assuming a fixed arity. For example, a single rule can inspect `heartbeat`, `source(sensor17)`, `temperature(sensor17, 38)`, and `signature(sensor17, sha256, Hash, Time)` as `heartbeat/0`, `source/1`, `temperature/2`, and `signature/4`; see [`context-schema-audit.pl`](../examples/context-schema-audit.pl).
|
|
477
477
|
|
|
478
|
-
The N3 example [`context-schema-audit.n3`](../examples/context-schema-audit.n3) shows the same idea in quoted graph form: members are encoded as predicates with RDF-list argument objects, then `log:includes` and `list:length` expose `Name + Arity` for schema checking.
|
|
479
|
-
|
|
480
478
|
### 9.10 Search control
|
|
481
479
|
|
|
482
480
|
| Built-in | Meaning |
|
package/index.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ export interface EyelangRunOptions {
|
|
|
6
6
|
proof?: boolean;
|
|
7
7
|
why?: boolean;
|
|
8
8
|
explain?: boolean;
|
|
9
|
+
maxDepth?: number;
|
|
10
|
+
solutionLimit?: number;
|
|
11
|
+
registry?: BuiltinRegistry;
|
|
12
|
+
sourceMetadata?: boolean;
|
|
13
|
+
markRecursive?: boolean;
|
|
9
14
|
[key: string]: unknown;
|
|
10
15
|
}
|
|
11
16
|
|
|
@@ -20,61 +25,194 @@ export interface EyelangSourcePart {
|
|
|
20
25
|
filename?: string;
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
export interface EyelangClause {
|
|
29
|
+
head: EyelangTerm;
|
|
30
|
+
body: EyelangTerm[];
|
|
31
|
+
index?: number;
|
|
32
|
+
filename?: string;
|
|
33
|
+
clauseNumber?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface EyelangPredicateGroup {
|
|
37
|
+
name: string;
|
|
38
|
+
arity: number;
|
|
39
|
+
clauses: EyelangClause[];
|
|
40
|
+
argIndexes: unknown[];
|
|
41
|
+
pairIndexes: unknown[];
|
|
42
|
+
memoized: boolean;
|
|
43
|
+
recursive: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type EyelangTerm = Term | { type: string; name: string; args?: EyelangTerm[]; arity?: number };
|
|
47
|
+
|
|
48
|
+
export class Term {
|
|
49
|
+
constructor(type: string, name?: unknown, args?: EyelangTerm[]);
|
|
50
|
+
type: string;
|
|
51
|
+
name: string;
|
|
52
|
+
args: EyelangTerm[];
|
|
53
|
+
get arity(): number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class Env {
|
|
57
|
+
constructor(bindings?: Iterable<readonly [string, EyelangTerm]> | null);
|
|
58
|
+
bindings: Map<string, EyelangTerm>;
|
|
59
|
+
clone(): Env;
|
|
60
|
+
has(name: string): boolean;
|
|
61
|
+
get(name: string): EyelangTerm | undefined;
|
|
62
|
+
bind(name: string, term: EyelangTerm): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
23
65
|
export class Program {
|
|
24
|
-
constructor(clauses?:
|
|
66
|
+
constructor(clauses?: EyelangClause[], options?: EyelangRunOptions);
|
|
67
|
+
clauses: EyelangClause[];
|
|
68
|
+
groups: Map<string, EyelangPredicateGroup>;
|
|
69
|
+
materializedGroups: Set<string>;
|
|
70
|
+
hasMaterialize: boolean;
|
|
25
71
|
static parse(source: string, options?: EyelangRunOptions): Program;
|
|
26
72
|
static parseSources(sources?: Array<string | EyelangSourcePart>, options?: EyelangRunOptions): Program;
|
|
27
|
-
|
|
73
|
+
makeGroup(name: string, arity: number): EyelangPredicateGroup;
|
|
74
|
+
indexClause(clause: EyelangClause): void;
|
|
75
|
+
findGroup(name: string, arity: number): EyelangPredicateGroup | null;
|
|
76
|
+
applyDeclarations(options?: EyelangRunOptions): void;
|
|
77
|
+
markRecursivePredicates(): void;
|
|
78
|
+
hasMaterializeDeclarations(): boolean;
|
|
79
|
+
groupIsMaterialized(group: EyelangPredicateGroup): boolean;
|
|
80
|
+
groupHasRule(group: EyelangPredicateGroup): boolean;
|
|
28
81
|
sourceFactLines(predicateKeys?: Set<string> | null): Set<string>;
|
|
82
|
+
materializationGoals(): EyelangTerm[];
|
|
29
83
|
}
|
|
30
84
|
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
|
|
85
|
+
export interface BuiltinDefinition {
|
|
86
|
+
name: string;
|
|
87
|
+
arity: number;
|
|
88
|
+
handler: BuiltinHandler;
|
|
89
|
+
deterministic: boolean;
|
|
90
|
+
ready: ((solver: Solver, goal: EyelangTerm, env: Env) => boolean) | null;
|
|
91
|
+
fallbackWhenNotReady: boolean;
|
|
92
|
+
shouldUse: ((solver: Solver, goal: EyelangTerm, env: Env) => boolean) | null;
|
|
93
|
+
}
|
|
34
94
|
|
|
35
|
-
export
|
|
36
|
-
|
|
95
|
+
export type BuiltinHandler = (context: { solver: Solver; goal: EyelangTerm; env: Env }) => Iterable<Env>;
|
|
96
|
+
|
|
97
|
+
export class BuiltinRegistry {
|
|
98
|
+
constructor();
|
|
99
|
+
defs: Map<string, BuiltinDefinition>;
|
|
100
|
+
add(name: string, arity: number, handler: BuiltinHandler, options?: Partial<BuiltinDefinition>): this;
|
|
101
|
+
get(name: string, arity: number): BuiltinDefinition | null;
|
|
37
102
|
}
|
|
38
103
|
|
|
39
104
|
export class Solver {
|
|
40
105
|
constructor(program: Program, options?: EyelangRunOptions);
|
|
106
|
+
program: Program;
|
|
107
|
+
registry: BuiltinRegistry;
|
|
108
|
+
maxDepth: number;
|
|
109
|
+
solutionLimit: number;
|
|
110
|
+
solutionsSeen: number;
|
|
111
|
+
active: unknown[];
|
|
112
|
+
memo: Map<string, unknown>;
|
|
41
113
|
stats: EyelangStats;
|
|
42
|
-
|
|
114
|
+
cloneForInnerGoal(solutionLimit?: number): Solver;
|
|
115
|
+
solve(goals: EyelangTerm | EyelangTerm[], env?: Env, depth?: number): Iterable<Env>;
|
|
116
|
+
activeVariant(goal: EyelangTerm, env: Env): boolean;
|
|
43
117
|
}
|
|
44
118
|
|
|
45
|
-
export
|
|
46
|
-
|
|
47
|
-
|
|
119
|
+
export const VAR: 'var';
|
|
120
|
+
export const ATOM: 'atom';
|
|
121
|
+
export const STRING: 'string';
|
|
122
|
+
export const NUMBER: 'number';
|
|
123
|
+
export const COMPOUND: 'compound';
|
|
48
124
|
|
|
125
|
+
export function variable(name: string): Term;
|
|
126
|
+
export function atom(name: string): Term;
|
|
127
|
+
export function stringTerm(value: string): Term;
|
|
128
|
+
export function numberTerm(value: string | number): Term;
|
|
129
|
+
export function compound(name: string, args?: EyelangTerm[]): Term;
|
|
130
|
+
export function emptyList(): Term;
|
|
131
|
+
export function cons(head: EyelangTerm, tail: EyelangTerm): Term;
|
|
132
|
+
export function deref(term: EyelangTerm, env: Env): EyelangTerm;
|
|
133
|
+
export function isScalar(term: EyelangTerm | null | undefined): boolean;
|
|
134
|
+
export function isEmptyList(term: EyelangTerm | null | undefined): boolean;
|
|
135
|
+
export function isCons(term: EyelangTerm | null | undefined): boolean;
|
|
136
|
+
export function isConjunction(term: EyelangTerm | null | undefined): boolean;
|
|
137
|
+
export function unify(left: EyelangTerm, right: EyelangTerm, env: Env): boolean;
|
|
138
|
+
export function cloneTerm(term: EyelangTerm): Term;
|
|
139
|
+
export function freshTerm(term: EyelangTerm, suffix: string | number): Term;
|
|
140
|
+
export function copyResolved(term: EyelangTerm, env: Env): Term;
|
|
141
|
+
export function termIsGround(term: EyelangTerm, env?: Env): boolean;
|
|
142
|
+
export function termToString(term: EyelangTerm, env?: Env, quoteStrings?: boolean): string;
|
|
143
|
+
export function lexicalValue(term: EyelangTerm, env: Env): string | null;
|
|
144
|
+
export function properListItems(list: EyelangTerm, env: Env): EyelangTerm[] | null;
|
|
145
|
+
export function listFromItems(items: EyelangTerm[], start?: number, end?: number, tail?: EyelangTerm): Term;
|
|
146
|
+
export function flattenConjunction(goal: EyelangTerm): EyelangTerm[];
|
|
147
|
+
export function termSignature(term: EyelangTerm | null | undefined): string | null;
|
|
148
|
+
export function variantTerms(left: EyelangTerm, leftEnv: Env, right: EyelangTerm, rightEnv: Env, pairs?: Map<string, string>, reverse?: Map<string, string>): boolean;
|
|
149
|
+
export function compareTerms(left: EyelangTerm, right: EyelangTerm): number;
|
|
150
|
+
export function isDecimalInteger(text: string | null | undefined): boolean;
|
|
151
|
+
export function compareIntegerText(left: string, right: string): number;
|
|
152
|
+
export function parseFiniteNumber(text: string | null | undefined): number | null;
|
|
153
|
+
export function numberTextFromDouble(value: number): string | null;
|
|
154
|
+
export function compareNumberText(left: string, right: string): number;
|
|
155
|
+
|
|
156
|
+
export function makeProgram(source: string, options?: EyelangRunOptions): Program;
|
|
157
|
+
export function parseClauses(source: string, options?: EyelangRunOptions): EyelangClause[];
|
|
158
|
+
export function parseProgramText(source: string, options?: EyelangRunOptions): EyelangClause[];
|
|
49
159
|
export function createDefaultRegistry(): BuiltinRegistry;
|
|
50
160
|
export function getDefaultRegistry(): BuiltinRegistry;
|
|
51
|
-
|
|
52
161
|
export function run(source: string | Program, options?: EyelangRunOptions): EyelangRunResult;
|
|
53
|
-
|
|
54
|
-
export function
|
|
55
|
-
export function
|
|
56
|
-
|
|
57
|
-
export const ATOM: 'atom';
|
|
58
|
-
export const VARIABLE: 'variable';
|
|
59
|
-
export const COMPOUND: 'compound';
|
|
60
|
-
export const NUMBER: 'number';
|
|
61
|
-
export const STRING: 'string';
|
|
62
|
-
export function atom(name: string): unknown;
|
|
63
|
-
export function variable(name: string): unknown;
|
|
64
|
-
export function compound(name: string, args?: unknown[]): unknown;
|
|
65
|
-
export function termToString(term: unknown, env?: Env, quoted?: boolean): string;
|
|
162
|
+
export function whyProof(program: Program, goal: EyelangTerm, options?: EyelangRunOptions): { ok: boolean; text: string };
|
|
163
|
+
export function whyNoProof(goal: EyelangTerm): string;
|
|
164
|
+
export function explainProof(program: Program, goal: EyelangTerm, options?: EyelangRunOptions): { ok: boolean; text: string };
|
|
66
165
|
|
|
67
166
|
declare const eyelang: {
|
|
68
|
-
|
|
167
|
+
VAR: typeof VAR;
|
|
168
|
+
ATOM: typeof ATOM;
|
|
169
|
+
STRING: typeof STRING;
|
|
170
|
+
NUMBER: typeof NUMBER;
|
|
171
|
+
COMPOUND: typeof COMPOUND;
|
|
172
|
+
Term: typeof Term;
|
|
173
|
+
Env: typeof Env;
|
|
69
174
|
Program: typeof Program;
|
|
175
|
+
Solver: typeof Solver;
|
|
176
|
+
BuiltinRegistry: typeof BuiltinRegistry;
|
|
177
|
+
variable: typeof variable;
|
|
178
|
+
atom: typeof atom;
|
|
179
|
+
stringTerm: typeof stringTerm;
|
|
180
|
+
numberTerm: typeof numberTerm;
|
|
181
|
+
compound: typeof compound;
|
|
182
|
+
emptyList: typeof emptyList;
|
|
183
|
+
cons: typeof cons;
|
|
184
|
+
deref: typeof deref;
|
|
185
|
+
isScalar: typeof isScalar;
|
|
186
|
+
isEmptyList: typeof isEmptyList;
|
|
187
|
+
isCons: typeof isCons;
|
|
188
|
+
isConjunction: typeof isConjunction;
|
|
189
|
+
unify: typeof unify;
|
|
190
|
+
cloneTerm: typeof cloneTerm;
|
|
191
|
+
freshTerm: typeof freshTerm;
|
|
192
|
+
copyResolved: typeof copyResolved;
|
|
193
|
+
termIsGround: typeof termIsGround;
|
|
194
|
+
termToString: typeof termToString;
|
|
195
|
+
lexicalValue: typeof lexicalValue;
|
|
196
|
+
properListItems: typeof properListItems;
|
|
197
|
+
listFromItems: typeof listFromItems;
|
|
198
|
+
flattenConjunction: typeof flattenConjunction;
|
|
199
|
+
termSignature: typeof termSignature;
|
|
200
|
+
variantTerms: typeof variantTerms;
|
|
201
|
+
compareTerms: typeof compareTerms;
|
|
202
|
+
isDecimalInteger: typeof isDecimalInteger;
|
|
203
|
+
compareIntegerText: typeof compareIntegerText;
|
|
204
|
+
parseFiniteNumber: typeof parseFiniteNumber;
|
|
205
|
+
numberTextFromDouble: typeof numberTextFromDouble;
|
|
206
|
+
compareNumberText: typeof compareNumberText;
|
|
70
207
|
makeProgram: typeof makeProgram;
|
|
71
208
|
parseClauses: typeof parseClauses;
|
|
72
209
|
parseProgramText: typeof parseProgramText;
|
|
73
|
-
Solver: typeof Solver;
|
|
74
|
-
Env: typeof Env;
|
|
75
|
-
BuiltinRegistry: typeof BuiltinRegistry;
|
|
76
210
|
createDefaultRegistry: typeof createDefaultRegistry;
|
|
77
211
|
getDefaultRegistry: typeof getDefaultRegistry;
|
|
212
|
+
run: typeof run;
|
|
213
|
+
whyProof: typeof whyProof;
|
|
214
|
+
whyNoProof: typeof whyNoProof;
|
|
215
|
+
explainProof: typeof explainProof;
|
|
78
216
|
};
|
|
79
217
|
|
|
80
218
|
export default eyelang;
|
package/package.json
CHANGED
package/test/run-regression.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import os from 'node:os';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import * as publicApi from '../src/index.js';
|
|
11
12
|
import {
|
|
12
13
|
run,
|
|
13
14
|
Program,
|
|
@@ -50,6 +51,7 @@ export function runRegression(reporter = new TestReporter()) {
|
|
|
50
51
|
|
|
51
52
|
try {
|
|
52
53
|
runSection(reporter, 'Regression', regressionCases());
|
|
54
|
+
runSection(reporter, 'Documentation sync', documentationSyncCases());
|
|
53
55
|
runSection(reporter, 'API', apiCases());
|
|
54
56
|
runSection(reporter, 'White-box', whiteBoxCases());
|
|
55
57
|
} finally {
|
|
@@ -254,8 +256,44 @@ why(
|
|
|
254
256
|
];
|
|
255
257
|
}
|
|
256
258
|
|
|
259
|
+
|
|
260
|
+
function documentationSyncCases() {
|
|
261
|
+
return [
|
|
262
|
+
{
|
|
263
|
+
name: 'language reference builtins match runtime registry',
|
|
264
|
+
run: () => assertArrayEqual(languageReferenceBuiltinNames(), registeredBuiltinNames(), 'builtins'),
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: 'guide builtin catalog matches runtime registry',
|
|
268
|
+
run: () => {
|
|
269
|
+
assertArrayEqual(guideBuiltinNames(), registeredBuiltinNames(), 'builtins');
|
|
270
|
+
const summary = guideBuiltinSummary();
|
|
271
|
+
const actual = registeredBuiltinSummary();
|
|
272
|
+
assertEqual(summary.entries, actual.entries, 'builtin entry count');
|
|
273
|
+
assertEqual(summary.names, actual.names, 'builtin predicate name count');
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'guide example catalog matches examples directory',
|
|
278
|
+
run: () => assertArrayEqual(guideCatalogExampleNames(), listExampleNames(), 'example catalog'),
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: 'documentation local links and anchors resolve',
|
|
282
|
+
run: () => assertArrayEqual(findBrokenDocLinks(), [], 'broken documentation links'),
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: 'documented npm scripts exist in package.json',
|
|
286
|
+
run: () => assertArrayEqual(missingDocumentedPackageScripts(), [], 'missing documented npm scripts'),
|
|
287
|
+
},
|
|
288
|
+
];
|
|
289
|
+
}
|
|
290
|
+
|
|
257
291
|
function apiCases() {
|
|
258
292
|
return [
|
|
293
|
+
{
|
|
294
|
+
name: 'public type declarations match runtime exports',
|
|
295
|
+
run: () => assertArrayEqual(declaredValueExportNames(), runtimeExportNames(), 'public value exports'),
|
|
296
|
+
},
|
|
259
297
|
{
|
|
260
298
|
name: 'run materialization through public API without proof by default',
|
|
261
299
|
run: () => {
|
|
@@ -455,6 +493,7 @@ function runSection(reporter, name, cases) {
|
|
|
455
493
|
}
|
|
456
494
|
|
|
457
495
|
function sectionLabel(name) {
|
|
496
|
+
if (name === 'Documentation sync') return 'documentation sync';
|
|
458
497
|
if (name === 'API') return 'API';
|
|
459
498
|
if (name === 'White-box') return 'white-box';
|
|
460
499
|
return name.toLowerCase();
|
|
@@ -499,9 +538,9 @@ function listExampleNames() {
|
|
|
499
538
|
.sort();
|
|
500
539
|
}
|
|
501
540
|
|
|
502
|
-
function
|
|
503
|
-
const
|
|
504
|
-
const section = between(
|
|
541
|
+
function guideCatalogExampleNames() {
|
|
542
|
+
const guide = fs.readFileSync(path.join(packageRoot, 'docs', 'guide.md'), 'utf8');
|
|
543
|
+
const section = between(guide, '## Example catalog', '## Golden outputs, tests, and conformance');
|
|
505
544
|
return [...section.matchAll(/examples\/([A-Za-z0-9_-]+)\.pl/g)]
|
|
506
545
|
.map((match) => match[1])
|
|
507
546
|
.filter((name, index, names) => names.indexOf(name) === index)
|
|
@@ -512,56 +551,137 @@ function registeredBuiltinNames() {
|
|
|
512
551
|
return [...createDefaultRegistry().defs.keys()].sort();
|
|
513
552
|
}
|
|
514
553
|
|
|
515
|
-
function
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
.map((
|
|
520
|
-
|
|
521
|
-
|
|
554
|
+
function registeredBuiltinSummary() {
|
|
555
|
+
const names = registeredBuiltinNames();
|
|
556
|
+
return {
|
|
557
|
+
entries: names.length,
|
|
558
|
+
names: new Set(names.map((name) => name.split('/')[0])).size,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function guideBuiltinNames() {
|
|
563
|
+
const guide = fs.readFileSync(path.join(packageRoot, 'docs', 'guide.md'), 'utf8');
|
|
564
|
+
return documentedBuiltinNames(between(guide, '### Builtins', '## Aggregation helpers'));
|
|
522
565
|
}
|
|
523
566
|
|
|
524
|
-
function
|
|
525
|
-
const
|
|
526
|
-
const section = between(
|
|
567
|
+
function guideBuiltinSummary() {
|
|
568
|
+
const guide = fs.readFileSync(path.join(packageRoot, 'docs', 'guide.md'), 'utf8');
|
|
569
|
+
const section = between(guide, '### Builtins', '## Aggregation helpers');
|
|
527
570
|
const match = section.match(/currently registers (\d+) name\/arity entries across (\d+) predicate names/);
|
|
528
|
-
if (match == null) throw new Error('
|
|
571
|
+
if (match == null) throw new Error('guide builtin summary not found');
|
|
529
572
|
return { entries: Number(match[1]), names: Number(match[2]) };
|
|
530
573
|
}
|
|
531
574
|
|
|
532
|
-
function
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
575
|
+
function languageReferenceBuiltinNames() {
|
|
576
|
+
const reference = fs.readFileSync(path.join(packageRoot, 'docs', 'language-reference.md'), 'utf8');
|
|
577
|
+
return documentedBuiltinNames(between(reference, '## 9. Standard built-in predicates', '## 10. Implementation-specific built-ins'));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function documentedBuiltinNames(section) {
|
|
581
|
+
const names = [];
|
|
582
|
+
for (const line of section.split('\n')) {
|
|
583
|
+
if (!line.trim().startsWith('|') || !line.includes('`')) continue;
|
|
584
|
+
for (const match of line.matchAll(/`([A-Za-z_][A-Za-z0-9_]*)\(([^`)]*)\)`/g)) {
|
|
585
|
+
const arity = match[2].trim() === '' ? 0 : match[2].split(',').length;
|
|
586
|
+
names.push(`${match[1]}/${arity}`);
|
|
587
|
+
}
|
|
588
|
+
for (const match of line.matchAll(/`([A-Za-z_][A-Za-z0-9_]*)\/(\d+)`/g)) {
|
|
589
|
+
names.push(`${match[1]}/${match[2]}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return [...new Set(names)].sort();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function runtimeExportNames() {
|
|
596
|
+
return Object.keys(publicApi).sort();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function declaredValueExportNames() {
|
|
600
|
+
const dts = fs.readFileSync(path.join(packageRoot, 'index.d.ts'), 'utf8');
|
|
601
|
+
return [...dts.matchAll(/^export\s+(?:declare\s+)?(?:class|function|const)\s+([A-Za-z_][A-Za-z0-9_]*)/gm)]
|
|
537
602
|
.map((match) => match[1])
|
|
603
|
+
.filter((name, index, names) => names.indexOf(name) === index)
|
|
538
604
|
.sort();
|
|
539
605
|
}
|
|
540
606
|
|
|
541
|
-
function
|
|
542
|
-
const
|
|
543
|
-
const
|
|
544
|
-
const
|
|
545
|
-
while (stack.length > 0) {
|
|
546
|
-
const file = stack.pop();
|
|
547
|
-
if (seen.has(file)) continue;
|
|
548
|
-
seen.add(file);
|
|
607
|
+
function missingDocumentedPackageScripts() {
|
|
608
|
+
const docs = documentationFiles();
|
|
609
|
+
const missing = [];
|
|
610
|
+
for (const file of docs) {
|
|
549
611
|
const text = fs.readFileSync(file, 'utf8');
|
|
550
|
-
for (const
|
|
551
|
-
|
|
552
|
-
if (!
|
|
553
|
-
const
|
|
554
|
-
|
|
612
|
+
for (const line of text.split('\n')) {
|
|
613
|
+
const trimmed = line.trim();
|
|
614
|
+
if (!trimmed.startsWith('npm ') && !line.includes('`npm ')) continue;
|
|
615
|
+
for (const match of line.matchAll(/\bnpm\s+(?:run\s+)?([A-Za-z0-9:_-]+)/g)) {
|
|
616
|
+
const command = match[1];
|
|
617
|
+
if (command === 'install') continue;
|
|
618
|
+
const script = command === 'test' ? 'test' : command;
|
|
619
|
+
if (!pkg.scripts?.[script]) missing.push(`${path.relative(packageRoot, file)}: npm ${command === 'test' ? 'test' : `run ${script}`}`);
|
|
620
|
+
}
|
|
555
621
|
}
|
|
556
622
|
}
|
|
557
|
-
return [...
|
|
623
|
+
return [...new Set(missing)].sort();
|
|
558
624
|
}
|
|
559
625
|
|
|
560
|
-
function
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
for (const
|
|
564
|
-
|
|
626
|
+
function findBrokenDocLinks() {
|
|
627
|
+
const broken = [];
|
|
628
|
+
const anchorsByFile = new Map();
|
|
629
|
+
for (const file of documentationFiles()) {
|
|
630
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
631
|
+
for (const target of markdownLinkTargets(text)) {
|
|
632
|
+
if (/^(?:https?:|mailto:)/i.test(target)) continue;
|
|
633
|
+
const [targetPathRaw, fragmentRaw] = target.split('#');
|
|
634
|
+
const targetPath = targetPathRaw === '' ? file : path.resolve(path.dirname(file), decodeURI(targetPathRaw));
|
|
635
|
+
const display = `${path.relative(packageRoot, file)} -> ${target}`;
|
|
636
|
+
if (!fs.existsSync(targetPath)) {
|
|
637
|
+
broken.push(`${display} (missing target)`);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (fragmentRaw != null && fragmentRaw !== '') {
|
|
641
|
+
const anchors = anchorsByFile.get(targetPath) ?? markdownAnchors(targetPath);
|
|
642
|
+
anchorsByFile.set(targetPath, anchors);
|
|
643
|
+
if (!anchors.has(fragmentRaw)) broken.push(`${display} (missing heading #${fragmentRaw})`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return broken.sort();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function documentationFiles() {
|
|
651
|
+
return [
|
|
652
|
+
path.join(packageRoot, 'README.md'),
|
|
653
|
+
path.join(packageRoot, 'docs', 'guide.md'),
|
|
654
|
+
path.join(packageRoot, 'docs', 'language-reference.md'),
|
|
655
|
+
];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function markdownLinkTargets(text) {
|
|
659
|
+
return [...text.matchAll(/!?\[[^\]\n]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)].map((match) => match[1]);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function markdownAnchors(file) {
|
|
663
|
+
if (!file.endsWith('.md')) return new Set();
|
|
664
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
665
|
+
const anchors = new Set();
|
|
666
|
+
const counts = new Map();
|
|
667
|
+
for (const match of text.matchAll(/^#{1,6}\s+(.+)$/gm)) {
|
|
668
|
+
const base = githubSlug(match[1]);
|
|
669
|
+
const count = counts.get(base) ?? 0;
|
|
670
|
+
counts.set(base, count + 1);
|
|
671
|
+
anchors.add(count === 0 ? base : `${base}-${count}`);
|
|
672
|
+
}
|
|
673
|
+
return anchors;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function githubSlug(heading) {
|
|
677
|
+
return heading
|
|
678
|
+
.replace(/`([^`]*)`/g, '$1')
|
|
679
|
+
.replace(/<[^>]+>/g, '')
|
|
680
|
+
.trim()
|
|
681
|
+
.toLowerCase()
|
|
682
|
+
.replace(/[^\p{Letter}\p{Number}\s-]/gu, '')
|
|
683
|
+
.trim()
|
|
684
|
+
.replace(/\s+/g, '-');
|
|
565
685
|
}
|
|
566
686
|
|
|
567
687
|
function between(text, startMarker, endMarker) {
|
|
@@ -594,6 +714,16 @@ function assertNotIncludes(actual, expected, label) {
|
|
|
594
714
|
if (String(actual).includes(expected)) throw new Error(`${label} unexpectedly included ${format(expected)}\nactual: ${format(actual)}`);
|
|
595
715
|
}
|
|
596
716
|
|
|
717
|
+
function assertArrayEqual(actual, expected, label) {
|
|
718
|
+
const actualText = actual.join('\n');
|
|
719
|
+
const expectedText = expected.join('\n');
|
|
720
|
+
if (actualText !== expectedText) {
|
|
721
|
+
const onlyActual = actual.filter((item) => !expected.includes(item));
|
|
722
|
+
const onlyExpected = expected.filter((item) => !actual.includes(item));
|
|
723
|
+
throw new Error(`${label} mismatch\nonly actual: ${format(onlyActual)}\nonly expected: ${format(onlyExpected)}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
597
727
|
function format(value) {
|
|
598
728
|
return typeof value === 'string' ? JSON.stringify(value) : String(value);
|
|
599
729
|
}
|