@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.
Files changed (53) hide show
  1. package/API.md +262 -191
  2. package/README.md +15 -18
  3. package/dist/blocks.d.ts.map +1 -1
  4. package/dist/blocks.js +33 -26
  5. package/dist/condition.d.ts +38 -14
  6. package/dist/condition.d.ts.map +1 -1
  7. package/dist/condition.js +401 -180
  8. package/dist/definition.d.ts +24 -1
  9. package/dist/definition.d.ts.map +1 -1
  10. package/dist/definition.js +303 -29
  11. package/dist/divide.d.ts.map +1 -1
  12. package/dist/divide.js +27 -17
  13. package/dist/document.d.ts +6 -3
  14. package/dist/document.d.ts.map +1 -1
  15. package/dist/document.js +315 -167
  16. package/dist/fields.d.ts +2 -0
  17. package/dist/fields.d.ts.map +1 -1
  18. package/dist/fields.js +7 -0
  19. package/dist/index.d.ts +15 -9
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +8 -6
  22. package/dist/links.d.ts +3 -0
  23. package/dist/links.d.ts.map +1 -1
  24. package/dist/links.js +66 -40
  25. package/dist/paths.d.ts +10 -1
  26. package/dist/paths.d.ts.map +1 -1
  27. package/dist/paths.js +115 -12
  28. package/dist/project.d.ts +1 -0
  29. package/dist/project.d.ts.map +1 -1
  30. package/dist/project.js +38 -15
  31. package/dist/references.d.ts +2 -2
  32. package/dist/references.d.ts.map +1 -1
  33. package/dist/references.js +6 -1
  34. package/dist/schema.js +2 -2
  35. package/dist/types.d.ts +49 -2
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/validate.d.ts +30 -2
  38. package/dist/validate.d.ts.map +1 -1
  39. package/dist/validate.js +290 -121
  40. package/dist/yaml.d.ts +7 -3
  41. package/dist/yaml.d.ts.map +1 -1
  42. package/dist/yaml.js +72 -43
  43. package/fixtures/README.md +6 -5
  44. package/fixtures/cases/cross-room-link/context.json +1 -0
  45. package/fixtures/cases/cross-room-link/expected.json +1 -0
  46. package/fixtures/cases/cross-room-link/library/clients/acme.md +6 -0
  47. package/fixtures/cases/cross-room-link/schema.yaml +3 -0
  48. package/fixtures/cases/cross-room-reference/context.json +1 -0
  49. package/fixtures/cases/cross-room-reference/expected.json +1 -0
  50. package/fixtures/cases/cross-room-reference/library/clients/acme.md +9 -0
  51. package/fixtures/cases/cross-room-reference/schema.yaml +9 -0
  52. package/fixtures/division.json +26 -26
  53. 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 { isRecord } from './yaml.js';
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 fitted(kind, facts, value, path, resolve) {
45
- if (!fits(kind, facts, value))
46
- return undefined;
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
- return reached === null ? undefined : resolve(reached);
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 members = {};
57
- const prose = Object.keys(document.sections).map((heading) => `/sections/${heading}`);
58
- const unreadable = [];
59
- for (const [name, declared] of Object.entries(definition === null ? {} : definition.fields)) {
60
- const value = document.fields[name];
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
- members,
96
- prose: prose.sort(),
97
- unreadable: unreadable.sort(),
109
+ sections: Object.keys(document.sections).sort(),
98
110
  };
99
111
  }
100
- function readDocument(facts, path) {
101
- if (path === '/path')
102
- return { kind: 'path', value: facts.path };
103
- if (path === '/type')
104
- return facts.type === null ? undefined : { kind: 'string', value: facts.type };
105
- if (facts.unreadable.includes(path))
106
- return { kind: 'unreadable' };
107
- if (facts.prose.includes(path))
108
- return { kind: 'present' };
109
- if (path.startsWith('/fields/')) {
110
- const name = path.slice('/fields/'.length);
111
- return Object.hasOwn(facts.fields, name) ? facts.fields[name] : undefined;
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
- if (path.startsWith('/members/')) {
114
- const name = path.slice('/members/'.length);
115
- return Object.hasOwn(facts.members, name)
116
- ? { kind: 'member', value: facts.members[name] }
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 readActor(actor, path) {
122
- const value = path === '/user'
123
- ? actor.user
124
- : path === '/site'
125
- ? actor.site
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 plain(value) {
132
- if (!isRecord(value))
215
+ export function valuedChange(definition, field) {
216
+ if (!field.startsWith('/fields/'))
133
217
  return false;
134
- const prototype = Object.getPrototypeOf(value);
135
- return prototype === Object.prototype || prototype === null;
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 part of pointer.slice(1).split('/')) {
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 { kind: 'number', value };
201
- if (Array.isArray(value))
202
- return { kind: 'list', value: value.filter(scalar) };
203
- return { kind: 'present' };
204
- }
205
- function readItem(item, path) {
206
- if (path.startsWith('/json/')) {
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
- const expected = wanted(value, owner);
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 expected === held.value;
423
+ return value === held.value;
243
424
  const left = instant(held.value);
244
- const right = instant(expected);
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 (held.kind === 'present' || Array.isArray(held.value))
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 leaf(test, held, owner) {
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, owner);
464
+ return equal(held, test.eq);
290
465
  if ('in' in test)
291
- return anyOf(test.in.map((value) => equal(held, value, owner)));
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 (held.kind === 'present' || typeof held.value !== 'string')
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 evaluated(test, read, owner) {
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, owner)));
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, owner)));
503
+ return anyOf(test.any.map((part) => evaluated(part, read)));
322
504
  if ('not' in test) {
323
- const truth = evaluated(test.not, read, owner);
505
+ const truth = evaluated(test.not, read);
324
506
  return truth === undefined ? undefined : !truth;
325
507
  }
326
- return leaf(test, read(test.path), owner);
508
+ return leaf(test, read(test.path));
327
509
  }
328
- export function holds(test, document, owner) {
329
- return evaluated(test, (path) => readDocument(document, path), owner);
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, owner) {
533
+ function matched(condition, test, document) {
341
534
  if (!inScope(condition.scope, document))
342
535
  return false;
343
- return test === undefined || holds(test, document, owner) === true;
536
+ return test === undefined || holds(test, document) === true;
344
537
  }
345
- function sameFact(left, right) {
346
- return JSON.stringify(left) === JSON.stringify(right);
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 among(values, document, field, owner) {
349
- return holds({ path: field, in: values }, document, owner) === true;
566
+ function signature(reached) {
567
+ return JSON.stringify(reached.map(({ fact }) => valueOf(fact)));
350
568
  }
351
- function fieldChanged(condition, event, owner) {
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
- if (from === undefined && to === undefined && event.changed.includes(field))
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 (!field.startsWith('/fields/'))
580
+ if (before.length === 0 && after.length === 0)
356
581
  return false;
357
- const name = field.slice('/fields/'.length);
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, owner)))
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, owner)))
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, owner) {
375
- if (condition.by !== undefined &&
376
- evaluated(condition.by, (path) => readActor(event.by, path), owner) !== true)
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, owner) &&
382
- !matched(condition, condition.match, event.before, owner));
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, owner) && fieldChanged(condition, event, owner);
607
+ return matched(condition, condition.match, side) && fieldChanged(condition, event);
386
608
  }
387
- export function itemFires(condition, item, owner) {
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
  }