@polydeukes/core 0.5.0 → 0.6.1
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.ko.md +50 -32
- package/README.md +50 -32
- package/dist/algebra.d.ts +174 -0
- package/dist/algebra.js +438 -0
- package/dist/catalogue.d.ts +69 -0
- package/dist/catalogue.js +191 -0
- package/dist/config.d.ts +15 -42
- package/dist/config.js +26 -164
- package/dist/exit-codes.d.ts +3 -3
- package/dist/exit-codes.js +3 -3
- package/dist/index.d.ts +10 -94
- package/dist/index.js +4 -53
- package/dist/is-plain-object.d.ts +3 -0
- package/dist/is-plain-object.js +3 -0
- package/dist/protocol.d.ts +152 -0
- package/dist/protocol.js +121 -0
- package/dist/source-names.d.ts +10 -0
- package/dist/source-names.js +18 -0
- package/dist/telemetry.d.ts +32 -5
- package/dist/telemetry.js +59 -8
- package/dist/transcript.d.ts +2 -9
- package/dist/transcript.js +1 -6
- package/dist/validation.d.ts +24 -0
- package/dist/validation.js +44 -0
- package/package.json +3 -2
- package/schema/algebra-declaration.schema.json +402 -0
- package/schema/polydeukes.schema.json +38 -91
package/dist/telemetry.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ROI telemetry — the single shared collector and its `gain` aggregation.
|
|
3
3
|
*
|
|
4
|
-
* One record is one line of
|
|
4
|
+
* One record is one line of TSV — four fields, or five when a judgment names witnesses;
|
|
5
|
+
* one append is one write call. I/O is confined to
|
|
5
6
|
* exactly two functions — {@link appendRecord} (the only write) and {@link readRecords}
|
|
6
7
|
* (the only read). Formatting, parsing, and aggregation are pure.
|
|
7
8
|
*/
|
|
8
9
|
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
9
10
|
import { dirname } from 'node:path';
|
|
11
|
+
/**
|
|
12
|
+
* Why a `skipped` row records no judgment, closed: the surface has no observation channel
|
|
13
|
+
* for what the entry reads, assembly could not compile the entry, or the declaration's own
|
|
14
|
+
* `supply: pass` let an absent source through.
|
|
15
|
+
*/
|
|
16
|
+
export const SKIP_REASONS = ['no-observation', 'config-fault', 'supply-pass'];
|
|
10
17
|
const TAB = '\t';
|
|
11
18
|
const VALID_EVENTS = [
|
|
12
19
|
'passed',
|
|
@@ -36,23 +43,55 @@ const LEGACY_WITNESSED_EVENT = 'bypassed';
|
|
|
36
43
|
function sanitize(value) {
|
|
37
44
|
return value.replace(/[\t\n\r]/g, ' ');
|
|
38
45
|
}
|
|
46
|
+
/** True when `text` parses as a JSON array — the only shape the fifth field takes. */
|
|
47
|
+
function isJsonArray(text) {
|
|
48
|
+
try {
|
|
49
|
+
return Array.isArray(JSON.parse(text));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
39
55
|
/**
|
|
40
56
|
* Serialize a {@link TelemetryRecord} into one newline-terminated TSV line (pure).
|
|
41
57
|
*
|
|
42
58
|
* The returned string already includes the trailing `\n`, so {@link appendRecord}
|
|
43
|
-
* writes it verbatim in a single call.
|
|
59
|
+
* writes it verbatim in a single call. The fifth field appears only when the record
|
|
60
|
+
* carries `witnesses` or a skip `reason`; a record without either writes the four-field
|
|
61
|
+
* line unchanged.
|
|
62
|
+
*
|
|
63
|
+
* Throws when a record claims the fifth field twice, or claims it as a reason on an event
|
|
64
|
+
* that is not `skipped`: both are assembly errors, and writing either one would produce a
|
|
65
|
+
* row no reader can take apart.
|
|
44
66
|
*/
|
|
45
67
|
export function formatRecordLine(record) {
|
|
68
|
+
if (record.reason !== undefined) {
|
|
69
|
+
if (record.event !== 'skipped') {
|
|
70
|
+
throw new Error(`a skip reason belongs to a skipped row, not to '${record.event}'`);
|
|
71
|
+
}
|
|
72
|
+
if (record.witnesses !== undefined) {
|
|
73
|
+
throw new Error('a record carries either witnesses or a skip reason, never both');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
46
76
|
const fields = [record.timestamp, record.event, sanitize(record.label), sanitize(record.subject)];
|
|
77
|
+
if (record.witnesses !== undefined) {
|
|
78
|
+
fields.push(sanitize(record.witnesses));
|
|
79
|
+
}
|
|
80
|
+
else if (record.reason !== undefined) {
|
|
81
|
+
fields.push(record.reason);
|
|
82
|
+
}
|
|
47
83
|
return `${fields.join(TAB)}\n`;
|
|
48
84
|
}
|
|
49
85
|
/**
|
|
50
86
|
* Parse one TSV line back into a {@link TelemetryRecord}, or `null` if malformed (pure).
|
|
51
87
|
*
|
|
52
88
|
* Tolerates a trailing newline (so it round-trips {@link formatRecordLine}). Returns
|
|
53
|
-
* `null` for
|
|
54
|
-
* empty line — a malformed line is rejected, never
|
|
55
|
-
*
|
|
89
|
+
* `null` for a field count outside four (no fifth field) and five (with one), an event
|
|
90
|
+
* outside the six valid events, or an empty line — a malformed line is rejected, never
|
|
91
|
+
* coerced into a bogus record. The fifth field is read per event: on `skipped` it is a
|
|
92
|
+
* token of {@link SKIP_REASONS} and nothing else, on every other event a JSON array of
|
|
93
|
+
* witnesses. The one exception is {@link LEGACY_WITNESSED_EVENT}, which reads back as
|
|
94
|
+
* `witnessed`.
|
|
56
95
|
*/
|
|
57
96
|
export function parseRecordLine(line) {
|
|
58
97
|
const trimmed = line.replace(/\n$/, '');
|
|
@@ -60,15 +99,27 @@ export function parseRecordLine(line) {
|
|
|
60
99
|
return null;
|
|
61
100
|
}
|
|
62
101
|
const fields = trimmed.split(TAB);
|
|
63
|
-
if (fields.length !== 4) {
|
|
102
|
+
if (fields.length !== 4 && fields.length !== 5) {
|
|
64
103
|
return null;
|
|
65
104
|
}
|
|
66
|
-
const [timestamp, event, label, subject] = fields;
|
|
105
|
+
const [timestamp, event, label, subject, fifth] = fields;
|
|
67
106
|
const resolved = event === LEGACY_WITNESSED_EVENT ? 'witnessed' : event;
|
|
68
107
|
if (!VALID_EVENTS.includes(resolved)) {
|
|
69
108
|
return null;
|
|
70
109
|
}
|
|
71
|
-
|
|
110
|
+
const record = { timestamp, event: resolved, label, subject };
|
|
111
|
+
if (fifth === undefined) {
|
|
112
|
+
return record;
|
|
113
|
+
}
|
|
114
|
+
if (resolved === 'skipped') {
|
|
115
|
+
// A skip carries no witness, so a JSON array here is as corrupt as a near-miss token.
|
|
116
|
+
return SKIP_REASONS.includes(fifth)
|
|
117
|
+
? { ...record, reason: fifth }
|
|
118
|
+
: null;
|
|
119
|
+
}
|
|
120
|
+
// The fifth field is a JSON array or the line is corrupt: a stray tab inside a subject
|
|
121
|
+
// would otherwise read as a record with the wrong subject and no witnesses.
|
|
122
|
+
return isJsonArray(fifth) ? { ...record, witnesses: fifth } : null;
|
|
72
123
|
}
|
|
73
124
|
/**
|
|
74
125
|
* Append one record to the log at `path` — the only write I/O.
|
package/dist/transcript.d.ts
CHANGED
|
@@ -6,11 +6,7 @@
|
|
|
6
6
|
* one source it can wrap. Concrete transcript formats stay in adapters; the core knows only
|
|
7
7
|
* the query vocabulary. Pure types and functions, zero I/O.
|
|
8
8
|
*/
|
|
9
|
-
import type { CovenantInput } from './
|
|
10
|
-
/** One subagent invocation observed in the session. `kind` is an adapter-supplied value. */
|
|
11
|
-
export type SubagentInvocation = {
|
|
12
|
-
kind: string;
|
|
13
|
-
};
|
|
9
|
+
import type { CovenantInput } from './protocol.ts';
|
|
14
10
|
/**
|
|
15
11
|
* One user message observed in the session.
|
|
16
12
|
*
|
|
@@ -43,8 +39,6 @@ export type TranscriptToolCall = {
|
|
|
43
39
|
* to the consumer.
|
|
44
40
|
*/
|
|
45
41
|
export type CanonicalTranscript = {
|
|
46
|
-
/** Invocations of the given kind, or all of them when omitted. Observation order preserved. */
|
|
47
|
-
findSubagentInvocations(kind?: string): SubagentInvocation[];
|
|
48
42
|
/** Every user message, observation order preserved. Missing timestampMs = freshness unprovable. */
|
|
49
43
|
findUserMessages(): TranscriptUserMessage[];
|
|
50
44
|
/** Tool calls with the given name, or all when omitted. Observation order preserved. */
|
|
@@ -59,8 +53,7 @@ export declare const noopTranscript: CanonicalTranscript;
|
|
|
59
53
|
/**
|
|
60
54
|
* Wrap a {@link CovenantInput} as a {@link CanonicalTranscript}.
|
|
61
55
|
*
|
|
62
|
-
* Exposes `
|
|
63
|
-
* `userMessages` with `timestampMs` omitted — the bare IR cannot prove freshness,
|
|
56
|
+
* Exposes `userMessages` with `timestampMs` omitted — the bare IR cannot prove freshness,
|
|
64
57
|
* and that absence is the *correct* fail-closed signal for a witness consumer.
|
|
65
58
|
* Order preserved; the input is never mutated, and every query returns fresh
|
|
66
59
|
* objects so consumers never hold live aliases into the shared IR.
|
package/dist/transcript.js
CHANGED
|
@@ -12,15 +12,13 @@
|
|
|
12
12
|
* so does a precedent consumer (no evidence, gate stays shut).
|
|
13
13
|
*/
|
|
14
14
|
export const noopTranscript = {
|
|
15
|
-
findSubagentInvocations: () => [],
|
|
16
15
|
findUserMessages: () => [],
|
|
17
16
|
findToolCalls: () => [],
|
|
18
17
|
};
|
|
19
18
|
/**
|
|
20
19
|
* Wrap a {@link CovenantInput} as a {@link CanonicalTranscript}.
|
|
21
20
|
*
|
|
22
|
-
* Exposes `
|
|
23
|
-
* `userMessages` with `timestampMs` omitted — the bare IR cannot prove freshness,
|
|
21
|
+
* Exposes `userMessages` with `timestampMs` omitted — the bare IR cannot prove freshness,
|
|
24
22
|
* and that absence is the *correct* fail-closed signal for a witness consumer.
|
|
25
23
|
* Order preserved; the input is never mutated, and every query returns fresh
|
|
26
24
|
* objects so consumers never hold live aliases into the shared IR.
|
|
@@ -32,9 +30,6 @@ export const noopTranscript = {
|
|
|
32
30
|
*/
|
|
33
31
|
export function transcriptFromInput(input) {
|
|
34
32
|
return {
|
|
35
|
-
findSubagentInvocations: (kind) => input.subagentSpawns
|
|
36
|
-
.filter((spawn) => kind === undefined || spawn.kind === kind)
|
|
37
|
-
.map((spawn) => ({ kind: spawn.kind })),
|
|
38
33
|
findUserMessages: () => input.userMessages.map((message) => ({ text: message.text })),
|
|
39
34
|
findToolCalls: (name) => input.toolCalls
|
|
40
35
|
.filter((call) => name === undefined || call.name === name)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `validation.ts` — the shape-checking primitives the core's validators share.
|
|
3
|
+
*
|
|
4
|
+
* Internal to the core: `config.ts` and `algebra.ts` both build on these, and neither
|
|
5
|
+
* owns them. Not a public export — `isPlainObject` has its own file because it *is* one.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* `ConfigValidationError` — raised when a config fails structural validation.
|
|
9
|
+
*
|
|
10
|
+
* The message names the offending field path so the developer sees exactly what is wrong.
|
|
11
|
+
* This throw is a developer-time error (config authoring), a different axis from the
|
|
12
|
+
* covenant runtime's fail-closed exit code — a bad config should fail loud and early.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ConfigValidationError extends Error {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
/** Throw on the first key outside the allowed vocabulary, naming the key and its location. */
|
|
18
|
+
export declare function rejectUnknownKeys(record: Record<string, unknown>, allowed: ReadonlySet<string>, location: string): void;
|
|
19
|
+
/** True when the value is a string with at least one character. */
|
|
20
|
+
export declare function isNonEmptyString(value: unknown): value is string;
|
|
21
|
+
/** True when the value is an array whose every element is a string. */
|
|
22
|
+
export declare function isStringArray(value: unknown): value is string[];
|
|
23
|
+
/** Throw unless the pattern string compiles with `new RegExp` — compilability only, never run. */
|
|
24
|
+
export declare function rejectUncompilableRegex(pattern: string, location: string): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `validation.ts` — the shape-checking primitives the core's validators share.
|
|
3
|
+
*
|
|
4
|
+
* Internal to the core: `config.ts` and `algebra.ts` both build on these, and neither
|
|
5
|
+
* owns them. Not a public export — `isPlainObject` has its own file because it *is* one.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* `ConfigValidationError` — raised when a config fails structural validation.
|
|
9
|
+
*
|
|
10
|
+
* The message names the offending field path so the developer sees exactly what is wrong.
|
|
11
|
+
* This throw is a developer-time error (config authoring), a different axis from the
|
|
12
|
+
* covenant runtime's fail-closed exit code — a bad config should fail loud and early.
|
|
13
|
+
*/
|
|
14
|
+
export class ConfigValidationError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'ConfigValidationError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Throw on the first key outside the allowed vocabulary, naming the key and its location. */
|
|
21
|
+
export function rejectUnknownKeys(record, allowed, location) {
|
|
22
|
+
for (const key of Object.keys(record)) {
|
|
23
|
+
if (!allowed.has(key)) {
|
|
24
|
+
throw new ConfigValidationError(`unknown key '${key}' in ${location}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** True when the value is a string with at least one character. */
|
|
29
|
+
export function isNonEmptyString(value) {
|
|
30
|
+
return typeof value === 'string' && value.length > 0;
|
|
31
|
+
}
|
|
32
|
+
/** True when the value is an array whose every element is a string. */
|
|
33
|
+
export function isStringArray(value) {
|
|
34
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
|
|
35
|
+
}
|
|
36
|
+
/** Throw unless the pattern string compiles with `new RegExp` — compilability only, never run. */
|
|
37
|
+
export function rejectUncompilableRegex(pattern, location) {
|
|
38
|
+
try {
|
|
39
|
+
new RegExp(pattern);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new ConfigValidationError(`${location} must be a compilable regular expression`);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polydeukes/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Polydeukes core — covenant protocol, config loader, and transcript interface. Domain- and agent-agnostic. Alpha.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
18
|
"import": "./dist/index.js"
|
|
19
19
|
},
|
|
20
|
-
"./schema.json": "./schema/polydeukes.schema.json"
|
|
20
|
+
"./schema.json": "./schema/polydeukes.schema.json",
|
|
21
|
+
"./algebra-declaration.schema.json": "./schema/algebra-declaration.schema.json"
|
|
21
22
|
},
|
|
22
23
|
"files": [
|
|
23
24
|
"dist",
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/huskyhoochu/polydeukes/schema/algebra-declaration.schema.json",
|
|
4
|
+
"title": "Polydeukes algebra declaration",
|
|
5
|
+
"description": "One algebra declaration: five blocks, a relation position closed to seven names, a binary combinator position closed to three, and an open unary extraction vocabulary. Reference resolution, relate-entry id uniqueness, and cycle detection are validator-only.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["discipline", "mechanism", "extract", "relate"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"discipline": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"minLength": 1
|
|
13
|
+
},
|
|
14
|
+
"mechanism": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"enum": [
|
|
17
|
+
"pairing",
|
|
18
|
+
"companion",
|
|
19
|
+
"monotonic-order",
|
|
20
|
+
"fingerprint-sync",
|
|
21
|
+
"producer-owned",
|
|
22
|
+
"self-absolution-ban",
|
|
23
|
+
"actor-scope",
|
|
24
|
+
"precedent",
|
|
25
|
+
"phase-order",
|
|
26
|
+
"turn-locality",
|
|
27
|
+
"stated-ground",
|
|
28
|
+
"controlled-vocabulary",
|
|
29
|
+
"naming",
|
|
30
|
+
"added-only",
|
|
31
|
+
"one-way-marker",
|
|
32
|
+
"scoped-valve",
|
|
33
|
+
"forbidden-command"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"scope": {
|
|
37
|
+
"$ref": "#/$defs/scope"
|
|
38
|
+
},
|
|
39
|
+
"sources": {
|
|
40
|
+
"$ref": "#/$defs/sources"
|
|
41
|
+
},
|
|
42
|
+
"supply": {
|
|
43
|
+
"$ref": "#/$defs/supply"
|
|
44
|
+
},
|
|
45
|
+
"extract": {
|
|
46
|
+
"$ref": "#/$defs/extract"
|
|
47
|
+
},
|
|
48
|
+
"relate": {
|
|
49
|
+
"$ref": "#/$defs/relate"
|
|
50
|
+
},
|
|
51
|
+
"witness": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"additionalProperties": false,
|
|
54
|
+
"required": ["relate"],
|
|
55
|
+
"properties": {
|
|
56
|
+
"extract": {
|
|
57
|
+
"$ref": "#/$defs/extract"
|
|
58
|
+
},
|
|
59
|
+
"relate": {
|
|
60
|
+
"$ref": "#/$defs/relate"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"$defs": {
|
|
66
|
+
"extractName": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"minLength": 1
|
|
69
|
+
},
|
|
70
|
+
"scope": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"additionalProperties": false,
|
|
73
|
+
"required": ["source"],
|
|
74
|
+
"properties": {
|
|
75
|
+
"source": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"minLength": 1,
|
|
78
|
+
"not": { "enum": ["changes", "state", "actor"] }
|
|
79
|
+
},
|
|
80
|
+
"include": {
|
|
81
|
+
"type": "array",
|
|
82
|
+
"items": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"format": "regex"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"exclude": {
|
|
88
|
+
"type": "array",
|
|
89
|
+
"items": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"format": "regex"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"excludeIgnoreCase": {
|
|
95
|
+
"type": "boolean"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"sources": {
|
|
100
|
+
"description": "What each name outside the target stands for, under the name a declaration reads it by. The kind position is closed to 'file', 'sidecar' and 'transcript', exactly one per entry: a file path is repo-relative, so a leading '/' and a '..' segment are refused, and a sidecar channel's or a transcript's value is the marker true because its location is the host's fact.",
|
|
101
|
+
"type": "object",
|
|
102
|
+
"propertyNames": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"minLength": 1,
|
|
105
|
+
"not": {
|
|
106
|
+
"enum": ["target.path", "pre", "post", "state", "changes", "command", "actor"]
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"additionalProperties": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"oneOf": [
|
|
112
|
+
{
|
|
113
|
+
"additionalProperties": false,
|
|
114
|
+
"required": ["file"],
|
|
115
|
+
"properties": {
|
|
116
|
+
"file": {
|
|
117
|
+
"type": "string",
|
|
118
|
+
"minLength": 1,
|
|
119
|
+
"pattern": "^(?!/)(?!(.*/)?\\.\\.(/|$))"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"additionalProperties": false,
|
|
125
|
+
"required": ["sidecar"],
|
|
126
|
+
"properties": {
|
|
127
|
+
"sidecar": {
|
|
128
|
+
"const": true
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"additionalProperties": false,
|
|
134
|
+
"required": ["transcript"],
|
|
135
|
+
"properties": {
|
|
136
|
+
"transcript": {
|
|
137
|
+
"const": true
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"supply": {
|
|
145
|
+
"type": "object",
|
|
146
|
+
"properties": {
|
|
147
|
+
"state": {
|
|
148
|
+
"enum": ["error", "pass"]
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
"additionalProperties": {
|
|
152
|
+
"enum": ["error", "pass", "empty"]
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"extract": {
|
|
156
|
+
"type": "object",
|
|
157
|
+
"additionalProperties": {
|
|
158
|
+
"$ref": "#/$defs/pipeline"
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"pipeline": {
|
|
162
|
+
"description": "A combinator stands only as the first step: prefixItems governs position 0, items governs the rest.",
|
|
163
|
+
"type": "array",
|
|
164
|
+
"minItems": 1,
|
|
165
|
+
"prefixItems": [
|
|
166
|
+
{
|
|
167
|
+
"$ref": "#/$defs/step"
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
"items": {
|
|
171
|
+
"$ref": "#/$defs/unaryStep"
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"step": {
|
|
175
|
+
"oneOf": [
|
|
176
|
+
{
|
|
177
|
+
"$ref": "#/$defs/combinatorStep"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"$ref": "#/$defs/unaryStep"
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
},
|
|
184
|
+
"combinatorStep": {
|
|
185
|
+
"type": "object",
|
|
186
|
+
"required": ["op"],
|
|
187
|
+
"properties": {
|
|
188
|
+
"op": {
|
|
189
|
+
"enum": ["union", "onlyIn", "intersect"]
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"oneOf": [
|
|
193
|
+
{
|
|
194
|
+
"additionalProperties": false,
|
|
195
|
+
"required": ["op", "of"],
|
|
196
|
+
"properties": {
|
|
197
|
+
"op": {
|
|
198
|
+
"enum": ["union", "intersect"]
|
|
199
|
+
},
|
|
200
|
+
"of": {
|
|
201
|
+
"type": "array",
|
|
202
|
+
"minItems": 2,
|
|
203
|
+
"maxItems": 2,
|
|
204
|
+
"items": {
|
|
205
|
+
"$ref": "#/$defs/extractName"
|
|
206
|
+
},
|
|
207
|
+
"uniqueItems": true
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"additionalProperties": false,
|
|
213
|
+
"required": ["op", "of", "notIn"],
|
|
214
|
+
"properties": {
|
|
215
|
+
"op": {
|
|
216
|
+
"const": "onlyIn"
|
|
217
|
+
},
|
|
218
|
+
"of": {
|
|
219
|
+
"$ref": "#/$defs/extractName"
|
|
220
|
+
},
|
|
221
|
+
"notIn": {
|
|
222
|
+
"$ref": "#/$defs/extractName"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
},
|
|
228
|
+
"unaryStep": {
|
|
229
|
+
"description": "Open vocabulary: any name outside the three combinators, arguments unconstrained — except that referencing two extractions (an array `of`, or a `notIn`) makes the step a combinator by shape.",
|
|
230
|
+
"type": "object",
|
|
231
|
+
"required": ["op"],
|
|
232
|
+
"properties": {
|
|
233
|
+
"op": {
|
|
234
|
+
"type": "string",
|
|
235
|
+
"minLength": 1,
|
|
236
|
+
"not": {
|
|
237
|
+
"enum": ["union", "onlyIn", "intersect"]
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
"of": {
|
|
241
|
+
"not": {
|
|
242
|
+
"type": "array"
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
"not": {
|
|
247
|
+
"required": ["notIn"]
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
"relate": {
|
|
251
|
+
"type": "array",
|
|
252
|
+
"minItems": 1,
|
|
253
|
+
"items": {
|
|
254
|
+
"$ref": "#/$defs/relateEntry"
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
"relateEntry": {
|
|
258
|
+
"type": "object",
|
|
259
|
+
"additionalProperties": false,
|
|
260
|
+
"required": ["id", "relation"],
|
|
261
|
+
"properties": {
|
|
262
|
+
"id": {
|
|
263
|
+
"type": "string",
|
|
264
|
+
"minLength": 1
|
|
265
|
+
},
|
|
266
|
+
"relation": {
|
|
267
|
+
"$ref": "#/$defs/relation"
|
|
268
|
+
},
|
|
269
|
+
"message": {
|
|
270
|
+
"type": "string",
|
|
271
|
+
"minLength": 1
|
|
272
|
+
},
|
|
273
|
+
"messageBySide": {
|
|
274
|
+
"type": "object",
|
|
275
|
+
"additionalProperties": false,
|
|
276
|
+
"required": ["left", "right"],
|
|
277
|
+
"properties": {
|
|
278
|
+
"left": {
|
|
279
|
+
"type": "string",
|
|
280
|
+
"minLength": 1
|
|
281
|
+
},
|
|
282
|
+
"right": {
|
|
283
|
+
"type": "string",
|
|
284
|
+
"minLength": 1
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
"oneOf": [
|
|
290
|
+
{
|
|
291
|
+
"description": "A plain message, on any relation.",
|
|
292
|
+
"required": ["message"],
|
|
293
|
+
"not": {
|
|
294
|
+
"required": ["messageBySide"]
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"description": "Side-keyed messages, only on Equal — the one relation with two sides.",
|
|
299
|
+
"required": ["messageBySide"],
|
|
300
|
+
"not": {
|
|
301
|
+
"required": ["message"]
|
|
302
|
+
},
|
|
303
|
+
"properties": {
|
|
304
|
+
"relation": {
|
|
305
|
+
"properties": {
|
|
306
|
+
"op": {
|
|
307
|
+
"const": "equal"
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
]
|
|
314
|
+
},
|
|
315
|
+
"relation": {
|
|
316
|
+
"type": "object",
|
|
317
|
+
"required": ["op"],
|
|
318
|
+
"properties": {
|
|
319
|
+
"op": {
|
|
320
|
+
"enum": ["empty", "nonEmpty", "equal", "subset", "implies", "ordered", "unchanged"]
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
"oneOf": [
|
|
324
|
+
{
|
|
325
|
+
"additionalProperties": false,
|
|
326
|
+
"required": ["op", "of"],
|
|
327
|
+
"properties": {
|
|
328
|
+
"op": {
|
|
329
|
+
"enum": ["empty", "nonEmpty", "unchanged"]
|
|
330
|
+
},
|
|
331
|
+
"of": {
|
|
332
|
+
"$ref": "#/$defs/extractName"
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
"additionalProperties": false,
|
|
338
|
+
"required": ["op", "of"],
|
|
339
|
+
"properties": {
|
|
340
|
+
"op": {
|
|
341
|
+
"const": "equal"
|
|
342
|
+
},
|
|
343
|
+
"of": {
|
|
344
|
+
"type": "array",
|
|
345
|
+
"minItems": 2,
|
|
346
|
+
"maxItems": 2,
|
|
347
|
+
"uniqueItems": true,
|
|
348
|
+
"items": {
|
|
349
|
+
"$ref": "#/$defs/extractName"
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
"additionalProperties": false,
|
|
356
|
+
"required": ["op", "of", "in"],
|
|
357
|
+
"properties": {
|
|
358
|
+
"op": {
|
|
359
|
+
"const": "subset"
|
|
360
|
+
},
|
|
361
|
+
"of": {
|
|
362
|
+
"$ref": "#/$defs/extractName"
|
|
363
|
+
},
|
|
364
|
+
"in": {
|
|
365
|
+
"$ref": "#/$defs/extractName"
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"additionalProperties": false,
|
|
371
|
+
"required": ["op", "of", "requires"],
|
|
372
|
+
"properties": {
|
|
373
|
+
"op": {
|
|
374
|
+
"const": "implies"
|
|
375
|
+
},
|
|
376
|
+
"of": {
|
|
377
|
+
"$ref": "#/$defs/extractName"
|
|
378
|
+
},
|
|
379
|
+
"requires": {
|
|
380
|
+
"$ref": "#/$defs/extractName"
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
"additionalProperties": false,
|
|
386
|
+
"required": ["op", "of"],
|
|
387
|
+
"properties": {
|
|
388
|
+
"op": {
|
|
389
|
+
"const": "ordered"
|
|
390
|
+
},
|
|
391
|
+
"of": {
|
|
392
|
+
"$ref": "#/$defs/extractName"
|
|
393
|
+
},
|
|
394
|
+
"strict": {
|
|
395
|
+
"type": "boolean"
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
]
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|