@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.
- package/API.md +170 -170
- package/README.md +15 -18
- package/dist/blocks.d.ts.map +1 -1
- package/dist/blocks.js +6 -13
- package/dist/condition.d.ts +1 -1
- package/dist/condition.d.ts.map +1 -1
- package/dist/condition.js +19 -9
- package/dist/definition.d.ts +15 -0
- package/dist/definition.d.ts.map +1 -1
- package/dist/definition.js +224 -24
- package/dist/divide.d.ts.map +1 -1
- package/dist/divide.js +18 -8
- package/dist/document.d.ts +5 -2
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +318 -182
- package/dist/index.d.ts +10 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/links.d.ts +1 -0
- package/dist/links.d.ts.map +1 -1
- package/dist/links.js +22 -11
- package/dist/paths.d.ts +9 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +24 -1
- package/dist/project.d.ts +1 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +35 -10
- package/dist/references.d.ts +2 -2
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +6 -1
- package/dist/types.d.ts +44 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/validate.d.ts +8 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +205 -100
- 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/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(
|
|
22
|
-
|
|
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 (
|
|
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
|
-
|
|
30
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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 @@
|
|
|
1
|
+
{ "binaryTargets": [] }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "verdict": "accept" }
|
|
@@ -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-
|
|
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",
|