@agentshouse/mdmodel 0.1.0-c64451b2127b4 → 0.1.0-c669bdbad8a03

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 (44) hide show
  1. package/API.md +170 -170
  2. package/README.md +15 -18
  3. package/dist/blocks.d.ts.map +1 -1
  4. package/dist/blocks.js +6 -13
  5. package/dist/condition.d.ts +1 -1
  6. package/dist/condition.d.ts.map +1 -1
  7. package/dist/condition.js +19 -9
  8. package/dist/definition.d.ts +15 -0
  9. package/dist/definition.d.ts.map +1 -1
  10. package/dist/definition.js +224 -24
  11. package/dist/divide.d.ts.map +1 -1
  12. package/dist/divide.js +18 -8
  13. package/dist/document.d.ts +5 -2
  14. package/dist/document.d.ts.map +1 -1
  15. package/dist/document.js +318 -182
  16. package/dist/index.d.ts +10 -7
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +6 -5
  19. package/dist/links.d.ts +1 -0
  20. package/dist/links.d.ts.map +1 -1
  21. package/dist/links.js +22 -11
  22. package/dist/paths.d.ts +9 -1
  23. package/dist/paths.d.ts.map +1 -1
  24. package/dist/paths.js +24 -1
  25. package/dist/project.d.ts +1 -0
  26. package/dist/project.d.ts.map +1 -1
  27. package/dist/project.js +35 -10
  28. package/dist/references.d.ts +2 -2
  29. package/dist/references.d.ts.map +1 -1
  30. package/dist/references.js +6 -1
  31. package/dist/types.d.ts +44 -1
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/validate.d.ts +8 -1
  34. package/dist/validate.d.ts.map +1 -1
  35. package/dist/validate.js +205 -100
  36. package/fixtures/cases/cross-room-link/context.json +1 -0
  37. package/fixtures/cases/cross-room-link/expected.json +1 -0
  38. package/fixtures/cases/cross-room-link/library/clients/acme.md +6 -0
  39. package/fixtures/cases/cross-room-link/schema.yaml +3 -0
  40. package/fixtures/cases/cross-room-reference/context.json +1 -0
  41. package/fixtures/cases/cross-room-reference/expected.json +1 -0
  42. package/fixtures/cases/cross-room-reference/library/clients/acme.md +9 -0
  43. package/fixtures/cases/cross-room-reference/schema.yaml +9 -0
  44. package/package.json +1 -1
package/dist/validate.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ATTACHMENT, BUILT_IN_TYPE, readSchema } from './definition.js';
2
2
  import { parse } from './document.js';
3
3
  import { hrefOccurrences, linkInventory } from './links.js';
4
- import { classifyHref } from './paths.js';
4
+ import { classifyHref, roomTarget } from './paths.js';
5
5
  import { references } from './references.js';
6
6
  import { refusal } from './refuse.js';
7
7
  import { fitsItem, frontmatterRefusal } from './schema.js';
@@ -18,25 +18,42 @@ function instanceType(document) {
18
18
  return null;
19
19
  return raw;
20
20
  }
21
- function reaches(file, href, to, types, binaryTargets) {
22
- const resolved = classifyHref(file, href);
23
- if (resolved.kind !== 'path')
21
+ function reaches(reference, typeOf, binary) {
22
+ if (reference.target === null)
24
23
  return false;
25
- if (to.includes(ATTACHMENT))
24
+ if (reference.to.includes(ATTACHMENT))
26
25
  return true;
27
- if (binaryTargets.has(resolved.path))
26
+ if (binary(reference.target))
27
+ return false;
28
+ const held = typeOf(reference.target);
29
+ return held === undefined || reference.to.includes(held);
30
+ }
31
+ function missing(reference, typeOf, binary) {
32
+ if (reference.to.includes(ATTACHMENT) || reference.target === null)
28
33
  return false;
29
- const held = types.get(resolved.path);
30
- return held === undefined || to.includes(held);
34
+ return typeOf(reference.target) === undefined && !binary(reference.target);
35
+ }
36
+ export function resolves(reference, typeOf, binary) {
37
+ return reaches(reference, typeOf, binary) && !missing(reference, typeOf, binary);
38
+ }
39
+ function usesIn(content) {
40
+ let held;
41
+ return () => {
42
+ if (held !== undefined)
43
+ return held;
44
+ const bodyStart = bodyStartOf(content);
45
+ held = linkInventory(content.slice(bodyStart)).uses.map((use) => ({
46
+ ...use,
47
+ start: bodyStart + use.start,
48
+ end: bodyStart + use.end,
49
+ }));
50
+ return held;
51
+ };
31
52
  }
32
- function itemHrefs(content, items) {
33
- const bodyStart = bodyStartOf(content);
34
- const { uses } = linkInventory(content.slice(bodyStart));
53
+ function itemHrefs(content, items, uses) {
35
54
  const hrefs = [];
36
55
  for (const item of items) {
37
- const link = uses.find((use) => bodyStart + use.start === item.start &&
38
- bodyStart + use.end === item.end &&
39
- content[item.start] === '[');
56
+ const link = uses().find((use) => use.start === item.start && use.end === item.end && content[item.start] === '[');
40
57
  if (link === undefined)
41
58
  return null;
42
59
  hrefs.push(link.destination.href);
@@ -53,10 +70,142 @@ export function verify(schema) {
53
70
  ...(read.location === undefined ? {} : { location: read.location }),
54
71
  };
55
72
  }
56
- export function validate(library, context = { binaryTargets: [] }, model = { types: {} }) {
73
+ function structure(file, content, document, definition) {
74
+ if (definition === undefined)
75
+ return { refusal: refuse(file, 'unknown-type', { location: '/type' }) };
76
+ const fieldFailure = frontmatterRefusal(definition, document.fields);
77
+ if (fieldFailure !== undefined) {
78
+ return { refusal: refuse(file, fieldFailure.rule, { location: fieldFailure.location }) };
79
+ }
80
+ const spans = headingSpans(content, bodyStartOf(content));
81
+ const uses = usesIn(content);
82
+ const sections = [];
83
+ for (const [name, spec] of Object.entries(definition.sections)) {
84
+ const occurrences = spans.filter((one) => one.name === name);
85
+ if (occurrences.length === 0 && spec.required)
86
+ return { refusal: refuse(file, 'required-section', { location: name }) };
87
+ if (spec.kind !== 'list')
88
+ continue;
89
+ for (const span of occurrences) {
90
+ const items = listItems(content, span.bodyStart, span.bodyEnd);
91
+ if (items === null ||
92
+ items.some((item) => !fitsItem(definition, name, content.slice(item.start, item.end))))
93
+ return { refusal: refuse(file, 'structured-section', { location: name }) };
94
+ if (spec.items.reference === undefined)
95
+ continue;
96
+ const hrefs = itemHrefs(content, items, uses);
97
+ if (hrefs === null)
98
+ return { refusal: refuse(file, 'structured-section', { location: name }) };
99
+ sections.push({
100
+ section: name,
101
+ to: spec.items.reference,
102
+ crossRoom: spec.items.crossRoom === true,
103
+ hrefs,
104
+ });
105
+ }
106
+ }
107
+ return { sections };
108
+ }
109
+ function linked(file, content) {
110
+ return hrefOccurrences(bodyAfterFrontmatter(content)).every((occurrence) => classifyHref(file, occurrence.href).kind !== 'invalid');
111
+ }
112
+ function reached(file, href, to) {
113
+ const resolved = classifyHref(file, href);
114
+ return { target: resolved.kind === 'path' ? resolved.path : null, to };
115
+ }
116
+ function crossing(file, href, crossRoom) {
117
+ return crossRoom === true && classifyHref(file, href).kind === 'room';
118
+ }
119
+ function definitionOf(document, model) {
120
+ const kind = instanceType(document);
121
+ return kind === null || !Object.hasOwn(model.types, kind) ? undefined : model.types[kind];
122
+ }
123
+ export function checkDocument(file, content, model) {
124
+ const parsed = parse(content, file);
125
+ if (!parsed.ok)
126
+ return { ok: false };
127
+ const document = parsed.document;
128
+ const type = document.type ?? BUILT_IN_TYPE;
129
+ if (instanceType(document) === BUILT_IN_TYPE)
130
+ return { ok: true, type, typed: false, references: [] };
131
+ const definition = definitionOf(document, model);
132
+ const read = structure(file, content, document, definition);
133
+ if ('refusal' in read || definition === undefined || !linked(file, content))
134
+ return { ok: false };
135
+ return {
136
+ ok: true,
137
+ type,
138
+ typed: true,
139
+ references: [
140
+ ...references(document, definition).flatMap(({ value, to, crossRoom }) => crossing(file, value, crossRoom) ? [] : [reached(file, value, to)]),
141
+ ...read.sections.flatMap(({ to, crossRoom, hrefs }) => hrefs.flatMap((href) => (crossing(file, href, crossRoom) ? [] : [reached(file, href, to)]))),
142
+ ],
143
+ };
144
+ }
145
+ export function sectionReferences(file, content, definition) {
146
+ const referring = (name) => {
147
+ const spec = Object.hasOwn(definition.sections, name) ? definition.sections[name] : undefined;
148
+ return spec?.kind === 'list' && spec.items.reference !== undefined ? spec.items : undefined;
149
+ };
150
+ if (!Object.keys(definition.sections).some((name) => referring(name) !== undefined))
151
+ return [];
152
+ const uses = usesIn(content);
153
+ const found = [];
154
+ for (const span of headingSpans(content, bodyStartOf(content))) {
155
+ const items = referring(span.name);
156
+ if (items === undefined)
157
+ continue;
158
+ const listed = listItems(content, span.bodyStart, span.bodyEnd);
159
+ const hrefs = listed === null ? null : itemHrefs(content, listed, uses);
160
+ for (const href of hrefs ?? [])
161
+ if (!crossing(file, href, items.crossRoom))
162
+ found.push(reached(file, href, items.reference));
163
+ }
164
+ return found;
165
+ }
166
+ export function bodyRoomLinks(body) {
167
+ const found = [];
168
+ for (const href of new Set(hrefOccurrences(body).map((occurrence) => occurrence.href))) {
169
+ const room = roomTarget(href);
170
+ if (room !== null)
171
+ found.push({ kind: 'link', href, ...room });
172
+ }
173
+ return found;
174
+ }
175
+ export function roomReferences(file, content, definition) {
176
+ const found = bodyRoomLinks(bodyAfterFrontmatter(content));
177
+ if (definition === undefined)
178
+ return found;
179
+ const parsed = parse(content, file);
180
+ if (!parsed.ok)
181
+ return found;
182
+ for (const { pointer, value, to, crossRoom } of references(parsed.document, definition)) {
183
+ const room = crossRoom === true ? roomTarget(value) : null;
184
+ if (room !== null)
185
+ found.push({ kind: 'field', pointer, value, to, ...room });
186
+ }
187
+ const uses = usesIn(content);
188
+ for (const span of headingSpans(content, bodyStartOf(content))) {
189
+ const spec = Object.hasOwn(definition.sections, span.name)
190
+ ? definition.sections[span.name]
191
+ : undefined;
192
+ if (spec?.kind !== 'list' || spec.items.reference === undefined || !spec.items.crossRoom)
193
+ continue;
194
+ const listed = listItems(content, span.bodyStart, span.bodyEnd);
195
+ for (const href of (listed === null ? null : itemHrefs(content, listed, uses)) ?? []) {
196
+ const room = roomTarget(href);
197
+ if (room !== null)
198
+ found.push({ kind: 'item', section: span.name, href, to: spec.items.reference, ...room });
199
+ }
200
+ }
201
+ return found;
202
+ }
203
+ export function* validating(library, context = { binaryTargets: [] }, model = { types: {} }) {
57
204
  const binaryTargets = new Set(context.binaryTargets);
205
+ const binary = (path) => binaryTargets.has(path);
58
206
  const files = Object.keys(library).sort();
59
207
  const types = new Map();
208
+ const typeOf = (path) => types.get(path);
60
209
  const documents = new Map();
61
210
  for (const file of files) {
62
211
  if (binaryTargets.has(file))
@@ -67,100 +216,56 @@ export function validate(library, context = { binaryTargets: [] }, model = { typ
67
216
  types.set(file, parsed.document.type ?? BUILT_IN_TYPE);
68
217
  if (instanceType(parsed.document) !== BUILT_IN_TYPE)
69
218
  documents.set(file, parsed.document);
219
+ yield file;
70
220
  }
71
- const definitions = new Map(Object.entries(model.types));
72
221
  const sectionReferences = new Map();
73
222
  for (const [file, document] of documents) {
74
- const kind = instanceType(document);
75
- if (kind === BUILT_IN_TYPE)
76
- continue;
77
- const definition = kind === null ? undefined : definitions.get(kind);
78
- if (definition === undefined)
79
- return refuse(file, 'unknown-type', { location: '/type' });
80
- const fieldFailure = frontmatterRefusal(definition, document.fields);
81
- if (fieldFailure !== undefined) {
82
- return refuse(file, fieldFailure.rule, { location: fieldFailure.location });
83
- }
84
- const content = library[file];
85
- const spans = headingSpans(content, bodyStartOf(content));
86
- const references = [];
87
- for (const [name, spec] of Object.entries(definition.sections)) {
88
- const occurrences = spans.filter((one) => one.name === name);
89
- if (occurrences.length === 0 && spec.required)
90
- return refuse(file, 'required-section', { location: name });
91
- if (spec.kind !== 'list')
92
- continue;
93
- for (const span of occurrences) {
94
- const items = listItems(content, span.bodyStart, span.bodyEnd);
95
- if (items === null ||
96
- items.some((item) => !fitsItem(definition, name, content.slice(item.start, item.end))))
97
- return refuse(file, 'structured-section', { location: name });
98
- if (spec.items.reference === undefined)
99
- continue;
100
- const hrefs = itemHrefs(content, items);
101
- if (hrefs === null)
102
- return refuse(file, 'structured-section', { location: name });
103
- references.push({ section: name, to: spec.items.reference, hrefs });
104
- }
105
- }
106
- sectionReferences.set(file, references);
223
+ const read = structure(file, library[file], document, definitionOf(document, model));
224
+ if ('refusal' in read)
225
+ return read.refusal;
226
+ sectionReferences.set(file, read.sections);
227
+ yield file;
107
228
  }
229
+ let least;
108
230
  for (const [file, document] of documents) {
109
- const kind = instanceType(document);
110
- if (kind === null || kind === BUILT_IN_TYPE)
111
- continue;
112
- const definition = definitions.get(kind);
113
- if (definition === undefined)
114
- continue;
115
- for (const { pointer, to, value } of references(document, definition)) {
116
- if (!reaches(file, value, to, types, binaryTargets))
117
- return refuse(file, 'unresolved-reference', { location: pointer });
118
- }
119
- const body = bodyAfterFrontmatter(library[file]);
120
- for (const occurrence of hrefOccurrences(body)) {
121
- const resolved = classifyHref(file, occurrence.href);
122
- if (resolved.kind === 'invalid')
123
- return refuse(file, 'unresolved-link');
231
+ const definition = definitionOf(document, model);
232
+ const fields = references(document, definition).flatMap(({ pointer, value, to, crossRoom }) => crossing(file, value, crossRoom)
233
+ ? []
234
+ : [{ reference: reached(file, value, to), location: pointer }]);
235
+ for (const { reference, location } of fields) {
236
+ if (!reaches(reference, typeOf, binary))
237
+ return refuse(file, 'unresolved-reference', { location });
124
238
  }
125
- for (const { section, to, hrefs } of sectionReferences.get(file)) {
126
- for (const href of hrefs) {
127
- if (!reaches(file, href, to, types, binaryTargets))
128
- return refuse(file, 'unresolved-reference', { location: section });
129
- }
239
+ if (!linked(file, library[file]))
240
+ return refuse(file, 'unresolved-link');
241
+ const sections = sectionReferences
242
+ .get(file)
243
+ .flatMap(({ section, to, crossRoom, hrefs }) => hrefs.flatMap((href) => crossing(file, href, crossRoom)
244
+ ? []
245
+ : [{ reference: reached(file, href, to), location: section }]));
246
+ for (const { reference, location } of sections) {
247
+ if (!reaches(reference, typeOf, binary))
248
+ return refuse(file, 'unresolved-reference', { location });
130
249
  }
131
- }
132
- const missing = new Map();
133
- for (const [file, document] of documents) {
134
- const kind = instanceType(document);
135
- if (kind === null || kind === BUILT_IN_TYPE)
136
- continue;
137
- const definition = definitions.get(kind);
138
- if (definition === undefined)
139
- continue;
140
- const named = [
141
- ...references(document, definition),
142
- ...sectionReferences
143
- .get(file)
144
- .flatMap(({ to, hrefs }) => hrefs.map((value) => ({ value, to }))),
145
- ];
146
- for (const { value, to } of named) {
147
- if (to.includes(ATTACHMENT))
148
- continue;
149
- const resolved = classifyHref(file, value);
150
- if (resolved.kind !== 'path')
151
- continue;
152
- if (types.has(resolved.path) || binaryTargets.has(resolved.path))
250
+ for (const { reference } of [...fields, ...sections]) {
251
+ if (!missing(reference, typeOf, binary))
153
252
  continue;
154
- const referrers = missing.get(resolved.path) ?? [];
155
- referrers.push(file);
156
- missing.set(resolved.path, referrers);
253
+ const path = reference.target;
254
+ if (least === undefined || path < least.path)
255
+ least = { path, referrers: [] };
256
+ if (path === least.path && least.referrers.at(-1) !== file)
257
+ least.referrers.push(file);
157
258
  }
259
+ yield file;
158
260
  }
159
- const missingPaths = [...missing.keys()].sort();
160
- if (missingPaths.length > 0) {
161
- const path = missingPaths[0];
162
- const referrers = [...new Set(missing.get(path) ?? [])].sort();
163
- return refuse(path, 'referenced-missing-file', { referrers });
164
- }
261
+ if (least !== undefined)
262
+ return refuse(least.path, 'referenced-missing-file', { referrers: least.referrers });
165
263
  return { verdict: 'accept' };
166
264
  }
265
+ export function validate(library, context = { binaryTargets: [] }, model = { types: {} }) {
266
+ const steps = validating(library, context, model);
267
+ let step = steps.next();
268
+ while (step.done !== true)
269
+ step = steps.next();
270
+ return step.value;
271
+ }
@@ -0,0 +1 @@
1
+ { "binaryTargets": [] }
@@ -0,0 +1 @@
1
+ { "verdict": "accept" }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: client
3
+ name: Acme
4
+ ---
5
+
6
+ Paid by [the March invoice](/rooms/finance/invoices/march.md#total).
@@ -0,0 +1,3 @@
1
+ client:
2
+ properties:
3
+ name: { type: string }
@@ -0,0 +1 @@
1
+ { "binaryTargets": [] }
@@ -0,0 +1 @@
1
+ { "verdict": "accept" }
@@ -0,0 +1,9 @@
1
+ ---
2
+ type: client
3
+ name: Acme
4
+ finance: /rooms/finance/invoices/acme.md
5
+ ---
6
+
7
+ ## Invoices
8
+
9
+ - [March](/rooms/finance/invoices/march.md)
@@ -0,0 +1,9 @@
1
+ client:
2
+ properties:
3
+ name: { type: string }
4
+ finance: { type: string, reference: invoice, cross_room: true }
5
+ sections:
6
+ Invoices: { kind: list, items: { type: string, reference: invoice, cross_room: true } }
7
+ invoice:
8
+ properties:
9
+ name: { type: string }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/mdmodel",
3
- "version": "0.1.0-c64451b2127b4",
3
+ "version": "0.1.0-c669bdbad8a03",
4
4
  "description": "MD Type language, parser, validator, structured change application, Markdown syntax profile, and conformance fixtures",
5
5
  "license": "MIT",
6
6
  "type": "module",