@rse/specbook 1.2.1 → 1.2.3
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 +6 -2
- package/dst/specbook-api.d.ts +1 -0
- package/dst/specbook-api.js +19 -5
- package/dst/specbook-cli.js +1 -1
- package/dst/specbook-cmd-describe.js +2 -2
- package/dst/specbook-cmd-lint.js +10 -2
- package/dst/specbook-cmd-preview.js +12 -9
- package/dst/specbook-config.d.ts +4 -0
- package/dst/specbook-config.js +48 -10
- package/dst/specbook-diagram.js +56 -37
- package/dst/specbook-export-common.js +33 -22
- package/dst/specbook-export-html.css +62 -0
- package/dst/specbook-export-html.d.ts +1 -0
- package/dst/specbook-export-html.js +178 -63
- package/dst/specbook-export-md.js +9 -7
- package/dst/specbook-export-pdf.js +22 -8
- package/dst/specbook-format-schema.js +2 -0
- package/dst/specbook-format-spec.js +2 -0
- package/dst/specbook-format.md +3 -2
- package/dst/specbook-format.yaml +200 -216
- package/dst/specbook-link.d.ts +1 -0
- package/dst/specbook-link.js +11 -4
- package/dst/specbook-parse-common.d.ts +1 -0
- package/dst/specbook-parse-common.js +5 -0
- package/dst/specbook-parse-semantic.d.ts +2 -1
- package/dst/specbook-parse-semantic.js +37 -25
- package/dst/specbook-parse-syntax.js +20 -23
- package/dst/specbook-parse.d.ts +5 -0
- package/dst/specbook-parse.js +20 -1
- package/package.json +3 -3
package/dst/specbook-link.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ interface LinkNode {
|
|
|
8
8
|
name: string;
|
|
9
9
|
}
|
|
10
10
|
export type LinkIndex = LinkNode[];
|
|
11
|
+
export declare const assignId: (index: LinkIndex, object: SpecObject, id: string) => void;
|
|
11
12
|
export declare const chainOf: (index: LinkIndex, object: SpecObject) => SpecObject[];
|
|
12
13
|
export interface LinkTarget {
|
|
13
14
|
target?: SpecObject;
|
package/dst/specbook-link.js
CHANGED
|
@@ -34,6 +34,13 @@ const lookup = (index) => {
|
|
|
34
34
|
}
|
|
35
35
|
return result;
|
|
36
36
|
};
|
|
37
|
+
/* assign the id of an indexed object, dropping the lookup structures
|
|
38
|
+
of its index (and with them the memoized resolutions), as they are
|
|
39
|
+
keyed on the ids as of their build and would freeze the old one */
|
|
40
|
+
export const assignId = (index, object, id) => {
|
|
41
|
+
object.id = id;
|
|
42
|
+
lookups.delete(index);
|
|
43
|
+
};
|
|
37
44
|
/* the ancestor-or-self chain of an object, root first
|
|
38
45
|
(empty for an object unknown to the index) */
|
|
39
46
|
export const chainOf = (index, object) => {
|
|
@@ -127,16 +134,16 @@ export const resolveSet = (index, reference) => {
|
|
|
127
134
|
subtree, which beats one in another artifact) */
|
|
128
135
|
const nearest = (index, matches, from) => {
|
|
129
136
|
const chain = chainOf(index, from);
|
|
130
|
-
const
|
|
137
|
+
const proximity = (object) => {
|
|
131
138
|
const other = chainOf(index, object);
|
|
132
139
|
let i = 0;
|
|
133
140
|
while (i < chain.length && i < other.length && chain[i] === other[i])
|
|
134
141
|
i++;
|
|
135
142
|
return i;
|
|
136
143
|
};
|
|
137
|
-
const
|
|
138
|
-
const best = Math.max(...
|
|
139
|
-
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);
|
|
140
147
|
};
|
|
141
148
|
/* resolve a reference into a unique target: a single segment tries the
|
|
142
149
|
ordered variants (1) id/anchor, (2) name, (3) "KIND:name-or-id", where
|
|
@@ -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+/;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type Spec, type SpecObject } from "./specbook-format-spec.js";
|
|
1
|
+
import { type Spec, type SpecObject, type SpecProperty } from "./specbook-format-spec.js";
|
|
2
2
|
import { type Schema, type SchemaObject } from "./specbook-format-schema.js";
|
|
3
3
|
import { ParseContext } from "./specbook-parse-common.js";
|
|
4
4
|
export declare const resolveArtifact: (config: Schema, object: SpecObject) => SchemaObject | undefined;
|
|
5
5
|
export declare const collectSchemas: (specification: Spec, config: Schema) => Map<SpecObject, SchemaObject>;
|
|
6
|
+
export declare const collectParenProps: (schemas: Map<SpecObject, SchemaObject>) => Map<SpecObject, SpecProperty>;
|
|
6
7
|
export declare const validate: (ctx: ParseContext, specification: Spec, config: Schema) => void;
|
|
7
8
|
export declare const validateReferences: (ctx: ParseContext, specification: Spec) => void;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
|
-
import { referenceRegex, resolveUnique, resolveSet, chainOf, plainText } from "./specbook-link.js";
|
|
6
|
+
import { referenceRegex, resolveUnique, resolveSet, chainOf, plainText, assignId } from "./specbook-link.js";
|
|
7
7
|
import { compileValueExpr, splitItems, anchored } from "./specbook-parse-value.js";
|
|
8
8
|
/* the syntax of a value consisting of exactly one Wiki-style reference */
|
|
9
9
|
const singleReferenceRegex = /^\[\[([^[\]]+)\]\]$/;
|
|
@@ -112,21 +112,23 @@ const checkPropValue = (ctx, object, prop, expr, property, meta) => {
|
|
|
112
112
|
`property "${prop.name}" value "${property.value}" is not a member of "${prop.value}"` :
|
|
113
113
|
`property "${prop.name}" value "${property.value}" does not match pattern "${prop.value}"`);
|
|
114
114
|
};
|
|
115
|
-
/*
|
|
116
|
-
|
|
115
|
+
/* derive the synthetic property the trailing parenthesized name token
|
|
116
|
+
of an object is consumed as: the first still missing configured
|
|
117
117
|
property whose pattern the token matches */
|
|
118
118
|
const parenProp = (object, props) => {
|
|
119
119
|
const paren = object.paren;
|
|
120
120
|
if (paren === undefined)
|
|
121
121
|
return undefined;
|
|
122
|
-
|
|
123
|
-
&& directMatches(compileValueExpr(
|
|
122
|
+
const prop = props.find((p) => p.value !== undefined && !object.properties.some((q) => q.key === p.name)
|
|
123
|
+
&& directMatches(compileValueExpr(p.value), paren));
|
|
124
|
+
return prop !== undefined ? { key: prop.name, value: paren } : undefined;
|
|
124
125
|
};
|
|
125
126
|
/* check the property values flagged unique for distinctness among
|
|
126
127
|
the sibling objects of the same kind and the ones flagged present
|
|
127
|
-
for occurring on at least one
|
|
128
|
-
values
|
|
129
|
-
|
|
128
|
+
for occurring on at least one sibling (all values, or only the
|
|
129
|
+
values matching the regexp or enum expression of the flag), where
|
|
130
|
+
a kind without any sibling fails a present flag, too, but only for
|
|
131
|
+
an optional kind, as the missing-kind check reports a required one */
|
|
130
132
|
const checkSiblingFlags = (ctx, object, childs, meta) => {
|
|
131
133
|
const marker = (flag) => flag === undefined || flag === false ? undefined :
|
|
132
134
|
{ filter: typeof flag === "string" ? compileValueExpr(flag) : undefined };
|
|
@@ -152,12 +154,12 @@ const checkSiblingFlags = (ctx, object, childs, meta) => {
|
|
|
152
154
|
if (first === undefined)
|
|
153
155
|
seen.set(value, sibling);
|
|
154
156
|
else {
|
|
155
|
-
const siblingMeta = ctx.
|
|
157
|
+
const siblingMeta = ctx.metaOf(sibling);
|
|
156
158
|
ctx.diagnose(siblingMeta.file, ctx.propMeta.get(property)?.line ?? siblingMeta.line, `value "${value}" of unique property "${prop.name}" on ${sibling.kind} "${sibling.name}" ` +
|
|
157
159
|
`already used by preceding ${sibling.kind} "${first.name}"`);
|
|
158
160
|
}
|
|
159
161
|
}
|
|
160
|
-
if (present !== undefined && siblings.length > 0 && !found)
|
|
162
|
+
if (present !== undefined && (siblings.length > 0 || child.optional === true) && !found)
|
|
161
163
|
ctx.diagnose(meta.file, meta.line, `no ${child.kind} below ${object.kind} "${object.name}" carries property "${prop.name}"` +
|
|
162
164
|
(typeof prop.present === "string" ? ` with a value matching "${prop.present}"` : ""));
|
|
163
165
|
}
|
|
@@ -165,7 +167,7 @@ const checkSiblingFlags = (ctx, object, childs, meta) => {
|
|
|
165
167
|
};
|
|
166
168
|
/* validate a single object (and recursively its childs) against its schema */
|
|
167
169
|
const validateObject = (ctx, object, schema, level) => {
|
|
168
|
-
const meta = ctx.
|
|
170
|
+
const meta = ctx.metaOf(object);
|
|
169
171
|
const props = schema.props ?? [];
|
|
170
172
|
/* check the name convention (the configured name of non-artifact
|
|
171
173
|
objects is a regular expression pattern) */
|
|
@@ -199,7 +201,7 @@ const validateObject = (ctx, object, schema, level) => {
|
|
|
199
201
|
/* check the configured child objects */
|
|
200
202
|
const childs = schema.childs ?? [];
|
|
201
203
|
for (const child of object.childs) {
|
|
202
|
-
const childMeta = ctx.
|
|
204
|
+
const childMeta = ctx.metaOf(child);
|
|
203
205
|
const childSchema = childs.find((c) => c.kind === child.kind);
|
|
204
206
|
if (childSchema === undefined) {
|
|
205
207
|
ctx.diagnose(childMeta.file, childMeta.line, `unknown object kind "${child.kind}" below ${object.kind} "${object.name}"`);
|
|
@@ -255,6 +257,19 @@ export const collectSchemas = (specification, config) => {
|
|
|
255
257
|
}
|
|
256
258
|
return schemas;
|
|
257
259
|
};
|
|
260
|
+
/* derive the synthetic properties supplied by the consumed parenthesized
|
|
261
|
+
name tokens of a schema mapping, so a consumer without a parsing
|
|
262
|
+
context -- the diagram derivation of an export -- sees exactly the
|
|
263
|
+
properties the validation sees */
|
|
264
|
+
export const collectParenProps = (schemas) => {
|
|
265
|
+
const parenProps = new Map();
|
|
266
|
+
for (const [object, schema] of schemas) {
|
|
267
|
+
const property = parenProp(object, schema.props ?? []);
|
|
268
|
+
if (property !== undefined)
|
|
269
|
+
parenProps.set(object, property);
|
|
270
|
+
}
|
|
271
|
+
return parenProps;
|
|
272
|
+
};
|
|
258
273
|
/* report the objects colliding on kind and id among their siblings
|
|
259
274
|
(recursively from the artifacts down), as such objects derive
|
|
260
275
|
identical anchor paths and hence cannot be addressed distinctly */
|
|
@@ -266,7 +281,7 @@ const checkIds = (ctx, objects) => {
|
|
|
266
281
|
if (first === undefined)
|
|
267
282
|
seen.set(key, object);
|
|
268
283
|
else {
|
|
269
|
-
const meta = ctx.
|
|
284
|
+
const meta = ctx.metaOf(object);
|
|
270
285
|
ctx.diagnose(meta.file, meta.line, `id "${object.id}" of ${object.kind} "${object.name}" collides with preceding ${object.kind} "${first.name}"`);
|
|
271
286
|
}
|
|
272
287
|
checkIds(ctx, object.childs);
|
|
@@ -337,7 +352,7 @@ const checkAutomata = (ctx, schemas) => {
|
|
|
337
352
|
const reachable = closure(initials, succs);
|
|
338
353
|
const coreachable = closure(finals, preds);
|
|
339
354
|
for (const node of nodes) {
|
|
340
|
-
const meta = ctx.
|
|
355
|
+
const meta = ctx.metaOf(node);
|
|
341
356
|
const name = `${node.kind} "${node.name}" of ${object.kind} "${object.name}"`;
|
|
342
357
|
if (initials.length > 0 && !reachable.has(node))
|
|
343
358
|
ctx.diagnose(meta.file, meta.line, `${name} is not reachable from the initial ${node.kind} through any ${automaton.edges}`);
|
|
@@ -368,7 +383,7 @@ const checkRelations = (ctx, schemas) => {
|
|
|
368
383
|
for (const [prop, objects] of groups) {
|
|
369
384
|
const members = new Set(objects);
|
|
370
385
|
const locate = (object) => {
|
|
371
|
-
const meta = ctx.
|
|
386
|
+
const meta = ctx.metaOf(object);
|
|
372
387
|
const property = findProp(ctx, object, prop.name);
|
|
373
388
|
const line = property !== undefined ? ctx.propMeta.get(property)?.line : undefined;
|
|
374
389
|
return { file: meta.file, line: line ?? meta.line };
|
|
@@ -455,7 +470,7 @@ const checkReferenced = (ctx, specification, schemas) => {
|
|
|
455
470
|
const covered = Array.from(referrers.get(object) ?? [])
|
|
456
471
|
.some((referrer) => chainOf(ctx.linkIndex, referrer).some((o) => admissible.has(o)));
|
|
457
472
|
if (!covered) {
|
|
458
|
-
const meta = ctx.
|
|
473
|
+
const meta = ctx.metaOf(object);
|
|
459
474
|
ctx.diagnose(meta.file, meta.line, `${object.kind} "${object.name}" is not referenced from any object matching ` +
|
|
460
475
|
schema.referenced.map((entry) => `"${entry}"`).join(" or "), "warning");
|
|
461
476
|
}
|
|
@@ -469,19 +484,16 @@ export const validate = (ctx, specification, config) => {
|
|
|
469
484
|
precedence), which has to happen before any property check, as
|
|
470
485
|
those resolve references by id across all artifacts */
|
|
471
486
|
const schemas = collectSchemas(specification, config);
|
|
487
|
+
for (const [object, property] of collectParenProps(schemas))
|
|
488
|
+
ctx.parenProps.set(object, property);
|
|
472
489
|
const promote = (object) => {
|
|
473
490
|
/* an object below an unresolved artifact or an unknown kind
|
|
474
491
|
carries no schema and hence no property able to consume the
|
|
475
492
|
token, so the token still acts as the id and a single
|
|
476
493
|
unresolved heading no longer cascades into unresolvable
|
|
477
494
|
references all over the corpus */
|
|
478
|
-
if (object.paren !== undefined)
|
|
479
|
-
|
|
480
|
-
if (prop !== undefined)
|
|
481
|
-
ctx.parenProps.set(object, { key: prop.name, value: object.paren });
|
|
482
|
-
else if (object.anchor === undefined)
|
|
483
|
-
object.id = object.paren;
|
|
484
|
-
}
|
|
495
|
+
if (object.paren !== undefined && object.anchor === undefined && !ctx.parenProps.has(object))
|
|
496
|
+
assignId(ctx.linkIndex, object, object.paren);
|
|
485
497
|
object.childs.forEach(promote);
|
|
486
498
|
};
|
|
487
499
|
for (const artifact of specification.artifacts)
|
|
@@ -492,7 +504,7 @@ export const validate = (ctx, specification, config) => {
|
|
|
492
504
|
const position = new Map();
|
|
493
505
|
for (const artifact of specification.artifacts) {
|
|
494
506
|
for (const object of artifact.objects) {
|
|
495
|
-
const meta = ctx.
|
|
507
|
+
const meta = ctx.metaOf(object);
|
|
496
508
|
const schema = schemas.get(object);
|
|
497
509
|
if (schema === undefined) {
|
|
498
510
|
ctx.diagnose(meta.file, meta.line, `unknown artifact "${object.kind}: ${object.name}" (id "${object.id}")`);
|
|
@@ -533,7 +545,7 @@ export const validateReferences = (ctx, specification) => {
|
|
|
533
545
|
}
|
|
534
546
|
};
|
|
535
547
|
const walk = (object) => {
|
|
536
|
-
const meta = ctx.
|
|
548
|
+
const meta = ctx.metaOf(object);
|
|
537
549
|
check(object, object.name, meta.file, meta.line);
|
|
538
550
|
for (const property of object.properties)
|
|
539
551
|
check(object, property.value, meta.file, ctx.propMeta.get(property)?.line ?? meta.line);
|
|
@@ -201,13 +201,15 @@ const parseConcise = (ctx, item, parent, file, line, group) => {
|
|
|
201
201
|
let kind;
|
|
202
202
|
let name;
|
|
203
203
|
if (group !== undefined) {
|
|
204
|
-
const nm = head.match(/^`([^`]+)
|
|
204
|
+
const nm = head.match(/^`([^`]+)`\s*([^`]*)$|^([^`:]+)$/);
|
|
205
205
|
if (nm === null) {
|
|
206
206
|
ctx.diagnose(file, line, `unrecognized ${group} item "${text}"`);
|
|
207
207
|
return;
|
|
208
208
|
}
|
|
209
209
|
kind = group;
|
|
210
|
-
name
|
|
210
|
+
/* a backquoted name keeps its trailing decorations outside the
|
|
211
|
+
backquotes, so they are re-joined for the marker detection below */
|
|
212
|
+
name = (nm[1] !== undefined ? `${nm[1]} ${nm[2]}` : nm[3]).trim();
|
|
211
213
|
}
|
|
212
214
|
else {
|
|
213
215
|
const km = head.match(/^([^:]+):\s*(.*)$/);
|
|
@@ -278,17 +280,17 @@ const parseHeading = (ctx, token, state, stamps, file, line) => {
|
|
|
278
280
|
state.group = null;
|
|
279
281
|
if (heading.malformed !== undefined)
|
|
280
282
|
ctx.diagnose(file, line, `malformed anchor "${heading.malformed}" in heading`);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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;
|
|
292
294
|
return;
|
|
293
295
|
}
|
|
294
296
|
const object = {
|
|
@@ -305,17 +307,10 @@ const parseHeading = (ctx, token, state, stamps, file, line) => {
|
|
|
305
307
|
if (heading.primary)
|
|
306
308
|
object.primary = true;
|
|
307
309
|
ctx.objectMeta.set(object, { file, line });
|
|
308
|
-
if (
|
|
310
|
+
if (parent === undefined)
|
|
309
311
|
state.artifacts.push({ ...stamps, objects: [object] });
|
|
310
|
-
else
|
|
311
|
-
const parent = state.stack[depth - 2];
|
|
312
|
-
if (parent === undefined) {
|
|
313
|
-
ctx.diagnose(file, line, `heading level ${depth} without parent object`);
|
|
314
|
-
state.current = null;
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
312
|
+
else
|
|
317
313
|
parent.childs.push(object);
|
|
318
|
-
}
|
|
319
314
|
state.stack.length = depth - 1;
|
|
320
315
|
state.stack[depth - 1] = object;
|
|
321
316
|
state.current = object;
|
|
@@ -360,7 +355,9 @@ export const parseFile = (ctx, source) => {
|
|
|
360
355
|
const flush = () => {
|
|
361
356
|
const current = state.current;
|
|
362
357
|
if (current !== null && parts.length > 0) {
|
|
363
|
-
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)
|
|
364
361
|
current.description = splitDescription(parts.join("\n\n").trim());
|
|
365
362
|
else
|
|
366
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.childs.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.3",
|
|
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",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"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"
|