@agentshouse/mdmodel 0.1.0-c3d58cb8652e6 → 0.1.0-c423be616ea39
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/API.md +262 -191
- package/README.md +15 -18
- package/dist/blocks.d.ts.map +1 -1
- package/dist/blocks.js +33 -26
- package/dist/condition.d.ts +38 -14
- package/dist/condition.d.ts.map +1 -1
- package/dist/condition.js +401 -180
- package/dist/definition.d.ts +24 -1
- package/dist/definition.d.ts.map +1 -1
- package/dist/definition.js +303 -29
- package/dist/divide.d.ts.map +1 -1
- package/dist/divide.js +27 -17
- package/dist/document.d.ts +6 -3
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +315 -167
- package/dist/fields.d.ts +2 -0
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +7 -0
- package/dist/index.d.ts +15 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/links.d.ts +3 -0
- package/dist/links.d.ts.map +1 -1
- package/dist/links.js +66 -40
- package/dist/paths.d.ts +10 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +115 -12
- package/dist/project.d.ts +1 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +38 -15
- package/dist/references.d.ts +2 -2
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +6 -1
- package/dist/schema.js +2 -2
- package/dist/types.d.ts +49 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/validate.d.ts +30 -2
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +290 -121
- package/dist/yaml.d.ts +7 -3
- package/dist/yaml.d.ts.map +1 -1
- package/dist/yaml.js +72 -43
- package/fixtures/README.md +6 -5
- package/fixtures/cases/cross-room-link/context.json +1 -0
- package/fixtures/cases/cross-room-link/expected.json +1 -0
- package/fixtures/cases/cross-room-link/library/clients/acme.md +6 -0
- package/fixtures/cases/cross-room-link/schema.yaml +3 -0
- package/fixtures/cases/cross-room-reference/context.json +1 -0
- package/fixtures/cases/cross-room-reference/expected.json +1 -0
- package/fixtures/cases/cross-room-reference/library/clients/acme.md +9 -0
- package/fixtures/cases/cross-room-reference/schema.yaml +9 -0
- package/fixtures/division.json +26 -26
- package/package.json +4 -2
package/dist/condition.js
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
|
-
import { ARRAY_INDEX } from './definition.js';
|
|
2
|
-
import { target } from './paths.js';
|
|
3
|
-
import {
|
|
1
|
+
import { ARRAY_INDEX, escapePointer, fieldFacts, itemFacts, memberFacts, pointerFacts, pointerSegments, } from './definition.js';
|
|
2
|
+
import { roomPath, target } from './paths.js';
|
|
3
|
+
import { plain } from './yaml.js';
|
|
4
|
+
export const CONDITION_OPERATORS = [
|
|
5
|
+
'eq',
|
|
6
|
+
'in',
|
|
7
|
+
'exists',
|
|
8
|
+
'contains',
|
|
9
|
+
'gt',
|
|
10
|
+
'gte',
|
|
11
|
+
'lt',
|
|
12
|
+
'lte',
|
|
13
|
+
'prefix',
|
|
14
|
+
];
|
|
4
15
|
export const WRITE_DOORS = ['browser', 'agent', 'client'];
|
|
16
|
+
const PROSE = { kind: 'prose' };
|
|
17
|
+
const NULL = { kind: 'null' };
|
|
18
|
+
const UNREADABLE = { kind: 'unreadable' };
|
|
19
|
+
const UNREAD = { present: undefined, values: [undefined], items: [undefined] };
|
|
20
|
+
const NO_ITEM = { present: false, values: [], items: [] };
|
|
5
21
|
const DATE = /^\d{4}-\d{2}-\d{2}$/u;
|
|
22
|
+
const HEADER = /^\/headers\/[!#$%&'*+.^_`|~0-9a-z-]+$/u;
|
|
6
23
|
const DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:[Zz]|([+-])(\d{2}):(\d{2}))$/u;
|
|
7
24
|
export function factKind(facts) {
|
|
8
25
|
if (facts.reference !== undefined)
|
|
@@ -21,9 +38,6 @@ export function factKind(facts) {
|
|
|
21
38
|
return 'time';
|
|
22
39
|
return 'string';
|
|
23
40
|
}
|
|
24
|
-
function referenced(model, facts) {
|
|
25
|
-
return (facts.reference ?? []).some((to) => Object.hasOwn(model.types, to) && model.types[to].memberDocument);
|
|
26
|
-
}
|
|
27
41
|
function fits(kind, facts, value) {
|
|
28
42
|
if (!scalar(value))
|
|
29
43
|
return false;
|
|
@@ -41,98 +55,296 @@ function fits(kind, facts, value) {
|
|
|
41
55
|
return instant(value) !== undefined;
|
|
42
56
|
return true;
|
|
43
57
|
}
|
|
44
|
-
function
|
|
45
|
-
if (
|
|
46
|
-
return
|
|
58
|
+
function factOf(value, declared, place) {
|
|
59
|
+
if (value === null)
|
|
60
|
+
return NULL;
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
if (declared?.type !== undefined && declared.type !== 'array')
|
|
63
|
+
return UNREADABLE;
|
|
64
|
+
return {
|
|
65
|
+
kind: 'list',
|
|
66
|
+
items: value.map((item, index) => factOf(item, declared === undefined ? undefined : itemFacts(declared, index), place)),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (plain(value)) {
|
|
70
|
+
if (declared?.type !== undefined && declared.type !== 'object')
|
|
71
|
+
return UNREADABLE;
|
|
72
|
+
const fields = Object.create(null);
|
|
73
|
+
for (const [key, held] of Object.entries(value))
|
|
74
|
+
fields[key] = factOf(held, declared === undefined ? undefined : memberFacts(declared, key), place);
|
|
75
|
+
return { kind: 'object', fields };
|
|
76
|
+
}
|
|
77
|
+
if (declared?.type === 'array' || declared?.type === 'object')
|
|
78
|
+
return UNREADABLE;
|
|
79
|
+
const kind = declared === undefined || declared.prose ? null : factKind(declared);
|
|
80
|
+
if (kind === null)
|
|
81
|
+
return PROSE;
|
|
82
|
+
if (!fits(kind, declared, value))
|
|
83
|
+
return UNREADABLE;
|
|
47
84
|
if (kind !== 'reference')
|
|
48
|
-
return { value };
|
|
49
|
-
const reached = target(path, value);
|
|
50
|
-
|
|
85
|
+
return { kind, value };
|
|
86
|
+
const reached = target(place.path, value) ?? roomPath(value);
|
|
87
|
+
const resolved = reached === null ? undefined : place.resolve(reached);
|
|
88
|
+
if (resolved === undefined)
|
|
89
|
+
return UNREADABLE;
|
|
90
|
+
return resolved.member === undefined
|
|
91
|
+
? { kind, value: resolved.id }
|
|
92
|
+
: { kind, value: resolved.id, member: resolved.member };
|
|
51
93
|
}
|
|
52
94
|
export function facts(document, at, model, resolve) {
|
|
53
95
|
const type = document.type ?? null;
|
|
54
96
|
const definition = type !== null && Object.hasOwn(model.types, type) ? model.types[type] : null;
|
|
55
|
-
const fields =
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (value === undefined || value === null)
|
|
62
|
-
continue;
|
|
63
|
-
const list = declared.type === 'array';
|
|
64
|
-
const facts = list ? declared.items : declared;
|
|
65
|
-
const kind = facts === undefined || facts.prose ? null : factKind(facts);
|
|
66
|
-
if (kind === null) {
|
|
67
|
-
prose.push(`/fields/${name}`);
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
const items = list ? value : [value];
|
|
71
|
-
const held = Array.isArray(items)
|
|
72
|
-
? items.map((item) => fitted(kind, facts, item, at.path, resolve))
|
|
73
|
-
: [undefined];
|
|
74
|
-
if (held.some((item) => item === undefined)) {
|
|
75
|
-
unreadable.push(`/fields/${name}`);
|
|
76
|
-
if (referenced(model, facts))
|
|
77
|
-
unreadable.push(`/members/${name}`);
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
const values = held.map((item) => ('id' in item ? item.id : item.value));
|
|
81
|
-
fields[name] = { kind, value: list ? values : values[0] };
|
|
82
|
-
if (!referenced(model, facts))
|
|
83
|
-
continue;
|
|
84
|
-
const bound = held.flatMap((item) => 'member' in item && item.member !== undefined ? [item.member] : []);
|
|
85
|
-
if (list)
|
|
86
|
-
members[name] = bound;
|
|
87
|
-
else if (bound[0] !== undefined)
|
|
88
|
-
members[name] = bound[0];
|
|
97
|
+
const fields = Object.create(null);
|
|
98
|
+
const place = { path: at.path, resolve };
|
|
99
|
+
for (const [name, value] of Object.entries(document.fields)) {
|
|
100
|
+
const declared = definition === null ? undefined : fieldFacts(definition, name);
|
|
101
|
+
if (declared !== undefined)
|
|
102
|
+
fields[name] = factOf(value, declared, place);
|
|
89
103
|
}
|
|
90
104
|
return {
|
|
91
105
|
id: at.id,
|
|
92
106
|
path: `/${at.path}`,
|
|
93
107
|
type,
|
|
94
108
|
fields,
|
|
95
|
-
|
|
96
|
-
prose: prose.sort(),
|
|
97
|
-
unreadable: unreadable.sort(),
|
|
109
|
+
sections: Object.keys(document.sections).sort(),
|
|
98
110
|
};
|
|
99
111
|
}
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
function rootOf(facts, prefix) {
|
|
113
|
+
return { pointer: prefix, fact: { kind: 'object', fields: facts.fields } };
|
|
114
|
+
}
|
|
115
|
+
export function prosePaths(facts) {
|
|
116
|
+
const paths = facts.sections.map((heading) => `/sections/${heading}`);
|
|
117
|
+
const visit = ({ pointer, fact }) => {
|
|
118
|
+
if (fact.kind === 'prose')
|
|
119
|
+
paths.push(pointer);
|
|
120
|
+
if (fact.kind === 'list')
|
|
121
|
+
fact.items.forEach((item, index) => visit({ pointer: `${pointer}/${index}`, fact: item }));
|
|
122
|
+
if (fact.kind === 'object')
|
|
123
|
+
for (const [key, held] of Object.entries(fact.fields))
|
|
124
|
+
visit({ pointer: `${pointer}/${escapePointer(key)}`, fact: held });
|
|
125
|
+
};
|
|
126
|
+
visit(rootOf(facts, '/fields'));
|
|
127
|
+
return paths.sort();
|
|
128
|
+
}
|
|
129
|
+
function walked(start, pointer) {
|
|
130
|
+
let reached = [start];
|
|
131
|
+
let several = false;
|
|
132
|
+
let missing = false;
|
|
133
|
+
const each = (from, next) => from.flatMap((one) => {
|
|
134
|
+
const found = next(one);
|
|
135
|
+
if (found === null)
|
|
136
|
+
missing = true;
|
|
137
|
+
return found ?? [];
|
|
138
|
+
});
|
|
139
|
+
for (const token of pointerSegments(pointer)) {
|
|
140
|
+
const step = ({ pointer: at, fact }) => {
|
|
141
|
+
if (fact.kind === 'unreadable')
|
|
142
|
+
return [{ pointer: at, fact }];
|
|
143
|
+
if (fact.kind === 'object')
|
|
144
|
+
return Object.hasOwn(fact.fields, token)
|
|
145
|
+
? [{ pointer: `${at}/${escapePointer(token)}`, fact: fact.fields[token] }]
|
|
146
|
+
: null;
|
|
147
|
+
if (fact.kind !== 'list')
|
|
148
|
+
return null;
|
|
149
|
+
if (ARRAY_INDEX.test(token)) {
|
|
150
|
+
const item = fact.items[Number(token)];
|
|
151
|
+
return item === undefined ? null : [{ pointer: `${at}/${token}`, fact: item }];
|
|
152
|
+
}
|
|
153
|
+
several = true;
|
|
154
|
+
return each(fact.items.map((item, index) => ({ pointer: `${at}/${index}`, fact: item })), step);
|
|
155
|
+
};
|
|
156
|
+
reached = each(reached, step);
|
|
112
157
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
158
|
+
return { reached, several, missing };
|
|
159
|
+
}
|
|
160
|
+
function expand(fact) {
|
|
161
|
+
return fact.kind === 'list' ? fact.items.flatMap(expand) : [fact];
|
|
162
|
+
}
|
|
163
|
+
function fieldHeld(fact) {
|
|
164
|
+
if (fact.kind === 'unreadable')
|
|
165
|
+
return undefined;
|
|
166
|
+
if (fact.kind === 'null')
|
|
167
|
+
return { kind: 'null' };
|
|
168
|
+
return 'value' in fact ? { kind: fact.kind, value: fact.value } : { kind: 'prose' };
|
|
169
|
+
}
|
|
170
|
+
function memberHeld(fact) {
|
|
171
|
+
if (fact.kind === 'unreadable')
|
|
172
|
+
return undefined;
|
|
173
|
+
return fact.kind === 'reference' && fact.member !== undefined
|
|
174
|
+
? { kind: 'member', value: fact.member }
|
|
175
|
+
: null;
|
|
176
|
+
}
|
|
177
|
+
function single(held) {
|
|
178
|
+
return { present: true, values: [held], items: null };
|
|
179
|
+
}
|
|
180
|
+
function documentRead({ reached, several, missing }, held) {
|
|
181
|
+
if (reached.length === 0)
|
|
182
|
+
return missing ? undefined : NO_ITEM;
|
|
183
|
+
const facts = reached.map(({ fact }) => fact);
|
|
184
|
+
const kept = (all) => all.map(held).filter((value) => value !== null);
|
|
185
|
+
const unknown = missing ? [undefined] : [];
|
|
186
|
+
const values = kept(facts.filter((fact) => fact.kind !== 'list'));
|
|
187
|
+
const listed = several || facts.some((fact) => fact.kind === 'list');
|
|
188
|
+
const present = facts.some((fact) => fact.kind === 'list') || values.some(Boolean);
|
|
189
|
+
return {
|
|
190
|
+
present: present ? true : values.length > 0 ? undefined : false,
|
|
191
|
+
values: [...values, ...unknown],
|
|
192
|
+
items: listed ? [...kept(facts.flatMap(expand)), ...unknown] : null,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function readDocument(facts, path) {
|
|
196
|
+
const own = tested(DOCUMENT_TESTS, path);
|
|
197
|
+
if (own !== undefined)
|
|
198
|
+
return own.read(facts, path);
|
|
199
|
+
if (path.startsWith('/sections/'))
|
|
200
|
+
return facts.sections.includes(path.slice('/sections/'.length))
|
|
201
|
+
? single({ kind: 'prose' })
|
|
117
202
|
: undefined;
|
|
118
|
-
|
|
203
|
+
if (path.startsWith('/fields/'))
|
|
204
|
+
return documentRead(walked(rootOf(facts, '/fields'), path.slice('/fields'.length)), fieldHeld);
|
|
205
|
+
if (path.startsWith('/members/'))
|
|
206
|
+
return documentRead(walked(rootOf(facts, '/members'), path.slice('/members'.length)), memberHeld);
|
|
119
207
|
return undefined;
|
|
120
208
|
}
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
: path === '/door'
|
|
127
|
-
? actor.door
|
|
128
|
-
: undefined;
|
|
129
|
-
return value === undefined ? undefined : { kind: 'actor', value };
|
|
209
|
+
export function datePointers(fields) {
|
|
210
|
+
return Object.entries(fields)
|
|
211
|
+
.filter(([, facts]) => facts.type === 'string' && facts.format === 'date')
|
|
212
|
+
.map(([name]) => `/fields/${escapePointer(name)}`)
|
|
213
|
+
.sort();
|
|
130
214
|
}
|
|
131
|
-
function
|
|
132
|
-
if (!
|
|
215
|
+
export function valuedChange(definition, field) {
|
|
216
|
+
if (!field.startsWith('/fields/'))
|
|
133
217
|
return false;
|
|
134
|
-
const
|
|
135
|
-
return
|
|
218
|
+
const reached = pointerFacts(definition, field.slice('/fields'.length));
|
|
219
|
+
return (reached !== undefined &&
|
|
220
|
+
!reached.through &&
|
|
221
|
+
reached.facts.type !== 'array' &&
|
|
222
|
+
!reached.facts.prose &&
|
|
223
|
+
factKind(reached.facts) !== null);
|
|
224
|
+
}
|
|
225
|
+
export function changeFields(definition) {
|
|
226
|
+
return [
|
|
227
|
+
...Object.keys(definition.fields).map((name) => `/fields/${escapePointer(name)}`),
|
|
228
|
+
...Object.keys(definition.sections).map((heading) => `/sections/${heading}`),
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
export function memberPointers(definition, memberType) {
|
|
232
|
+
return Object.entries(definition.fields)
|
|
233
|
+
.filter(([, facts]) => (facts.type === 'array' ? facts.items : facts)?.reference?.includes(memberType) === true)
|
|
234
|
+
.map(([name]) => `/members/${escapePointer(name)}`);
|
|
235
|
+
}
|
|
236
|
+
function kindOperators(kind) {
|
|
237
|
+
switch (kind) {
|
|
238
|
+
case 'string':
|
|
239
|
+
return ['eq', 'in', 'prefix', 'exists'];
|
|
240
|
+
case 'boolean':
|
|
241
|
+
return ['eq', 'exists'];
|
|
242
|
+
case 'number':
|
|
243
|
+
case 'date':
|
|
244
|
+
case 'time':
|
|
245
|
+
return ['eq', 'in', 'gt', 'gte', 'lt', 'lte', 'exists'];
|
|
246
|
+
default:
|
|
247
|
+
return ['eq', 'in', 'exists'];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function pointerOperators({ facts, through }) {
|
|
251
|
+
const scalar = facts.type === 'array' ? facts.items : facts;
|
|
252
|
+
const kind = scalar === undefined || scalar.prose ? null : factKind(scalar);
|
|
253
|
+
if (kind === null)
|
|
254
|
+
return ['exists'];
|
|
255
|
+
if (facts.type === 'array')
|
|
256
|
+
return ['contains', 'exists'];
|
|
257
|
+
const own = kindOperators(kind);
|
|
258
|
+
return through ? [...own.filter((operator) => operator !== 'exists'), 'contains', 'exists'] : own;
|
|
259
|
+
}
|
|
260
|
+
function typeOperators(definition, path, memberType) {
|
|
261
|
+
if (path.startsWith('/sections/'))
|
|
262
|
+
return Object.hasOwn(definition.sections, path.slice('/sections/'.length)) ? ['exists'] : [];
|
|
263
|
+
const pointer = path.startsWith('/fields/')
|
|
264
|
+
? path.slice('/fields'.length)
|
|
265
|
+
: memberType !== null && memberPointers(definition, memberType).includes(path)
|
|
266
|
+
? path.slice('/members'.length)
|
|
267
|
+
: undefined;
|
|
268
|
+
const reached = pointer === undefined ? undefined : pointerFacts(definition, pointer);
|
|
269
|
+
return reached === undefined ? [] : pointerOperators(reached);
|
|
270
|
+
}
|
|
271
|
+
function envelope(path, kind, value) {
|
|
272
|
+
return {
|
|
273
|
+
path,
|
|
274
|
+
names: (named) => named === path,
|
|
275
|
+
kind,
|
|
276
|
+
operators: kindOperators(kind),
|
|
277
|
+
read: (facts) => {
|
|
278
|
+
const held = value(facts);
|
|
279
|
+
return held === null || held === undefined ? undefined : single({ kind, value: held });
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function tested(tests, path) {
|
|
284
|
+
return tests.find(({ names }) => names(path));
|
|
285
|
+
}
|
|
286
|
+
function reader(tests, facts) {
|
|
287
|
+
return (path) => tested(tests, path)?.read(facts, path);
|
|
288
|
+
}
|
|
289
|
+
const DOCUMENT_TESTS = [
|
|
290
|
+
{
|
|
291
|
+
path: '/path',
|
|
292
|
+
names: (named) => named === '/path',
|
|
293
|
+
kind: 'string',
|
|
294
|
+
operators: ['eq', 'in', 'prefix'],
|
|
295
|
+
read: (facts) => single({ kind: 'string', value: facts.path }),
|
|
296
|
+
},
|
|
297
|
+
envelope('/type', 'enum', (facts) => facts.type),
|
|
298
|
+
];
|
|
299
|
+
const ACTOR_TESTS = [
|
|
300
|
+
envelope('/user', 'enum', (actor) => actor.user),
|
|
301
|
+
envelope('/site', 'enum', (actor) => actor.site),
|
|
302
|
+
envelope('/door', 'enum', (actor) => actor.door),
|
|
303
|
+
];
|
|
304
|
+
const ITEM_TESTS = [
|
|
305
|
+
envelope('/delivery', 'enum', (item) => item.delivery),
|
|
306
|
+
envelope('/source', 'enum', (item) => item.source),
|
|
307
|
+
envelope('/container', 'string', (item) => item.container),
|
|
308
|
+
envelope('/author', 'string', (item) => item.author),
|
|
309
|
+
{
|
|
310
|
+
path: '/headers/<lowercase name>',
|
|
311
|
+
names: (named) => HEADER.test(named),
|
|
312
|
+
kind: 'string',
|
|
313
|
+
operators: kindOperators('string'),
|
|
314
|
+
read: (item, path) => {
|
|
315
|
+
const name = path.slice('/headers/'.length);
|
|
316
|
+
return Object.hasOwn(item.headers, name)
|
|
317
|
+
? single({ kind: 'string', value: item.headers[name] })
|
|
318
|
+
: undefined;
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
path: '/json/<JSON Pointer>',
|
|
323
|
+
names: (named) => named.startsWith('/json/'),
|
|
324
|
+
kind: null,
|
|
325
|
+
operators: CONDITION_OPERATORS,
|
|
326
|
+
read: (item, path) => item.json === null ? UNREAD : jsonRead(pointed(item.json.value, path.slice('/json'.length))),
|
|
327
|
+
},
|
|
328
|
+
];
|
|
329
|
+
export const DOCUMENT_PATHS = DOCUMENT_TESTS.map(({ path }) => path);
|
|
330
|
+
export const ACTOR_PATHS = ACTOR_TESTS.map(({ path }) => path);
|
|
331
|
+
export const ITEM_PATHS = ITEM_TESTS.map(({ path }) => path);
|
|
332
|
+
export function testOperators(definitions, path, memberType) {
|
|
333
|
+
const own = tested(DOCUMENT_TESTS, path);
|
|
334
|
+
if (own !== undefined)
|
|
335
|
+
return own.operators;
|
|
336
|
+
return [
|
|
337
|
+
...new Set(definitions.flatMap((definition) => typeOperators(definition, path, memberType))),
|
|
338
|
+
];
|
|
339
|
+
}
|
|
340
|
+
export function actorOperators(path) {
|
|
341
|
+
return tested(ACTOR_TESTS, path)?.operators ?? [];
|
|
342
|
+
}
|
|
343
|
+
export function itemOperators(path) {
|
|
344
|
+
return tested(ITEM_TESTS, path)?.operators ?? [];
|
|
345
|
+
}
|
|
346
|
+
export function itemKind(path) {
|
|
347
|
+
return tested(ITEM_TESTS, path)?.kind ?? null;
|
|
136
348
|
}
|
|
137
349
|
export function dated(value) {
|
|
138
350
|
return (typeof value === 'string' && DATE.test(value) && instant(`${value}T00:00:00Z`) !== undefined);
|
|
@@ -162,11 +374,8 @@ export function instant(value) {
|
|
|
162
374
|
return (BigInt(at.getTime()) - BigInt(offset) * 60000n) * 1000000n + BigInt(fraction);
|
|
163
375
|
}
|
|
164
376
|
export function pointed(value, pointer) {
|
|
165
|
-
if (pointer === '')
|
|
166
|
-
return value;
|
|
167
377
|
let reached = value;
|
|
168
|
-
for (const
|
|
169
|
-
const token = part.replaceAll('~1', '/').replaceAll('~0', '~');
|
|
378
|
+
for (const token of pointerSegments(pointer)) {
|
|
170
379
|
if (Array.isArray(reached)) {
|
|
171
380
|
if (!ARRAY_INDEX.test(token))
|
|
172
381
|
return undefined;
|
|
@@ -187,8 +396,6 @@ function scalar(value) {
|
|
|
187
396
|
(typeof value === 'number' && Number.isFinite(value)));
|
|
188
397
|
}
|
|
189
398
|
function jsonHeld(value) {
|
|
190
|
-
if (value === undefined)
|
|
191
|
-
return undefined;
|
|
192
399
|
if (typeof value === 'string')
|
|
193
400
|
return {
|
|
194
401
|
kind: dated(value) ? 'date' : instant(value) === undefined ? 'string' : 'time',
|
|
@@ -196,59 +403,33 @@ function jsonHeld(value) {
|
|
|
196
403
|
};
|
|
197
404
|
if (typeof value === 'boolean')
|
|
198
405
|
return { kind: 'boolean', value };
|
|
406
|
+
return { kind: 'number', value };
|
|
407
|
+
}
|
|
408
|
+
function jsonRead(value) {
|
|
409
|
+
if (value === undefined)
|
|
410
|
+
return undefined;
|
|
199
411
|
if (scalar(value))
|
|
200
|
-
return
|
|
201
|
-
if (Array.isArray(value))
|
|
202
|
-
return { kind: '
|
|
203
|
-
return {
|
|
204
|
-
}
|
|
205
|
-
function
|
|
206
|
-
if (
|
|
207
|
-
if (item.json === null)
|
|
208
|
-
return { kind: 'unreadable' };
|
|
209
|
-
return jsonHeld(pointed(item.json.value, path.slice('/json'.length)));
|
|
210
|
-
}
|
|
211
|
-
if (path.startsWith('/headers/')) {
|
|
212
|
-
const name = path.slice('/headers/'.length);
|
|
213
|
-
return Object.hasOwn(item.headers, name)
|
|
214
|
-
? { kind: 'string', value: item.headers[name] }
|
|
215
|
-
: undefined;
|
|
216
|
-
}
|
|
217
|
-
if (path === '/has_attachment')
|
|
218
|
-
return { kind: 'boolean', value: item.has_attachment };
|
|
219
|
-
const value = path === '/delivery'
|
|
220
|
-
? item.delivery
|
|
221
|
-
: path === '/source'
|
|
222
|
-
? item.source
|
|
223
|
-
: path === '/provider'
|
|
224
|
-
? item.provider
|
|
225
|
-
: path === '/container'
|
|
226
|
-
? item.container
|
|
227
|
-
: path === '/author'
|
|
228
|
-
? item.author
|
|
229
|
-
: null;
|
|
230
|
-
return value === null ? undefined : { kind: 'string', value };
|
|
231
|
-
}
|
|
232
|
-
function wanted(value, owner) {
|
|
233
|
-
return isRecord(value) && value.owner === true ? owner : value;
|
|
234
|
-
}
|
|
235
|
-
function equal(held, value, owner) {
|
|
236
|
-
if (held.kind === 'present' || Array.isArray(held.value))
|
|
412
|
+
return single(jsonHeld(value));
|
|
413
|
+
if (!Array.isArray(value))
|
|
414
|
+
return single({ kind: 'prose' });
|
|
415
|
+
return { present: true, values: [], items: value.filter(scalar).map(jsonHeld), loose: true };
|
|
416
|
+
}
|
|
417
|
+
function equal(held, value) {
|
|
418
|
+
if (!('value' in held))
|
|
237
419
|
return undefined;
|
|
238
|
-
|
|
239
|
-
if (typeof expected !== typeof held.value)
|
|
420
|
+
if (typeof value !== typeof held.value)
|
|
240
421
|
return undefined;
|
|
241
422
|
if (held.kind !== 'time')
|
|
242
|
-
return
|
|
423
|
+
return value === held.value;
|
|
243
424
|
const left = instant(held.value);
|
|
244
|
-
const right = instant(
|
|
425
|
+
const right = instant(value);
|
|
245
426
|
return left === undefined || right === undefined ? undefined : left === right;
|
|
246
427
|
}
|
|
247
428
|
function sign(left, right) {
|
|
248
429
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
249
430
|
}
|
|
250
431
|
function ordered(held, value, compare) {
|
|
251
|
-
if (
|
|
432
|
+
if (!('value' in held))
|
|
252
433
|
return undefined;
|
|
253
434
|
if (held.kind === 'number') {
|
|
254
435
|
if (typeof value !== 'number' || typeof held.value !== 'number')
|
|
@@ -278,29 +459,13 @@ function allOf(truths) {
|
|
|
278
459
|
return false;
|
|
279
460
|
return truths.some((truth) => truth === undefined) ? undefined : true;
|
|
280
461
|
}
|
|
281
|
-
function
|
|
282
|
-
if (held?.kind === 'unreadable')
|
|
283
|
-
return undefined;
|
|
284
|
-
if ('exists' in test)
|
|
285
|
-
return (held !== undefined) === test.exists;
|
|
286
|
-
if (held === undefined)
|
|
287
|
-
return undefined;
|
|
462
|
+
function compared(test, held) {
|
|
288
463
|
if ('eq' in test)
|
|
289
|
-
return equal(held, test.eq
|
|
464
|
+
return equal(held, test.eq);
|
|
290
465
|
if ('in' in test)
|
|
291
|
-
return anyOf(test.in.map((value) => equal(held, value
|
|
292
|
-
if ('contains' in test) {
|
|
293
|
-
if (held.kind === 'present' || !Array.isArray(held.value))
|
|
294
|
-
return undefined;
|
|
295
|
-
const expected = wanted(test.contains, owner);
|
|
296
|
-
if (held.value.some((item) => item === expected))
|
|
297
|
-
return true;
|
|
298
|
-
if (held.kind === 'list')
|
|
299
|
-
return false;
|
|
300
|
-
return held.value.every((item) => typeof item === typeof expected) ? false : undefined;
|
|
301
|
-
}
|
|
466
|
+
return anyOf(test.in.map((value) => equal(held, value)));
|
|
302
467
|
if ('prefix' in test) {
|
|
303
|
-
if (
|
|
468
|
+
if (!('value' in held) || typeof held.value !== 'string')
|
|
304
469
|
return undefined;
|
|
305
470
|
if (held.kind === 'number' || held.kind === 'reference' || held.kind === 'member')
|
|
306
471
|
return undefined;
|
|
@@ -314,19 +479,47 @@ function leaf(test, held, owner) {
|
|
|
314
479
|
return ordered(held, test.lt, (sign) => sign < 0);
|
|
315
480
|
return ordered(held, test.lte, (sign) => sign <= 0);
|
|
316
481
|
}
|
|
317
|
-
function
|
|
482
|
+
function leaf(test, read) {
|
|
483
|
+
if ('exists' in test) {
|
|
484
|
+
const present = read === undefined ? false : read.present;
|
|
485
|
+
return present === undefined ? undefined : present === test.exists;
|
|
486
|
+
}
|
|
487
|
+
if (read === undefined)
|
|
488
|
+
return undefined;
|
|
489
|
+
if ('contains' in test) {
|
|
490
|
+
if (read.items === null)
|
|
491
|
+
return undefined;
|
|
492
|
+
const truths = read.items.map((item) => item === undefined ? undefined : equal(item, test.contains));
|
|
493
|
+
return read.loose === true && !truths.includes(true) ? false : anyOf(truths);
|
|
494
|
+
}
|
|
495
|
+
if (read.values.length === 0)
|
|
496
|
+
return read === NO_ITEM ? false : undefined;
|
|
497
|
+
return anyOf(read.values.map((value) => (value === undefined ? undefined : compared(test, value))));
|
|
498
|
+
}
|
|
499
|
+
function evaluated(test, read) {
|
|
318
500
|
if ('all' in test)
|
|
319
|
-
return allOf(test.all.map((part) => evaluated(part, read
|
|
501
|
+
return allOf(test.all.map((part) => evaluated(part, read)));
|
|
320
502
|
if ('any' in test)
|
|
321
|
-
return anyOf(test.any.map((part) => evaluated(part, read
|
|
503
|
+
return anyOf(test.any.map((part) => evaluated(part, read)));
|
|
322
504
|
if ('not' in test) {
|
|
323
|
-
const truth = evaluated(test.not, read
|
|
505
|
+
const truth = evaluated(test.not, read);
|
|
324
506
|
return truth === undefined ? undefined : !truth;
|
|
325
507
|
}
|
|
326
|
-
return leaf(test, read(test.path)
|
|
508
|
+
return leaf(test, read(test.path));
|
|
327
509
|
}
|
|
328
|
-
export function
|
|
329
|
-
|
|
510
|
+
export function members(document, path) {
|
|
511
|
+
if (!path.startsWith('/members/'))
|
|
512
|
+
return [];
|
|
513
|
+
const read = readDocument(document, path);
|
|
514
|
+
const values = read === undefined ? [] : [...read.values, ...(read.items ?? [])];
|
|
515
|
+
return [
|
|
516
|
+
...new Set(values.flatMap((value) => value !== undefined && value.kind === 'member' && 'value' in value
|
|
517
|
+
? [String(value.value)]
|
|
518
|
+
: [])),
|
|
519
|
+
].sort();
|
|
520
|
+
}
|
|
521
|
+
export function holds(test, document) {
|
|
522
|
+
return evaluated(test, (path) => readDocument(document, path));
|
|
330
523
|
}
|
|
331
524
|
function inScope(scope, document) {
|
|
332
525
|
if (document === null)
|
|
@@ -337,58 +530,86 @@ function inScope(scope, document) {
|
|
|
337
530
|
return document.id === scope.document;
|
|
338
531
|
return scope.folder === '/' || document.path.startsWith(`${scope.folder}/`);
|
|
339
532
|
}
|
|
340
|
-
function matched(condition, test, document
|
|
533
|
+
function matched(condition, test, document) {
|
|
341
534
|
if (!inScope(condition.scope, document))
|
|
342
535
|
return false;
|
|
343
|
-
return test === undefined || holds(test, document
|
|
536
|
+
return test === undefined || holds(test, document) === true;
|
|
344
537
|
}
|
|
345
|
-
function
|
|
346
|
-
|
|
538
|
+
function reachedAt(document, field) {
|
|
539
|
+
if (document === null)
|
|
540
|
+
return [];
|
|
541
|
+
if (field.startsWith('/sections/'))
|
|
542
|
+
return document.sections.includes(field.slice('/sections/'.length))
|
|
543
|
+
? [{ pointer: field, fact: PROSE }]
|
|
544
|
+
: [];
|
|
545
|
+
if (!field.startsWith('/fields/'))
|
|
546
|
+
return [];
|
|
547
|
+
return walked(rootOf(document, '/fields'), field.slice('/fields'.length)).reached;
|
|
548
|
+
}
|
|
549
|
+
function valueOf(fact) {
|
|
550
|
+
if (fact.kind === 'list')
|
|
551
|
+
return { kind: fact.kind, items: fact.items.map(valueOf) };
|
|
552
|
+
if (fact.kind === 'object')
|
|
553
|
+
return {
|
|
554
|
+
kind: fact.kind,
|
|
555
|
+
fields: Object.keys(fact.fields)
|
|
556
|
+
.sort()
|
|
557
|
+
.map((key) => [key, valueOf(fact.fields[key])]),
|
|
558
|
+
};
|
|
559
|
+
if (fact.kind === 'time') {
|
|
560
|
+
const at = instant(fact.value);
|
|
561
|
+
if (at !== undefined)
|
|
562
|
+
return { kind: fact.kind, at: String(at) };
|
|
563
|
+
}
|
|
564
|
+
return 'value' in fact ? { kind: fact.kind, value: fact.value } : fact;
|
|
347
565
|
}
|
|
348
|
-
function
|
|
349
|
-
return
|
|
566
|
+
function signature(reached) {
|
|
567
|
+
return JSON.stringify(reached.map(({ fact }) => valueOf(fact)));
|
|
350
568
|
}
|
|
351
|
-
function
|
|
569
|
+
function among(values, document, field) {
|
|
570
|
+
return holds({ path: field, in: values }, document) === true;
|
|
571
|
+
}
|
|
572
|
+
function fieldChanged(condition, event) {
|
|
352
573
|
const { field, from, to } = condition;
|
|
353
|
-
|
|
574
|
+
const before = reachedAt(event.before, field);
|
|
575
|
+
const after = reachedAt(event.after, field);
|
|
576
|
+
const pointers = [field, ...[...before, ...after].map(({ pointer }) => pointer)];
|
|
577
|
+
const prose = event.changed.some((changed) => pointers.some((pointer) => changed === pointer || changed.startsWith(`${pointer}/`)));
|
|
578
|
+
if (from === undefined && to === undefined && prose)
|
|
354
579
|
return true;
|
|
355
|
-
if (
|
|
580
|
+
if (before.length === 0 && after.length === 0)
|
|
356
581
|
return false;
|
|
357
|
-
|
|
358
|
-
const before = event.before?.fields[name];
|
|
359
|
-
const after = event.after?.fields[name];
|
|
360
|
-
if (before === undefined && after === undefined)
|
|
361
|
-
return false;
|
|
362
|
-
if (event.before !== null && event.after !== null && sameFact(before, after))
|
|
582
|
+
if (event.before !== null && event.after !== null && signature(before) === signature(after))
|
|
363
583
|
return false;
|
|
364
584
|
if (from === 'new'
|
|
365
585
|
? event.before !== null
|
|
366
|
-
: from !== undefined && (event.before === null || !among(from, event.before, field
|
|
586
|
+
: from !== undefined && (event.before === null || !among(from, event.before, field)))
|
|
367
587
|
return false;
|
|
368
588
|
if (to === 'deleted'
|
|
369
589
|
? event.after !== null
|
|
370
|
-
: to !== undefined && (event.after === null || !among(to, event.after, field
|
|
590
|
+
: to !== undefined && (event.after === null || !among(to, event.after, field)))
|
|
371
591
|
return false;
|
|
372
592
|
return true;
|
|
373
593
|
}
|
|
374
|
-
export function fires(condition, event
|
|
375
|
-
if (condition.
|
|
376
|
-
|
|
594
|
+
export function fires(condition, event) {
|
|
595
|
+
if (condition.moved_between_rooms !== undefined &&
|
|
596
|
+
condition.moved_between_rooms !== event.moved_between_rooms)
|
|
597
|
+
return false;
|
|
598
|
+
if (condition.by !== undefined && evaluated(condition.by, reader(ACTOR_TESTS, event.by)) !== true)
|
|
377
599
|
return false;
|
|
378
600
|
if (condition.mode === 'starts_matching') {
|
|
379
601
|
if (event.before === null && !condition.new_documents)
|
|
380
602
|
return false;
|
|
381
|
-
return (matched(condition, condition.match, event.after
|
|
382
|
-
!matched(condition, condition.match, event.before
|
|
603
|
+
return (matched(condition, condition.match, event.after) &&
|
|
604
|
+
!matched(condition, condition.match, event.before));
|
|
383
605
|
}
|
|
384
606
|
const side = event.after ?? event.before;
|
|
385
|
-
return matched(condition, condition.match, side
|
|
607
|
+
return matched(condition, condition.match, side) && fieldChanged(condition, event);
|
|
386
608
|
}
|
|
387
|
-
export function itemFires(condition, item
|
|
609
|
+
export function itemFires(condition, item) {
|
|
388
610
|
if (condition.source !== undefined && item.source !== condition.source)
|
|
389
611
|
return false;
|
|
390
612
|
if (condition.address !== undefined && item.address !== condition.address)
|
|
391
613
|
return false;
|
|
392
|
-
return (condition.match === undefined ||
|
|
393
|
-
evaluated(condition.match, (path) => readItem(item, path), owner) === true);
|
|
614
|
+
return (condition.match === undefined || evaluated(condition.match, reader(ITEM_TESTS, item)) === true);
|
|
394
615
|
}
|