@rse/specbook 1.2.2 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -203
- package/dst/specbook-api.js +7 -3
- package/dst/specbook-cmd-describe.js +2 -2
- package/dst/specbook-cmd-export.js +4 -1
- package/dst/specbook-cmd-lint.js +10 -2
- package/dst/specbook-cmd-preview.js +7 -10
- package/dst/specbook-config.d.ts +4 -0
- package/dst/specbook-config.js +37 -14
- package/dst/specbook-diagram.js +16 -10
- package/dst/specbook-export-ast.js +1 -1
- package/dst/specbook-export-common.js +34 -23
- package/dst/specbook-export-html.css +62 -0
- package/dst/specbook-export-html.d.ts +1 -1
- package/dst/specbook-export-html.js +145 -82
- package/dst/specbook-export-md.js +17 -15
- package/dst/specbook-export-pdf.js +11 -9
- package/dst/specbook-format-schema.d.ts +1 -1
- package/dst/specbook-format-schema.js +3 -1
- package/dst/specbook-format-spec.d.ts +1 -1
- package/dst/specbook-format-spec.js +3 -1
- package/dst/specbook-format.md +4 -4
- package/dst/specbook-format.yaml +240 -256
- package/dst/specbook-link.js +6 -6
- package/dst/specbook-parse-common.d.ts +1 -0
- package/dst/specbook-parse-common.js +5 -0
- package/dst/specbook-parse-semantic.js +31 -31
- package/dst/specbook-parse-syntax.js +21 -26
- package/dst/specbook-parse.d.ts +5 -0
- package/dst/specbook-parse.js +20 -1
- package/package.json +4 -4
package/dst/specbook-link.js
CHANGED
|
@@ -55,7 +55,7 @@ export const buildLinkIndex = (specification) => {
|
|
|
55
55
|
const walk = (object, parent) => {
|
|
56
56
|
const node = { object, parent, pos: index.length, name: plainText(object.name) };
|
|
57
57
|
index.push(node);
|
|
58
|
-
for (const child of object.
|
|
58
|
+
for (const child of object.children)
|
|
59
59
|
walk(child, node);
|
|
60
60
|
};
|
|
61
61
|
for (const artifact of specification.artifacts)
|
|
@@ -134,16 +134,16 @@ export const resolveSet = (index, reference) => {
|
|
|
134
134
|
subtree, which beats one in another artifact) */
|
|
135
135
|
const nearest = (index, matches, from) => {
|
|
136
136
|
const chain = chainOf(index, from);
|
|
137
|
-
const
|
|
137
|
+
const proximity = (object) => {
|
|
138
138
|
const other = chainOf(index, object);
|
|
139
139
|
let i = 0;
|
|
140
140
|
while (i < chain.length && i < other.length && chain[i] === other[i])
|
|
141
141
|
i++;
|
|
142
142
|
return i;
|
|
143
143
|
};
|
|
144
|
-
const
|
|
145
|
-
const best = Math.max(...
|
|
146
|
-
return matches.filter((_, i) =>
|
|
144
|
+
const proximities = matches.map((object) => proximity(object));
|
|
145
|
+
const best = Math.max(...proximities);
|
|
146
|
+
return matches.filter((_, i) => proximities[i] === best);
|
|
147
147
|
};
|
|
148
148
|
/* resolve a reference into a unique target: a single segment tries the
|
|
149
149
|
ordered variants (1) id/anchor, (2) name, (3) "KIND:name-or-id", where
|
|
@@ -173,7 +173,7 @@ export const resolveUnique = (index, reference, from) => {
|
|
|
173
173
|
};
|
|
174
174
|
/* derive the fully-qualified anchor path of every object: the
|
|
175
175
|
"<kind>-<id>" segments (whitespace in kinds dashed) of the ancestor
|
|
176
|
-
chain joined with "." (the index lists parents before
|
|
176
|
+
chain joined with "." (the index lists parents before children) */
|
|
177
177
|
export const anchorPaths = (index) => {
|
|
178
178
|
const paths = new Map();
|
|
179
179
|
for (const node of index) {
|
|
@@ -24,6 +24,7 @@ export declare class ParseContext {
|
|
|
24
24
|
parenProps: WeakMap<SpecObject, SpecProperty>;
|
|
25
25
|
assets: Set<string>;
|
|
26
26
|
diagnose(file: string, line: number, message: string, severity?: DiagnosticSeverity): void;
|
|
27
|
+
metaOf(object: SpecObject): ObjectMeta;
|
|
27
28
|
}
|
|
28
29
|
export declare const becauseRegex: RegExp;
|
|
29
30
|
export declare const embeddingRegex: RegExp;
|
|
@@ -20,6 +20,11 @@ export class ParseContext {
|
|
|
20
20
|
diagnose(file, line, message, severity = "error") {
|
|
21
21
|
this.diagnostics.push({ file, line, column: 1, severity, message });
|
|
22
22
|
}
|
|
23
|
+
/* the source location of an object (defensively falling back onto
|
|
24
|
+
line 1 of no file for an object unknown to the syntactic phase) */
|
|
25
|
+
metaOf(object) {
|
|
26
|
+
return this.objectMeta.get(object) ?? { file: "", line: 1 };
|
|
27
|
+
}
|
|
23
28
|
}
|
|
24
29
|
/* the marker separating a statement from its rationale */
|
|
25
30
|
export const becauseRegex = /,\s*(?:\*\*BECAUSE\*\*|BECAUSE)\s+/;
|
|
@@ -129,11 +129,11 @@ const parenProp = (object, props) => {
|
|
|
129
129
|
values matching the regexp or enum expression of the flag), where
|
|
130
130
|
a kind without any sibling fails a present flag, too, but only for
|
|
131
131
|
an optional kind, as the missing-kind check reports a required one */
|
|
132
|
-
const checkSiblingFlags = (ctx, object,
|
|
132
|
+
const checkSiblingFlags = (ctx, object, children, meta) => {
|
|
133
133
|
const marker = (flag) => flag === undefined || flag === false ? undefined :
|
|
134
134
|
{ filter: typeof flag === "string" ? compileValueExpr(flag) : undefined };
|
|
135
|
-
for (const child of
|
|
136
|
-
const siblings = object.
|
|
135
|
+
for (const child of children) {
|
|
136
|
+
const siblings = object.children.filter((c) => c.kind === child.kind);
|
|
137
137
|
for (const prop of child.props ?? []) {
|
|
138
138
|
const unique = marker(prop.unique);
|
|
139
139
|
const present = marker(prop.present);
|
|
@@ -154,7 +154,7 @@ const checkSiblingFlags = (ctx, object, childs, meta) => {
|
|
|
154
154
|
if (first === undefined)
|
|
155
155
|
seen.set(value, sibling);
|
|
156
156
|
else {
|
|
157
|
-
const siblingMeta = ctx.
|
|
157
|
+
const siblingMeta = ctx.metaOf(sibling);
|
|
158
158
|
ctx.diagnose(siblingMeta.file, ctx.propMeta.get(property)?.line ?? siblingMeta.line, `value "${value}" of unique property "${prop.name}" on ${sibling.kind} "${sibling.name}" ` +
|
|
159
159
|
`already used by preceding ${sibling.kind} "${first.name}"`);
|
|
160
160
|
}
|
|
@@ -165,9 +165,9 @@ const checkSiblingFlags = (ctx, object, childs, meta) => {
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
};
|
|
168
|
-
/* validate a single object (and recursively its
|
|
168
|
+
/* validate a single object (and recursively its children) against its schema */
|
|
169
169
|
const validateObject = (ctx, object, schema, level) => {
|
|
170
|
-
const meta = ctx.
|
|
170
|
+
const meta = ctx.metaOf(object);
|
|
171
171
|
const props = schema.props ?? [];
|
|
172
172
|
/* check the name convention (the configured name of non-artifact
|
|
173
173
|
objects is a regular expression pattern) */
|
|
@@ -199,10 +199,10 @@ const validateObject = (ctx, object, schema, level) => {
|
|
|
199
199
|
if (!props.some((p) => p.name === property.key))
|
|
200
200
|
ctx.diagnose(meta.file, ctx.propMeta.get(property)?.line ?? meta.line, `unknown property "${property.key}" on ${object.kind} "${object.name}"`);
|
|
201
201
|
/* check the configured child objects */
|
|
202
|
-
const
|
|
203
|
-
for (const child of object.
|
|
204
|
-
const childMeta = ctx.
|
|
205
|
-
const childSchema =
|
|
202
|
+
const children = schema.children ?? [];
|
|
203
|
+
for (const child of object.children) {
|
|
204
|
+
const childMeta = ctx.metaOf(child);
|
|
205
|
+
const childSchema = children.find((c) => c.kind === child.kind);
|
|
206
206
|
if (childSchema === undefined) {
|
|
207
207
|
ctx.diagnose(childMeta.file, childMeta.line, `unknown object kind "${child.kind}" below ${object.kind} "${object.name}"`);
|
|
208
208
|
continue;
|
|
@@ -210,20 +210,20 @@ const validateObject = (ctx, object, schema, level) => {
|
|
|
210
210
|
validateObject(ctx, child, childSchema, level + 1);
|
|
211
211
|
}
|
|
212
212
|
/* check the sibling-scoped property flags of the child kinds */
|
|
213
|
-
checkSiblingFlags(ctx, object,
|
|
213
|
+
checkSiblingFlags(ctx, object, children, meta);
|
|
214
214
|
/* report the configured child object kinds which are missing */
|
|
215
|
-
for (const child of
|
|
215
|
+
for (const child of children)
|
|
216
216
|
if (child.optional !== true
|
|
217
|
-
&& !object.
|
|
217
|
+
&& !object.children.some((c) => c.kind === child.kind))
|
|
218
218
|
ctx.diagnose(meta.file, meta.line, `required object kind "${child.kind}" missing below ${object.kind} "${object.name}"`);
|
|
219
|
-
/* order the
|
|
219
|
+
/* order the children and properties exactly along the schema
|
|
220
220
|
definition (the sort is stable, so objects of the same kind
|
|
221
221
|
and unknown items keep their document order) */
|
|
222
222
|
const kindPos = (kind) => {
|
|
223
|
-
const i =
|
|
224
|
-
return i >= 0 ? i :
|
|
223
|
+
const i = children.findIndex((c) => c.kind === kind);
|
|
224
|
+
return i >= 0 ? i : children.length;
|
|
225
225
|
};
|
|
226
|
-
object.
|
|
226
|
+
object.children.sort((a, b) => kindPos(a.kind) - kindPos(b.kind));
|
|
227
227
|
const propPos = (key) => {
|
|
228
228
|
const i = props.findIndex((p) => p.name === key);
|
|
229
229
|
return i >= 0 ? i : props.length;
|
|
@@ -242,8 +242,8 @@ export const collectSchemas = (specification, config) => {
|
|
|
242
242
|
const schemas = new Map();
|
|
243
243
|
const walk = (object, schema) => {
|
|
244
244
|
schemas.set(object, schema);
|
|
245
|
-
for (const child of object.
|
|
246
|
-
const childSchema = (schema.
|
|
245
|
+
for (const child of object.children) {
|
|
246
|
+
const childSchema = (schema.children ?? []).find((c) => c.kind === child.kind);
|
|
247
247
|
if (childSchema !== undefined)
|
|
248
248
|
walk(child, childSchema);
|
|
249
249
|
}
|
|
@@ -281,10 +281,10 @@ const checkIds = (ctx, objects) => {
|
|
|
281
281
|
if (first === undefined)
|
|
282
282
|
seen.set(key, object);
|
|
283
283
|
else {
|
|
284
|
-
const meta = ctx.
|
|
284
|
+
const meta = ctx.metaOf(object);
|
|
285
285
|
ctx.diagnose(meta.file, meta.line, `id "${object.id}" of ${object.kind} "${object.name}" collides with preceding ${object.kind} "${first.name}"`);
|
|
286
286
|
}
|
|
287
|
-
checkIds(ctx, object.
|
|
287
|
+
checkIds(ctx, object.children);
|
|
288
288
|
}
|
|
289
289
|
};
|
|
290
290
|
/* the objects a property of an object references (resolved and
|
|
@@ -317,7 +317,7 @@ const checkAutomata = (ctx, schemas) => {
|
|
|
317
317
|
const automaton = schema.automaton;
|
|
318
318
|
if (automaton === undefined)
|
|
319
319
|
continue;
|
|
320
|
-
const nodes = object.
|
|
320
|
+
const nodes = object.children.filter((child) => child.kind === automaton.nodes);
|
|
321
321
|
const nodeSet = new Set(nodes);
|
|
322
322
|
const flagged = (node, name) => {
|
|
323
323
|
const property = findProp(ctx, node, name);
|
|
@@ -329,7 +329,7 @@ const checkAutomata = (ctx, schemas) => {
|
|
|
329
329
|
the node set is skipped, as the property checks report it) */
|
|
330
330
|
const succs = new Map(nodes.map((node) => [node, new Set()]));
|
|
331
331
|
const preds = new Map(nodes.map((node) => [node, new Set()]));
|
|
332
|
-
for (const edge of object.
|
|
332
|
+
for (const edge of object.children.filter((child) => child.kind === automaton.edges)) {
|
|
333
333
|
const source = referencedObjects(ctx, edge, automaton.source)[0];
|
|
334
334
|
const target = referencedObjects(ctx, edge, automaton.target)[0];
|
|
335
335
|
if (source !== undefined && target !== undefined && nodeSet.has(source) && nodeSet.has(target)) {
|
|
@@ -352,7 +352,7 @@ const checkAutomata = (ctx, schemas) => {
|
|
|
352
352
|
const reachable = closure(initials, succs);
|
|
353
353
|
const coreachable = closure(finals, preds);
|
|
354
354
|
for (const node of nodes) {
|
|
355
|
-
const meta = ctx.
|
|
355
|
+
const meta = ctx.metaOf(node);
|
|
356
356
|
const name = `${node.kind} "${node.name}" of ${object.kind} "${object.name}"`;
|
|
357
357
|
if (initials.length > 0 && !reachable.has(node))
|
|
358
358
|
ctx.diagnose(meta.file, meta.line, `${name} is not reachable from the initial ${node.kind} through any ${automaton.edges}`);
|
|
@@ -383,7 +383,7 @@ const checkRelations = (ctx, schemas) => {
|
|
|
383
383
|
for (const [prop, objects] of groups) {
|
|
384
384
|
const members = new Set(objects);
|
|
385
385
|
const locate = (object) => {
|
|
386
|
-
const meta = ctx.
|
|
386
|
+
const meta = ctx.metaOf(object);
|
|
387
387
|
const property = findProp(ctx, object, prop.name);
|
|
388
388
|
const line = property !== undefined ? ctx.propMeta.get(property)?.line : undefined;
|
|
389
389
|
return { file: meta.file, line: line ?? meta.line };
|
|
@@ -446,7 +446,7 @@ const checkReferenced = (ctx, specification, schemas) => {
|
|
|
446
446
|
if (target !== undefined && !chain.includes(target))
|
|
447
447
|
referrers.set(target, (referrers.get(target) ?? new Set()).add(object));
|
|
448
448
|
}
|
|
449
|
-
object.
|
|
449
|
+
object.children.forEach(walk);
|
|
450
450
|
};
|
|
451
451
|
for (const artifact of specification.artifacts)
|
|
452
452
|
artifact.objects.forEach(walk);
|
|
@@ -470,7 +470,7 @@ const checkReferenced = (ctx, specification, schemas) => {
|
|
|
470
470
|
const covered = Array.from(referrers.get(object) ?? [])
|
|
471
471
|
.some((referrer) => chainOf(ctx.linkIndex, referrer).some((o) => admissible.has(o)));
|
|
472
472
|
if (!covered) {
|
|
473
|
-
const meta = ctx.
|
|
473
|
+
const meta = ctx.metaOf(object);
|
|
474
474
|
ctx.diagnose(meta.file, meta.line, `${object.kind} "${object.name}" is not referenced from any object matching ` +
|
|
475
475
|
schema.referenced.map((entry) => `"${entry}"`).join(" or "), "warning");
|
|
476
476
|
}
|
|
@@ -494,7 +494,7 @@ export const validate = (ctx, specification, config) => {
|
|
|
494
494
|
references all over the corpus */
|
|
495
495
|
if (object.paren !== undefined && object.anchor === undefined && !ctx.parenProps.has(object))
|
|
496
496
|
assignId(ctx.linkIndex, object, object.paren);
|
|
497
|
-
object.
|
|
497
|
+
object.children.forEach(promote);
|
|
498
498
|
};
|
|
499
499
|
for (const artifact of specification.artifacts)
|
|
500
500
|
artifact.objects.forEach(promote);
|
|
@@ -504,7 +504,7 @@ export const validate = (ctx, specification, config) => {
|
|
|
504
504
|
const position = new Map();
|
|
505
505
|
for (const artifact of specification.artifacts) {
|
|
506
506
|
for (const object of artifact.objects) {
|
|
507
|
-
const meta = ctx.
|
|
507
|
+
const meta = ctx.metaOf(object);
|
|
508
508
|
const schema = schemas.get(object);
|
|
509
509
|
if (schema === undefined) {
|
|
510
510
|
ctx.diagnose(meta.file, meta.line, `unknown artifact "${object.kind}: ${object.name}" (id "${object.id}")`);
|
|
@@ -545,7 +545,7 @@ export const validateReferences = (ctx, specification) => {
|
|
|
545
545
|
}
|
|
546
546
|
};
|
|
547
547
|
const walk = (object) => {
|
|
548
|
-
const meta = ctx.
|
|
548
|
+
const meta = ctx.metaOf(object);
|
|
549
549
|
check(object, object.name, meta.file, meta.line);
|
|
550
550
|
for (const property of object.properties)
|
|
551
551
|
check(object, property.value, meta.file, ctx.propMeta.get(property)?.line ?? meta.line);
|
|
@@ -554,7 +554,7 @@ export const validateReferences = (ctx, specification) => {
|
|
|
554
554
|
if (object.description.rationale !== undefined)
|
|
555
555
|
check(object, object.description.rationale, meta.file, meta.line);
|
|
556
556
|
}
|
|
557
|
-
object.
|
|
557
|
+
object.children.forEach(walk);
|
|
558
558
|
};
|
|
559
559
|
for (const artifact of specification.artifacts)
|
|
560
560
|
artifact.objects.forEach(walk);
|
|
@@ -150,7 +150,7 @@ const embed = (ctx, object, file) => {
|
|
|
150
150
|
load(object.description, object.description.description, line);
|
|
151
151
|
for (const property of object.properties)
|
|
152
152
|
load(property, property.value, ctx.propMeta.get(property)?.line ?? line);
|
|
153
|
-
for (const child of object.
|
|
153
|
+
for (const child of object.children)
|
|
154
154
|
embed(ctx, child, file);
|
|
155
155
|
};
|
|
156
156
|
/* parse an unordered list of key-values and/or concise-format objects */
|
|
@@ -229,7 +229,7 @@ const parseConcise = (ctx, item, parent, file, line, group) => {
|
|
|
229
229
|
id: anchor ?? slugify(name),
|
|
230
230
|
name,
|
|
231
231
|
properties: [],
|
|
232
|
-
|
|
232
|
+
children: []
|
|
233
233
|
};
|
|
234
234
|
if (anchor !== undefined)
|
|
235
235
|
object.anchor = anchor;
|
|
@@ -267,7 +267,7 @@ const parseConcise = (ctx, item, parent, file, line, group) => {
|
|
|
267
267
|
ctx.diagnose(file, subLine, `nested ordered list below ${kind} "${name}" ignored`);
|
|
268
268
|
subLine += (sub.raw.match(/\n/g) ?? []).length;
|
|
269
269
|
}
|
|
270
|
-
parent.
|
|
270
|
+
parent.children.push(object);
|
|
271
271
|
};
|
|
272
272
|
/* parse a heading token into either a grouping container (e.g. "### STATE",
|
|
273
273
|
collecting objects of its kind below the parent object) or a new object
|
|
@@ -280,17 +280,17 @@ const parseHeading = (ctx, token, state, stamps, file, line) => {
|
|
|
280
280
|
state.group = null;
|
|
281
281
|
if (heading.malformed !== undefined)
|
|
282
282
|
ctx.diagnose(file, line, `malformed anchor "${heading.malformed}" in heading`);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
283
|
+
/* a nested heading requires the enclosing object one level above */
|
|
284
|
+
const parent = depth > 1 ? state.stack[depth - 2] : undefined;
|
|
285
|
+
if (depth > 1 && parent === undefined) {
|
|
286
|
+
ctx.diagnose(file, line, `heading level ${depth} without parent object`);
|
|
287
|
+
state.current = null;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (parent !== undefined && heading.name === "") {
|
|
291
|
+
state.group = { parent, kind: heading.kind };
|
|
292
|
+
state.current = parent;
|
|
293
|
+
state.stack.length = depth - 1;
|
|
294
294
|
return;
|
|
295
295
|
}
|
|
296
296
|
const object = {
|
|
@@ -298,7 +298,7 @@ const parseHeading = (ctx, token, state, stamps, file, line) => {
|
|
|
298
298
|
id: heading.id ?? (depth === 1 ? heading.paren : undefined) ?? slugify(heading.name),
|
|
299
299
|
name: heading.name,
|
|
300
300
|
properties: [],
|
|
301
|
-
|
|
301
|
+
children: []
|
|
302
302
|
};
|
|
303
303
|
if (heading.id !== undefined)
|
|
304
304
|
object.anchor = heading.id;
|
|
@@ -307,17 +307,10 @@ const parseHeading = (ctx, token, state, stamps, file, line) => {
|
|
|
307
307
|
if (heading.primary)
|
|
308
308
|
object.primary = true;
|
|
309
309
|
ctx.objectMeta.set(object, { file, line });
|
|
310
|
-
if (
|
|
310
|
+
if (parent === undefined)
|
|
311
311
|
state.artifacts.push({ ...stamps, objects: [object] });
|
|
312
|
-
else
|
|
313
|
-
|
|
314
|
-
if (parent === undefined) {
|
|
315
|
-
ctx.diagnose(file, line, `heading level ${depth} without parent object`);
|
|
316
|
-
state.current = null;
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
parent.childs.push(object);
|
|
320
|
-
}
|
|
312
|
+
else
|
|
313
|
+
parent.children.push(object);
|
|
321
314
|
state.stack.length = depth - 1;
|
|
322
315
|
state.stack[depth - 1] = object;
|
|
323
316
|
state.current = object;
|
|
@@ -362,7 +355,9 @@ export const parseFile = (ctx, source) => {
|
|
|
362
355
|
const flush = () => {
|
|
363
356
|
const current = state.current;
|
|
364
357
|
if (current !== null && parts.length > 0) {
|
|
365
|
-
if (
|
|
358
|
+
if (state.group !== null)
|
|
359
|
+
ctx.diagnose(source.file, partsLine, `content ignored below ${state.group.kind} container of ${current.kind} "${current.name}"`);
|
|
360
|
+
else if (current.description === undefined)
|
|
366
361
|
current.description = splitDescription(parts.join("\n\n").trim());
|
|
367
362
|
else
|
|
368
363
|
ctx.diagnose(source.file, partsLine, `content ignored, as ${current.kind} "${current.name}" already carries a description`);
|
package/dst/specbook-parse.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { Spec } from "./specbook-format-spec.js";
|
|
1
2
|
import { type Schema } from "./specbook-format-schema.js";
|
|
2
3
|
import { type SourceFile, type ParseResult } from "./specbook-parse-common.js";
|
|
3
4
|
export { type SourceFile, type ParseResult } from "./specbook-parse-common.js";
|
|
4
5
|
export { resolveArtifact } from "./specbook-parse-semantic.js";
|
|
5
6
|
export declare const parseSpecification: (sources: SourceFile[], config?: Schema) => ParseResult;
|
|
7
|
+
export declare const specStatistics: (specification: Spec) => {
|
|
8
|
+
objects: number;
|
|
9
|
+
links: number;
|
|
10
|
+
};
|
package/dst/specbook-parse.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as v from "valibot";
|
|
7
7
|
import { Spec } from "./specbook-format-spec.js";
|
|
8
|
-
import { buildLinkIndex } from "./specbook-link.js";
|
|
8
|
+
import { buildLinkIndex, referenceRegex, plainText } from "./specbook-link.js";
|
|
9
9
|
import { ParseContext } from "./specbook-parse-common.js";
|
|
10
10
|
import { parseFile } from "./specbook-parse-syntax.js";
|
|
11
11
|
import { validate, validateReferences } from "./specbook-parse-semantic.js";
|
|
@@ -38,3 +38,22 @@ export const parseSpecification = (sources, config) => {
|
|
|
38
38
|
}
|
|
39
39
|
return { specification, diagnostics: ctx.diagnostics, assets: Array.from(ctx.assets) };
|
|
40
40
|
};
|
|
41
|
+
/* the statistics of a specification: the number of defined objects
|
|
42
|
+
(recursively) and of relationships, i.e. the Wiki-style references
|
|
43
|
+
in the names, property values, descriptions, and rationales */
|
|
44
|
+
export const specStatistics = (specification) => {
|
|
45
|
+
let objects = 0;
|
|
46
|
+
let links = 0;
|
|
47
|
+
const walk = (object) => {
|
|
48
|
+
objects++;
|
|
49
|
+
const texts = [object.name, ...object.properties.map((p) => p.value)];
|
|
50
|
+
if (object.description !== undefined)
|
|
51
|
+
texts.push(object.description.description, object.description.rationale ?? "");
|
|
52
|
+
for (const text of texts)
|
|
53
|
+
links += (plainText(text).match(referenceRegex) ?? []).length;
|
|
54
|
+
object.children.forEach(walk);
|
|
55
|
+
};
|
|
56
|
+
for (const artifact of specification.artifacts)
|
|
57
|
+
artifact.objects.forEach(walk);
|
|
58
|
+
return { objects, links };
|
|
59
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rse/specbook",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"description": "Markdown-based Specification Format (API, CLI and MCP service)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"pdf-lib": "1.17.1",
|
|
25
25
|
"subset-font": "2.7.0",
|
|
26
26
|
"@rse/mrcs": "0.9.2",
|
|
27
|
-
"@rse/gradia": "1.1.
|
|
27
|
+
"@rse/gradia": "1.1.9",
|
|
28
28
|
"@toon-format/toon": "4.1.1",
|
|
29
29
|
"valibot": "1.4.2",
|
|
30
30
|
"nunjucks": "3.2.4",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"source-code-error": "1.2.0",
|
|
37
37
|
"commander": "15.0.0",
|
|
38
38
|
"chalk": "6.0.0",
|
|
39
|
-
"
|
|
39
|
+
"chokidar5": "npm:chokidar@5.0.0",
|
|
40
40
|
"glob": "13.0.6",
|
|
41
41
|
"es-toolkit": "1.52.0",
|
|
42
|
-
"fastify": "5.12.
|
|
42
|
+
"fastify": "5.12.3",
|
|
43
43
|
"@fastify/websocket": "11.3.0",
|
|
44
44
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
45
45
|
"zod": "4.5.4"
|