@rse/specbook 1.5.2 → 1.6.1
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/LICENSE.txt +201 -0
- package/README.md +288 -43
- package/dst/specbook-api.d.ts +23 -3
- package/dst/specbook-api.js +133 -59
- package/dst/specbook-cli.js +93 -26
- package/dst/specbook-cmd-describe.js +4 -4
- package/dst/specbook-cmd-export-schema.d.ts +8 -0
- package/dst/specbook-cmd-export-schema.js +441 -0
- package/dst/specbook-cmd-export.js +23 -6
- package/dst/specbook-cmd-init-sample.d.ts +4 -0
- package/dst/specbook-cmd-init-sample.js +322 -0
- package/dst/specbook-cmd-init.d.ts +1 -0
- package/dst/specbook-cmd-init.js +42 -10
- package/dst/specbook-cmd-lint.js +46 -48
- package/dst/specbook-cmd-query.d.ts +30 -0
- package/dst/specbook-cmd-query.js +165 -0
- package/dst/specbook-config.d.ts +9 -1
- package/dst/specbook-config.js +135 -10
- package/dst/specbook-coverage.js +2 -2
- package/dst/specbook-diagram.d.ts +7 -5
- package/dst/specbook-diagram.js +546 -143
- package/dst/specbook-export-ast.d.ts +1 -1
- package/dst/specbook-export-ast.js +11 -8
- package/dst/specbook-export-code.js +3 -3
- package/dst/specbook-export-common.d.ts +1 -1
- package/dst/specbook-export-common.js +3 -2
- package/dst/specbook-export-diagram.d.ts +1 -1
- package/dst/specbook-export-diagram.js +25 -7
- package/dst/specbook-export-html-fold.d.ts +16 -6
- package/dst/specbook-export-html-fold.js +65 -44
- package/dst/specbook-export-html-render.js +107 -26
- package/dst/specbook-export-html-search.js +18 -20
- package/dst/specbook-export-html-template.js +38 -9
- package/dst/specbook-export-html-widget.d.ts +2 -0
- package/dst/specbook-export-html-widget.js +219 -19
- package/dst/specbook-export-html.css +208 -38
- package/dst/specbook-export-html.js +96 -53
- package/dst/specbook-export-md.d.ts +2 -1
- package/dst/specbook-export-md.js +7 -8
- package/dst/specbook-export-pdf.js +1 -1
- package/dst/specbook-files.d.ts +2 -0
- package/dst/specbook-files.js +41 -0
- package/dst/specbook-format-meta.yaml +401 -0
- package/dst/specbook-format-pm.yaml +1240 -0
- package/dst/specbook-format-schema.d.ts +33 -3
- package/dst/specbook-format-schema.js +49 -3
- package/dst/specbook-format.md +427 -47
- package/dst/specbook-format.yaml +524 -228
- package/dst/specbook-ignore.js +2 -2
- package/dst/specbook-link.d.ts +4 -0
- package/dst/specbook-link.js +61 -6
- package/dst/specbook-mcp.js +103 -19
- package/dst/specbook-parse-semantic.d.ts +1 -0
- package/dst/specbook-parse-semantic.js +11 -4
- package/dst/specbook-parse-syntax.d.ts +1 -0
- package/dst/specbook-parse-syntax.js +1 -1
- package/dst/specbook-parse.d.ts +1 -1
- package/dst/specbook-parse.js +3 -3
- package/dst/specbook-project.d.ts +2 -0
- package/dst/specbook-project.js +15 -5
- package/dst/specbook-remote.d.ts +28 -0
- package/dst/specbook-remote.js +429 -0
- package/package.json +32 -20
package/dst/specbook-ignore.js
CHANGED
|
@@ -155,8 +155,8 @@ export const excluder = (dir) => {
|
|
|
155
155
|
return rules;
|
|
156
156
|
};
|
|
157
157
|
return (file) => {
|
|
158
|
-
const rel = path.relative(root, path.resolve(file)).
|
|
159
|
-
if (rel === "" || rel.startsWith("../"))
|
|
158
|
+
const rel = path.relative(root, path.resolve(file)).split(path.sep).join("/");
|
|
159
|
+
if (rel === "" || rel === ".." || rel.startsWith("../") || path.isAbsolute(rel))
|
|
160
160
|
return false;
|
|
161
161
|
const segments = rel.split("/");
|
|
162
162
|
/* walk the directory chain from the working tree root down to
|
package/dst/specbook-link.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Spec, SpecObject } from "./specbook-format-spec.js";
|
|
2
2
|
export declare const referenceRegex: RegExp;
|
|
3
|
+
export declare const outsideCode: (text: string, apply: (part: string) => string) => string;
|
|
3
4
|
export declare const plainText: (text: string) => string;
|
|
5
|
+
export declare const referenceText: (text: string) => string;
|
|
4
6
|
interface LinkNode {
|
|
5
7
|
object: SpecObject;
|
|
6
8
|
parent?: LinkNode;
|
|
@@ -17,6 +19,8 @@ export interface LinkTarget {
|
|
|
17
19
|
export declare const buildLinkIndex: (specification: Spec) => LinkIndex;
|
|
18
20
|
export declare const resolveSet: (index: LinkIndex, reference: string) => SpecObject[];
|
|
19
21
|
export declare const resolveUnique: (index: LinkIndex, reference: string, from?: SpecObject) => LinkTarget;
|
|
22
|
+
export declare const referenceTo: (index: LinkIndex, target: SpecObject, from: SpecObject) => string;
|
|
23
|
+
export declare const referenceTargets: (index: LinkIndex, object: SpecObject) => SpecObject[];
|
|
20
24
|
export declare const anchorPaths: (index: LinkIndex) => Map<SpecObject, string>;
|
|
21
25
|
export declare const expandReferences: (text: string, replace: (reference: string) => string) => string;
|
|
22
26
|
export {};
|
package/dst/specbook-link.js
CHANGED
|
@@ -8,21 +8,38 @@ export const referenceRegex = /\[\[([^[\]]+)\]\]/g;
|
|
|
8
8
|
/* the fenced code blocks of a Markdown text (from the opening fence line
|
|
9
9
|
up to the closing fence line, or to the end of an unclosed one) */
|
|
10
10
|
const fenceRegex = /^[ \t]*(`{3,}|~{3,})[^\n]*(?:\n[\s\S]*?(?:\n[ \t]*\1[`~]*[ \t]*(?=\n|$)|(?![\s\S]))|(?![\s\S]))/gm;
|
|
11
|
+
/* the inline code spans of a Markdown text (a backtick run up to the
|
|
12
|
+
next backtick run of the same length) */
|
|
13
|
+
const spanRegex = /(?<!`)(`+)(?!`)[\s\S]*?(?<!`)\1(?!`)/g;
|
|
11
14
|
/* apply a transformation to the parts of a text outside of its fenced
|
|
12
|
-
code blocks, which stay verbatim, as code
|
|
13
|
-
|
|
15
|
+
code blocks and inline code spans, which stay verbatim, as code
|
|
16
|
+
carries no references */
|
|
17
|
+
export const outsideCode = (text, apply) => {
|
|
18
|
+
const outsideSpans = (part) => {
|
|
19
|
+
let result = "";
|
|
20
|
+
let last = 0;
|
|
21
|
+
for (const m of part.matchAll(spanRegex)) {
|
|
22
|
+
result += apply(part.slice(last, m.index)) + m[0];
|
|
23
|
+
last = m.index + m[0].length;
|
|
24
|
+
}
|
|
25
|
+
return result + apply(part.slice(last));
|
|
26
|
+
};
|
|
14
27
|
let result = "";
|
|
15
28
|
let last = 0;
|
|
16
29
|
for (const m of text.matchAll(fenceRegex)) {
|
|
17
|
-
result +=
|
|
30
|
+
result += outsideSpans(text.slice(last, m.index)) + m[0];
|
|
18
31
|
last = m.index + m[0].length;
|
|
19
32
|
}
|
|
20
|
-
return result +
|
|
33
|
+
return result + outsideSpans(text.slice(last));
|
|
21
34
|
};
|
|
22
35
|
/* strip the fenced code blocks of a description and the inline code
|
|
23
36
|
markup of a name, property value, or description (preserved in the
|
|
24
37
|
AST for rendering) for matching and labeling */
|
|
25
38
|
export const plainText = (text) => text.replace(fenceRegex, "").replace(/`/g, "");
|
|
39
|
+
/* strip the fenced code blocks and the inline code spans (with their
|
|
40
|
+
contents) of a text for scanning its references, as code carries no
|
|
41
|
+
references */
|
|
42
|
+
export const referenceText = (text) => text.replace(fenceRegex, "").replace(spanRegex, "");
|
|
26
43
|
/* provide the lookup structures of an index, built once on first use */
|
|
27
44
|
const lookups = new WeakMap();
|
|
28
45
|
const lookup = (index) => {
|
|
@@ -186,6 +203,44 @@ export const resolveUnique = (index, reference, from) => {
|
|
|
186
203
|
matches = nearest(index, matches, from);
|
|
187
204
|
return { target: matches.length === 1 ? matches[0] : undefined, ambiguous: matches.length > 1 };
|
|
188
205
|
};
|
|
206
|
+
/* the shortest reference uniquely resolving onto a target from a
|
|
207
|
+
referencing object: the kind-qualified name of the target, prefixed
|
|
208
|
+
by as many of its ancestors (their parenthesized id, else their
|
|
209
|
+
kind-qualified name) as the resolution needs, and as a last resort
|
|
210
|
+
the kind-qualified id of the target (unique among its siblings),
|
|
211
|
+
should its name be shared by a sibling of its kind */
|
|
212
|
+
export const referenceTo = (index, target, from) => {
|
|
213
|
+
const chain = chainOf(index, target);
|
|
214
|
+
const quote = (text) => /[.:]/.test(text) ? `"${text}"` : text;
|
|
215
|
+
const parents = chain.slice(0, -1).map((object) => object.paren !== undefined ? object.paren : `${object.kind}:${quote(object.name)}`);
|
|
216
|
+
const lasts = [`${target.kind}:${quote(target.name)}`, `${target.kind}:${quote(target.id)}`];
|
|
217
|
+
for (const last of lasts) {
|
|
218
|
+
const segments = [...parents, last];
|
|
219
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
220
|
+
const reference = segments.slice(i).join(".");
|
|
221
|
+
if (resolveUnique(index, reference, from).target === target)
|
|
222
|
+
return reference;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return [...parents, lasts[0]].join(".");
|
|
226
|
+
};
|
|
227
|
+
/* resolve the references of the own texts of an object (its name,
|
|
228
|
+
property values, description, rationale, and elaboration) into
|
|
229
|
+
their distinct targets, in order of occurrence, leniently skipping
|
|
230
|
+
the unresolvable ones, as the reference pass reports those */
|
|
231
|
+
export const referenceTargets = (index, object) => {
|
|
232
|
+
const texts = [object.name, ...object.properties.map((property) => property.value)];
|
|
233
|
+
if (object.description !== undefined)
|
|
234
|
+
texts.push(object.description.description, object.description.rationale ?? "", object.description.elaboration ?? "");
|
|
235
|
+
const targets = new Set();
|
|
236
|
+
for (const text of texts)
|
|
237
|
+
for (const m of referenceText(text).matchAll(referenceRegex)) {
|
|
238
|
+
const target = resolveUnique(index, m[1].trim(), object).target;
|
|
239
|
+
if (target !== undefined && target !== object)
|
|
240
|
+
targets.add(target);
|
|
241
|
+
}
|
|
242
|
+
return Array.from(targets);
|
|
243
|
+
};
|
|
189
244
|
/* derive the fully-qualified anchor path of every object: the
|
|
190
245
|
"<kind>-<id>" segments (whitespace in kinds dashed) of the ancestor
|
|
191
246
|
chain joined with "." (the index lists parents before children) */
|
|
@@ -200,5 +255,5 @@ export const anchorPaths = (index) => {
|
|
|
200
255
|
return paths;
|
|
201
256
|
};
|
|
202
257
|
/* expand all references of a text via a per-reference replacer
|
|
203
|
-
(except inside its fenced code blocks) */
|
|
204
|
-
export const expandReferences = (text, replace) =>
|
|
258
|
+
(except inside its fenced code blocks and inline code spans) */
|
|
259
|
+
export const expandReferences = (text, replace) => outsideCode(text, (part) => part.replace(referenceRegex, (_, reference) => replace(reference.trim())));
|
package/dst/specbook-mcp.js
CHANGED
|
@@ -8,7 +8,7 @@ import * as path from "node:path";
|
|
|
8
8
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
9
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
10
|
import { z } from "zod";
|
|
11
|
-
import { SpecBook, renderDiagnostic, renderVerbose, formats, parseOutputSpec, describeFormats, describeParts, compressLevels, projectFile, omitAspects, version } from "./specbook-api.js";
|
|
11
|
+
import { SpecBook, renderDiagnostic, renderVerbose, formats, parseOutputSpec, describeFormats, describeParts, compressLevels, projectFile, omitAspects, fetchModes, version, renderNodes, queryFormats, parseQueryOutput } from "./specbook-api.js";
|
|
12
12
|
/* render an error with its cause chain into a tool error result */
|
|
13
13
|
const errorResult = (err) => {
|
|
14
14
|
let msg = err instanceof Error ? err.message : String(err);
|
|
@@ -24,17 +24,26 @@ export const serveMcp = async (verbose) => {
|
|
|
24
24
|
const server = new McpServer({ name: "specbook", version });
|
|
25
25
|
const specbook = new SpecBook({ verbose });
|
|
26
26
|
/* the input schema fields shared by the specification processing tools */
|
|
27
|
-
const
|
|
28
|
-
|
|
27
|
+
const remote = "\"npm:<pkg>[@<range>][/<path>]\" or \"github:<user>/<repo>[/<path>][#<ref>]\"";
|
|
28
|
+
const config = z.array(z.string()).optional().describe("YAML schema configuration files, glob " +
|
|
29
|
+
`patterns, or remote references (${remote}, or an "http[s]://..." URL), merged in order ` +
|
|
30
|
+
"(\"std\" for the bundled standard schema configuration, \"meta\" for the bundled meta one, " +
|
|
31
|
+
"\"pm\" for the bundled project management one; " +
|
|
29
32
|
"default: the SPECBOOK_CONFIG environment variable, else the \"config\" entry " +
|
|
30
33
|
"of the project configuration file, else the bundled standard " +
|
|
31
34
|
"schema configuration)");
|
|
32
35
|
const basedir = z.string().optional().describe("base directory of the specification Markdown files " +
|
|
33
36
|
"(default: the SPECBOOK_BASEDIR environment variable, else the \"basedir\" entry " +
|
|
34
37
|
"of the project configuration file, else \".\")");
|
|
38
|
+
const fetch = z.enum(fetchModes).optional().describe("when to fetch the remote references through " +
|
|
39
|
+
"the network (\"never\": the download cache only, \"auto\": whenever the download cache lacks them " +
|
|
40
|
+
"or is no longer fresh, \"always\": ignoring the download cache; default: \"auto\")");
|
|
35
41
|
const gitignore = z.boolean().optional().describe("skip the artifact files excluded by the Git " +
|
|
36
42
|
"exclude rules (the \".gitignore\" files, \"info/exclude\", and the global excludes " +
|
|
37
43
|
"file), treating such a file exactly like an absent one (default: false)");
|
|
44
|
+
const sample = z.boolean().optional().describe("scaffold a sample object per allowed object kind " +
|
|
45
|
+
"into the artifact files, with placeholder values satisfying the schema configuration " +
|
|
46
|
+
"(default: false)");
|
|
38
47
|
/* the working directory of the client, which this server does not
|
|
39
48
|
necessarily share: it anchors the relative paths and starts the
|
|
40
49
|
upward search for the project configuration file */
|
|
@@ -46,10 +55,11 @@ export const serveMcp = async (verbose) => {
|
|
|
46
55
|
server.registerTool("specbook_init", {
|
|
47
56
|
title: "Initialize Specification",
|
|
48
57
|
description: "Initialize the configured specification artifact files below the base directory " +
|
|
49
|
-
"with their frontmatter and artifact heading,
|
|
58
|
+
"with their frontmatter and artifact heading (or, with \"sample\", with a sample object per " +
|
|
59
|
+
"allowed object kind), skipping already existing files, and " +
|
|
50
60
|
`create the project configuration file "${projectFile}" in the working directory, ` +
|
|
51
61
|
"persisting the effective schema configuration and base directory, unless one already applies.",
|
|
52
|
-
inputSchema: { config, basedir, cwd }
|
|
62
|
+
inputSchema: { config, basedir, fetch, sample, cwd }
|
|
53
63
|
}, async (args) => {
|
|
54
64
|
try {
|
|
55
65
|
const result = await specbook.init(args);
|
|
@@ -65,9 +75,9 @@ export const serveMcp = async (verbose) => {
|
|
|
65
75
|
});
|
|
66
76
|
server.registerTool("specbook_lint", {
|
|
67
77
|
title: "Lint Specification",
|
|
68
|
-
description: "Lint
|
|
69
|
-
"
|
|
70
|
-
inputSchema: { config, basedir, gitignore, cwd }
|
|
78
|
+
description: "Lint all specification Markdown files below the base directory against the " +
|
|
79
|
+
"YAML schema configuration and return all diagnostics.",
|
|
80
|
+
inputSchema: { config, basedir, fetch, gitignore, cwd }
|
|
71
81
|
}, async (args) => {
|
|
72
82
|
try {
|
|
73
83
|
const result = await specbook.lint(args);
|
|
@@ -82,20 +92,25 @@ export const serveMcp = async (verbose) => {
|
|
|
82
92
|
});
|
|
83
93
|
server.registerTool("specbook_export", {
|
|
84
94
|
title: "Export Specification",
|
|
85
|
-
description: "Export
|
|
86
|
-
"
|
|
95
|
+
description: "Export all specification Markdown files below the base directory (or, with " +
|
|
96
|
+
"\"schema\", the effective YAML schema configuration " +
|
|
97
|
+
"itself, described as a SpecBook document through the bundled meta schema configuration) " +
|
|
98
|
+
"as JSON, JSON5, " +
|
|
87
99
|
"YAML, TOON, HTML, PDF, or normalized Markdown. The result is written to the output file " +
|
|
88
100
|
"if an output path is given, else it is returned directly (PDF as a base64-encoded resource). " +
|
|
89
101
|
"The normalized Markdown embeds its images as \"data:\" URL definitions (so it is large), and " +
|
|
90
102
|
"its remaining image references are re-based onto the directory of the " +
|
|
91
103
|
"output file, while a directly returned result keeps them unchanged.",
|
|
92
|
-
inputSchema: { config, basedir, gitignore, cwd,
|
|
104
|
+
inputSchema: { config, basedir, fetch, gitignore, cwd,
|
|
93
105
|
format: z.enum(formats).optional().describe("output format (default: inferred from the " +
|
|
94
106
|
"output file extension, else json)"),
|
|
95
107
|
output: z.string().optional().describe("output file path (\"-\" or omitted returns the result directly)"),
|
|
96
108
|
omit: z.array(z.string()).optional().describe("content aspects to omit from the HTML, " +
|
|
97
109
|
"the PDF, and (the diagrams) the AST output, instead of leaving them foldable: \"diagram\" " +
|
|
98
|
-
`(all diagrams) or "${omitAspects.join("\", \"")}" (default: none)`)
|
|
110
|
+
`(all diagrams) or "${omitAspects.join("\", \"")}" (default: none)`),
|
|
111
|
+
schema: z.boolean().optional().describe("export the effective YAML schema configuration " +
|
|
112
|
+
"itself as a SpecBook document instead of the specification (the base directory is not " +
|
|
113
|
+
"applicable then; default: false)")
|
|
99
114
|
}
|
|
100
115
|
}, async (args) => {
|
|
101
116
|
try {
|
|
@@ -110,8 +125,8 @@ export const serveMcp = async (verbose) => {
|
|
|
110
125
|
const rebase = spec.format === "md" && spec.output !== undefined && spec.output !== "-" ?
|
|
111
126
|
path.dirname(spec.output) : undefined;
|
|
112
127
|
const [data] = await specbook.export({ config: args.config, basedir: args.basedir,
|
|
113
|
-
cwd: args.cwd, formats: [spec.format], rebase: [rebase],
|
|
114
|
-
gitignore: args.gitignore });
|
|
128
|
+
cwd: args.cwd, fetch: args.fetch, formats: [spec.format], rebase: [rebase],
|
|
129
|
+
omit: args.omit, gitignore: args.gitignore, schema: args.schema });
|
|
115
130
|
if (spec.output !== undefined && spec.output !== "-") {
|
|
116
131
|
await fs.promises.writeFile(outputOf(args, spec.output), data);
|
|
117
132
|
return { content: [{ type: "text", text: `exported specification into "${spec.output}" (${data.length} bytes)` }] };
|
|
@@ -128,6 +143,47 @@ export const serveMcp = async (verbose) => {
|
|
|
128
143
|
return errorResult(err);
|
|
129
144
|
}
|
|
130
145
|
});
|
|
146
|
+
server.registerTool("specbook_query", {
|
|
147
|
+
title: "Query Specification",
|
|
148
|
+
description: "Load all specification Markdown files below the base directory (or, with " +
|
|
149
|
+
"\"schema\", the effective YAML schema configuration itself, described as a SpecBook document " +
|
|
150
|
+
"through the bundled meta schema configuration), strictly validated, and query its Abstract " +
|
|
151
|
+
"Syntax Tree (AST) with an XPath-like ASTq selector (like \"// USE-CASE [ @STATUS == 'Done' ]\"). " +
|
|
152
|
+
"The root node is of type \"specification\", its children are of type \"artifact\" (attributes " +
|
|
153
|
+
"\"created\", \"modified\", and \"file\"), and the objects below are typed by their kind (attributes " +
|
|
154
|
+
"\"kind\", \"id\", \"anchor\", \"paren\", \"name\", \"primary\", \"description\", \"rationale\", " +
|
|
155
|
+
"\"elaboration\", and one per property key), where the query function \"refs()\" yields the objects " +
|
|
156
|
+
"the references of an object resolve to. The matched nodes are rendered as a JSON (or JSON5, " +
|
|
157
|
+
"YAML, or TOON) array (their subtrees included, the embedded file contents dropped), written to " +
|
|
158
|
+
"the output file if an output path is given, else returned directly.",
|
|
159
|
+
inputSchema: { config, basedir, fetch, gitignore, cwd,
|
|
160
|
+
schema: z.boolean().optional().describe("query the effective YAML schema configuration " +
|
|
161
|
+
"itself as a SpecBook document instead of the specification (the base directory is not " +
|
|
162
|
+
"applicable then; default: false)"),
|
|
163
|
+
selector: z.string().describe("the ASTq selector of the nodes to return"),
|
|
164
|
+
params: z.record(z.string(), z.string()).optional().describe("the values of the \"{name}\" " +
|
|
165
|
+
"parameters of the selector (default: none)"),
|
|
166
|
+
format: z.enum(queryFormats).optional().describe("output format (default: inferred from the " +
|
|
167
|
+
"output file like for the export, else json)"),
|
|
168
|
+
output: z.string().optional().describe("output file path (\"-\" or omitted returns the result directly)")
|
|
169
|
+
}
|
|
170
|
+
}, async (args) => {
|
|
171
|
+
try {
|
|
172
|
+
const { selector, params, format, output, ...options } = args;
|
|
173
|
+
const spec = parseQueryOutput(output ?? "-", format);
|
|
174
|
+
const document = await specbook.load(options);
|
|
175
|
+
const text = renderNodes(document.query(selector, params), spec.format);
|
|
176
|
+
if (spec.output !== "-") {
|
|
177
|
+
await fs.promises.writeFile(outputOf(args, spec.output), text);
|
|
178
|
+
return { content: [{ type: "text", text: `queried specification into "${spec.output}" ` +
|
|
179
|
+
`(${Buffer.byteLength(text)} bytes)` }] };
|
|
180
|
+
}
|
|
181
|
+
return { content: [{ type: "text", text }] };
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
return errorResult(err);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
131
187
|
server.registerTool("specbook_describe", {
|
|
132
188
|
title: "Describe Specification Format",
|
|
133
189
|
description: "Describe the SpecBook models and formats as a Markdown document, which embeds the " +
|
|
@@ -136,20 +192,24 @@ export const serveMcp = async (verbose) => {
|
|
|
136
192
|
"points to the artifacts of that particular project. The description can be reduced to a " +
|
|
137
193
|
"single part and emitted as the raw original file content instead of Markdown.",
|
|
138
194
|
inputSchema: {
|
|
139
|
-
config: z.array(z.string()).optional().describe("YAML schema configuration files
|
|
140
|
-
|
|
195
|
+
config: z.array(z.string()).optional().describe("YAML schema configuration files, glob " +
|
|
196
|
+
`patterns, or remote references (${remote}, or an "http[s]://..." URL), merged in order ` +
|
|
197
|
+
"(\"std\" for the bundled standard schema configuration; " +
|
|
141
198
|
"default: the SPECBOOK_CONFIG environment variable, else the \"config\" entry " +
|
|
142
199
|
"of the project configuration file, else the bundled standard " +
|
|
143
200
|
"schema configuration, embedded)"),
|
|
144
201
|
basedir: z.string().optional().describe("base directory of the specification Markdown files " +
|
|
145
202
|
"(default: the SPECBOOK_BASEDIR environment variable, else the \"basedir\" entry " +
|
|
146
203
|
"of the project configuration file)"),
|
|
204
|
+
fetch,
|
|
147
205
|
embed: z.boolean().optional().describe("embed the given YAML schema configuration instead " +
|
|
148
|
-
"of just referencing it (default: false; the bundled standard one
|
|
206
|
+
"of just referencing it (default: false; the bundled standard one and a remote one are " +
|
|
207
|
+
"always embedded)"),
|
|
149
208
|
compress: z.literal(compressLevels).optional().describe("compression level of the emitted " +
|
|
150
209
|
"YAML schema configuration (embedded or raw): 0 for verbatim, 1 for re-emitted with 2-space " +
|
|
151
|
-
"indentation and without comments, 2 for additionally without its \"refs\", \"
|
|
152
|
-
"\"format\" fields, or 3 for additionally without its
|
|
210
|
+
"indentation and without comments, 2 for additionally without its \"refs\", \"badge\", " +
|
|
211
|
+
"\"multiple\", \"diagram\", and \"format\" fields, or 3 for additionally without its " +
|
|
212
|
+
"\"desc\" fields (default: 2)"),
|
|
153
213
|
format: z.enum(describeFormats).optional().describe("output format: \"md\" for Markdown or " +
|
|
154
214
|
"\"raw\" for the raw original file content of the part (default: \"md\")"),
|
|
155
215
|
part: z.enum(describeParts).optional().describe("document part: \"all\" for the entire " +
|
|
@@ -173,5 +233,29 @@ export const serveMcp = async (verbose) => {
|
|
|
173
233
|
return errorResult(err);
|
|
174
234
|
}
|
|
175
235
|
});
|
|
236
|
+
server.registerTool("specbook_search", {
|
|
237
|
+
title: "Search Schema Configurations",
|
|
238
|
+
description: "Search the bundled standard, meta, and project management schema configurations " +
|
|
239
|
+
"(references \"std\", \"meta\", and \"pm\"), the npm " +
|
|
240
|
+
"registry (the packages carrying the keyword \"specbook-schema\"), and GitHub (the repositories " +
|
|
241
|
+
"carrying the same topic) for the shareable schema configurations (reusable building blocks for " +
|
|
242
|
+
"the \"config\" option) matching a term, and return the hits as a JSON array of objects with the " +
|
|
243
|
+
"fields \"ref\" (the remote reference to use), \"name\", \"version\", \"description\", and " +
|
|
244
|
+
"\"source\" (\"builtin\", \"npm\", or \"github\").",
|
|
245
|
+
inputSchema: {
|
|
246
|
+
term: z.string().optional().describe("the words a matching schema configuration has to carry " +
|
|
247
|
+
"(default: none, matching all of them)"),
|
|
248
|
+
fetch,
|
|
249
|
+
cwd
|
|
250
|
+
}
|
|
251
|
+
}, async (args) => {
|
|
252
|
+
try {
|
|
253
|
+
const hits = await specbook.search(args);
|
|
254
|
+
return { content: [{ type: "text", text: JSON.stringify(hits, null, 4) }] };
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
return errorResult(err);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
176
260
|
await server.connect(new StdioServerTransport());
|
|
177
261
|
};
|
|
@@ -2,6 +2,7 @@ import { type Spec, type SpecObject, type SpecProperty } from "./specbook-format
|
|
|
2
2
|
import { type Schema, type SchemaObject } from "./specbook-format-schema.js";
|
|
3
3
|
import { type ParseContext } from "./specbook-parse-common.js";
|
|
4
4
|
export declare const resolveArtifact: (config: Schema, object: SpecObject) => SchemaObject | undefined;
|
|
5
|
+
export declare const isRecurring: (schema: SchemaObject) => boolean;
|
|
5
6
|
export declare const collectSchemas: (specification: Spec, config: Schema) => Map<SpecObject, SchemaObject>;
|
|
6
7
|
export declare const collectParenProps: (schemas: Map<SpecObject, SchemaObject>) => Map<SpecObject, SpecProperty>;
|
|
7
8
|
export declare const validate: (ctx: ParseContext, specification: Spec, config: Schema) => 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, assignId } from "./specbook-link.js";
|
|
6
|
+
import { referenceRegex, resolveUnique, resolveSet, chainOf, plainText, referenceText, assignId } from "./specbook-link.js";
|
|
7
7
|
import { compileValueExpr, splitItems, anchored } from "./specbook-parse-value.js";
|
|
8
8
|
import { referencedCoverage } from "./specbook-coverage.js";
|
|
9
9
|
/* the syntax of a value consisting of exactly one Wiki-style reference */
|
|
@@ -233,10 +233,17 @@ const validateObject = (ctx, object, schema, level) => {
|
|
|
233
233
|
};
|
|
234
234
|
/* resolve a level 1 object onto its configured artifact, deliberately
|
|
235
235
|
leniently by kind and id first and by case-insensitive name second,
|
|
236
|
-
so deviations of the heading can still be precisely reported
|
|
236
|
+
so deviations of the heading can still be precisely reported, and
|
|
237
|
+
by kind alone third onto a recurring artifact (one configured with
|
|
238
|
+
neither name nor id), which every level 1 object of its kind matches */
|
|
237
239
|
export const resolveArtifact = (config, object) => config.find((s) => (s.kind === object.kind && s.id === object.id) || (s.id !== undefined && `${s.kind}-${s.id}` === object.id)) ??
|
|
238
240
|
config.find((s) => s.name !== undefined
|
|
239
|
-
&& s.name.toUpperCase() === plainText(object.name).toUpperCase())
|
|
241
|
+
&& s.name.toUpperCase() === plainText(object.name).toUpperCase()) ??
|
|
242
|
+
config.find((s) => isRecurring(s) && s.kind === object.kind);
|
|
243
|
+
/* whether a configured artifact is a recurring one: configured with
|
|
244
|
+
neither name nor id, so any number of level 1 objects of its kind
|
|
245
|
+
match it (and none at all, as it is optional by definition) */
|
|
246
|
+
export const isRecurring = (schema) => schema.name === undefined && schema.id === undefined;
|
|
240
247
|
/* map the specification objects onto their schema configuration
|
|
241
248
|
nodes (the artifact resolution is shared with the validation) */
|
|
242
249
|
export const collectSchemas = (specification, config) => {
|
|
@@ -498,7 +505,7 @@ export const validate = (ctx, specification, config) => {
|
|
|
498
505
|
its carrying object), independent of any configuration */
|
|
499
506
|
export const validateReferences = (ctx, specification) => {
|
|
500
507
|
const check = (object, text, file, line) => {
|
|
501
|
-
for (const m of
|
|
508
|
+
for (const m of referenceText(text).matchAll(referenceRegex)) {
|
|
502
509
|
const ref = m[1].trim();
|
|
503
510
|
const resolution = resolveUnique(ctx.linkIndex, ref, object);
|
|
504
511
|
if (resolution.ambiguous)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { type SpecArtifact } from "./specbook-format-spec.js";
|
|
2
2
|
import { type ParseContext, type SourceFile } from "./specbook-parse-common.js";
|
|
3
|
+
export declare const slugify: (text: string) => string;
|
|
3
4
|
export declare const parseFile: (ctx: ParseContext, source: SourceFile) => SpecArtifact[];
|
|
@@ -87,7 +87,7 @@ const parseHeadingText = (raw) => {
|
|
|
87
87
|
};
|
|
88
88
|
};
|
|
89
89
|
/* derive a stable slug identifier from an object name */
|
|
90
|
-
const slugify = (text) => text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
90
|
+
export const slugify = (text) => text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
91
91
|
/* extract the plain text lines of a Markdown list item (excluding any
|
|
92
92
|
nested lists), whose body is a "text" token in a tight list, but a
|
|
93
93
|
"paragraph" token in a loose list (and never "item.text", as this
|
package/dst/specbook-parse.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Spec } from "./specbook-format-spec.js";
|
|
|
2
2
|
import { type Schema } from "./specbook-format-schema.js";
|
|
3
3
|
import { type SourceFile, type ParseResult } from "./specbook-parse-common.js";
|
|
4
4
|
export { type SourceFile, type ParseResult } from "./specbook-parse-common.js";
|
|
5
|
-
export { resolveArtifact, collectSchemas } from "./specbook-parse-semantic.js";
|
|
5
|
+
export { resolveArtifact, isRecurring, collectSchemas } from "./specbook-parse-semantic.js";
|
|
6
6
|
export declare const parseSpecification: (sources: SourceFile[], config?: Schema) => ParseResult;
|
|
7
7
|
export declare const specStatistics: (specification: Spec) => {
|
|
8
8
|
objects: number;
|
package/dst/specbook-parse.js
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as v from "valibot";
|
|
7
7
|
import { Spec } from "./specbook-format-spec.js";
|
|
8
|
-
import { buildLinkIndex, referenceRegex,
|
|
8
|
+
import { buildLinkIndex, referenceRegex, referenceText } 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";
|
|
12
12
|
import { validateDiagrams } from "./specbook-diagram.js";
|
|
13
|
-
export { resolveArtifact, collectSchemas } from "./specbook-parse-semantic.js";
|
|
13
|
+
export { resolveArtifact, isRecurring, collectSchemas } from "./specbook-parse-semantic.js";
|
|
14
14
|
/* parse a set of specification Markdown files into the specification
|
|
15
15
|
AST and optionally validate the result against a configuration */
|
|
16
16
|
export const parseSpecification = (sources, config) => {
|
|
@@ -54,7 +54,7 @@ export const specStatistics = (specification) => {
|
|
|
54
54
|
if (object.description !== undefined)
|
|
55
55
|
texts.push(object.description.description, object.description.rationale ?? "", object.description.elaboration ?? "");
|
|
56
56
|
for (const text of texts)
|
|
57
|
-
links += (
|
|
57
|
+
links += (referenceText(text).match(referenceRegex) ?? []).length;
|
|
58
58
|
object.children.forEach(walk);
|
|
59
59
|
};
|
|
60
60
|
for (const artifact of specification.artifacts)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export declare const projectFile = ".specbook.yaml";
|
|
2
|
+
export declare const isRemote: (value: string) => boolean;
|
|
2
3
|
export interface Project {
|
|
3
4
|
file: string;
|
|
4
5
|
config?: string[];
|
|
5
6
|
basedir?: string;
|
|
6
7
|
}
|
|
7
8
|
export declare const loadProject: (dir: string) => Project | undefined;
|
|
9
|
+
export declare const loadProjectFile: (file: string) => Project;
|
|
8
10
|
export declare const writeProject: (dir: string, config: string[], basedir: string) => string;
|
package/dst/specbook-project.js
CHANGED
|
@@ -11,6 +11,12 @@ import { lineColOfPath } from "./specbook-config.js";
|
|
|
11
11
|
import { renderDiagnostic } from "./specbook-diagnostic.js";
|
|
12
12
|
/* the name of the YAML project configuration file */
|
|
13
13
|
export const projectFile = ".specbook.yaml";
|
|
14
|
+
/* whether a schema configuration pattern is a remote reference
|
|
15
|
+
(resolved by the remote module) instead of a local path: the literals
|
|
16
|
+
"std", "meta", and "pm" (the bundled standard, meta, and project
|
|
17
|
+
management schema configurations), an "npm:" or "github:" reference,
|
|
18
|
+
or an "http:" or "https:" URL */
|
|
19
|
+
export const isRemote = (value) => value === "std" || value === "meta" || value === "pm" || (/^(?:npm|github):|^https?:\/\//).test(value);
|
|
14
20
|
/* the schema of the YAML project configuration: the files or glob
|
|
15
21
|
patterns of the YAML schema configuration and the base directory
|
|
16
22
|
of the specification Markdown files */
|
|
@@ -62,12 +68,16 @@ const readProject = (file) => {
|
|
|
62
68
|
/* find, load, and validate the project configuration, failing on any
|
|
63
69
|
problem, where its relative paths resolve against the directory of
|
|
64
70
|
the file itself (and not against the perhaps deeper directory the
|
|
65
|
-
search started at)
|
|
66
|
-
standard schema configuration */
|
|
71
|
+
search started at) */
|
|
67
72
|
export const loadProject = (dir) => {
|
|
68
73
|
const file = findProject(dir);
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
return file !== undefined ? loadProjectFile(file) : undefined;
|
|
75
|
+
};
|
|
76
|
+
/* load and validate a particular project configuration file, failing
|
|
77
|
+
on any problem, where its relative paths resolve against the
|
|
78
|
+
directory of the file and the remote references (the literal "std"
|
|
79
|
+
included) stay as they are */
|
|
80
|
+
export const loadProjectFile = (file) => {
|
|
71
81
|
/* parse the YAML, reporting its syntax errors as positioned diagnostics */
|
|
72
82
|
const diagnostics = new Array();
|
|
73
83
|
const lines = new LineCounter();
|
|
@@ -100,7 +110,7 @@ export const loadProject = (dir) => {
|
|
|
100
110
|
const patterns = typeof config === "string" ? [config] : config;
|
|
101
111
|
return {
|
|
102
112
|
file,
|
|
103
|
-
config: patterns?.map((pattern) => pattern
|
|
113
|
+
config: patterns?.map((pattern) => isRemote(pattern) ? pattern : path.resolve(base, pattern)),
|
|
104
114
|
basedir: basedir !== undefined ? path.resolve(base, basedir) : undefined
|
|
105
115
|
};
|
|
106
116
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ConfigOverlap } from "./specbook-config.js";
|
|
2
|
+
import { type Verbose } from "./specbook-verbose.js";
|
|
3
|
+
export declare const version: string;
|
|
4
|
+
export declare const standardConfig: string;
|
|
5
|
+
export declare const metaConfig: string;
|
|
6
|
+
export declare const pmConfig: string;
|
|
7
|
+
export declare const isBundled: (file: string) => boolean;
|
|
8
|
+
export declare const cacheDir: string;
|
|
9
|
+
export declare const isCached: (file: string) => boolean;
|
|
10
|
+
export declare const reportOverlaps: (overlaps: ConfigOverlap[], verbose: Verbose) => void;
|
|
11
|
+
export declare const fetchModes: readonly ["never", "auto", "always"];
|
|
12
|
+
export type FetchMode = typeof fetchModes[number];
|
|
13
|
+
export declare const parseFetchMode: (value: string) => FetchMode;
|
|
14
|
+
export interface RemoteOptions {
|
|
15
|
+
fetch: FetchMode;
|
|
16
|
+
cwd: string;
|
|
17
|
+
verbose: Verbose;
|
|
18
|
+
}
|
|
19
|
+
export declare const expandPattern: (pattern: string) => Promise<string[]>;
|
|
20
|
+
export declare const resolveConfig: (ref: string, options: RemoteOptions, chain?: string[]) => Promise<string[]>;
|
|
21
|
+
export interface SearchHit {
|
|
22
|
+
ref: string;
|
|
23
|
+
name: string;
|
|
24
|
+
version?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
source: "builtin" | "npm" | "github";
|
|
27
|
+
}
|
|
28
|
+
export declare const search: (term: string, options: RemoteOptions) => Promise<SearchHit[]>;
|