@rse/specbook 0.9.5 → 0.9.6
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 +58 -328
- package/dst/specbook-api.d.ts +13 -20
- package/dst/specbook-api.js +28 -25
- package/dst/specbook-cli.js +34 -50
- package/dst/specbook-cmd-describe.d.ts +5 -2
- package/dst/specbook-cmd-describe.js +32 -91
- package/dst/specbook-cmd-export.d.ts +3 -3
- package/dst/specbook-cmd-export.js +4 -3
- package/dst/specbook-cmd-init.d.ts +2 -2
- package/dst/specbook-cmd-init.js +35 -13
- package/dst/specbook-cmd-lint.d.ts +6 -6
- package/dst/specbook-cmd-lint.js +36 -23
- package/dst/specbook-config.d.ts +2 -2
- package/dst/specbook-config.js +2 -2
- package/dst/specbook-diagram.d.ts +4 -4
- package/dst/specbook-diagram.js +21 -15
- package/dst/specbook-export-ast.d.ts +3 -3
- package/dst/specbook-export-common.d.ts +10 -9
- package/dst/specbook-export-common.js +23 -9
- package/dst/specbook-export-html.css +13 -3
- package/dst/specbook-export-html.d.ts +4 -4
- package/dst/specbook-export-html.js +61 -32
- package/dst/specbook-export-logo-dark.svg +35 -0
- package/dst/specbook-export-logo-light.svg +35 -0
- package/dst/specbook-export-md.d.ts +3 -3
- package/dst/{specbook-struct-schema.d.ts → specbook-format-schema.d.ts} +3 -2
- package/dst/{specbook-struct-schema.js → specbook-format-schema.js} +2 -1
- package/dst/specbook-format-spec.d.ts +31 -0
- package/dst/{specbook-struct-spec.js → specbook-format-spec.js} +10 -10
- package/dst/specbook-format.md +734 -0
- package/dst/specbook-link.d.ts +2 -2
- package/dst/specbook-mcp.d.ts +2 -1
- package/dst/specbook-mcp.js +13 -52
- package/dst/specbook-parse-common.d.ts +5 -3
- package/dst/specbook-parse-common.js +8 -0
- package/dst/specbook-parse-semantic.d.ts +4 -4
- package/dst/specbook-parse-semantic.js +4 -65
- package/dst/specbook-parse-syntax.d.ts +2 -2
- package/dst/specbook-parse-syntax.js +18 -14
- package/dst/specbook-parse.d.ts +4 -4
- package/dst/specbook-parse.js +3 -3
- package/dst/specbook-verbose.d.ts +2 -0
- package/dst/specbook-verbose.js +16 -0
- package/package.json +3 -7
- package/dst/specbook-cmd-edit.d.ts +0 -9
- package/dst/specbook-cmd-edit.js +0 -74
- package/dst/specbook-cmd-import.d.ts +0 -9
- package/dst/specbook-cmd-import.js +0 -78
- package/dst/specbook-export-logo.svg +0 -35
- package/dst/specbook-llm.d.ts +0 -13
- package/dst/specbook-llm.js +0 -51
- package/dst/specbook-struct-spec.d.ts +0 -31
package/dst/specbook-link.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Spec, SpecObject } from "./specbook-format-spec.js";
|
|
2
2
|
export declare const referenceRegex: RegExp;
|
|
3
3
|
interface LinkNode {
|
|
4
4
|
object: SpecObject;
|
|
@@ -9,7 +9,7 @@ export interface LinkTarget {
|
|
|
9
9
|
target?: SpecObject;
|
|
10
10
|
ambiguous: boolean;
|
|
11
11
|
}
|
|
12
|
-
export declare const buildLinkIndex: (specification:
|
|
12
|
+
export declare const buildLinkIndex: (specification: Spec) => LinkIndex;
|
|
13
13
|
export declare const resolveSet: (index: LinkIndex, reference: string) => SpecObject[];
|
|
14
14
|
export declare const resolveUnique: (index: LinkIndex, reference: string) => LinkTarget;
|
|
15
15
|
export declare const anchorPaths: (index: LinkIndex) => Map<SpecObject, string>;
|
package/dst/specbook-mcp.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { type VerboseSink } from "./specbook-api.js";
|
|
2
|
+
export declare const serveMcp: (verbose: VerboseSink) => Promise<void>;
|
package/dst/specbook-mcp.js
CHANGED
|
@@ -43,10 +43,10 @@ export const serveMcp = async (verbose) => {
|
|
|
43
43
|
});
|
|
44
44
|
server.registerTool("specbook_lint", {
|
|
45
45
|
title: "Lint Specification",
|
|
46
|
-
description: "Lint the specification Markdown files
|
|
47
|
-
"
|
|
46
|
+
description: "Lint the specification Markdown files the YAML schema configuration references " +
|
|
47
|
+
"below the base directory against this configuration and return all diagnostics.",
|
|
48
48
|
inputSchema: {
|
|
49
|
-
config: z.string().
|
|
49
|
+
config: z.string().describe("YAML schema configuration file"),
|
|
50
50
|
basedir: z.string().optional().describe("base directory of the specification Markdown files (default: \".\")")
|
|
51
51
|
}
|
|
52
52
|
}, async (args) => {
|
|
@@ -63,11 +63,12 @@ export const serveMcp = async (verbose) => {
|
|
|
63
63
|
});
|
|
64
64
|
server.registerTool("specbook_export", {
|
|
65
65
|
title: "Export Specification",
|
|
66
|
-
description: "Export the specification Markdown files
|
|
66
|
+
description: "Export the specification Markdown files the YAML schema configuration references " +
|
|
67
|
+
"below the base directory as JSON, JSON5, " +
|
|
67
68
|
"YAML, TOON, HTML, PDF, or normalized Markdown. The result is written to the output file " +
|
|
68
69
|
"if an output path is given, else it is returned directly (PDF as a base64-encoded resource).",
|
|
69
70
|
inputSchema: {
|
|
70
|
-
config: z.string().
|
|
71
|
+
config: z.string().describe("YAML schema configuration file"),
|
|
71
72
|
basedir: z.string().optional().describe("base directory of the specification Markdown files (default: \".\")"),
|
|
72
73
|
format: z.enum(formats).optional().describe("output format (default: json)"),
|
|
73
74
|
output: z.string().optional().describe("output file path")
|
|
@@ -94,10 +95,14 @@ export const serveMcp = async (verbose) => {
|
|
|
94
95
|
});
|
|
95
96
|
server.registerTool("specbook_describe", {
|
|
96
97
|
title: "Describe Specification Format",
|
|
97
|
-
description: "Describe the
|
|
98
|
-
"
|
|
98
|
+
description: "Describe the SpecBook models and formats as a Markdown document. If the YAML " +
|
|
99
|
+
"schema configuration or the base directory is given, the description additionally points " +
|
|
100
|
+
"to the artifacts of that particular project.",
|
|
99
101
|
inputSchema: {
|
|
100
|
-
config: z.string().describe("YAML schema configuration file"),
|
|
102
|
+
config: z.string().optional().describe("YAML schema configuration file"),
|
|
103
|
+
basedir: z.string().optional().describe("base directory of the specification Markdown files"),
|
|
104
|
+
embed: z.boolean().optional().describe("embed the YAML schema configuration instead of " +
|
|
105
|
+
"just referencing it (default: false)"),
|
|
101
106
|
output: z.string().optional().describe("output file path (default: return description)")
|
|
102
107
|
}
|
|
103
108
|
}, async (args) => {
|
|
@@ -113,49 +118,5 @@ export const serveMcp = async (verbose) => {
|
|
|
113
118
|
return errorResult(err);
|
|
114
119
|
}
|
|
115
120
|
});
|
|
116
|
-
server.registerTool("specbook_import", {
|
|
117
|
-
title: "Import Foreign Sources",
|
|
118
|
-
description: "Import the information of foreign source files into the configured specification " +
|
|
119
|
-
"artifact files below the base directory, using an LLM.",
|
|
120
|
-
inputSchema: {
|
|
121
|
-
config: z.string().describe("YAML schema configuration file"),
|
|
122
|
-
basedir: z.string().optional().describe("base directory of the specification Markdown files (default: \".\")"),
|
|
123
|
-
inputs: z.array(z.string()).describe("foreign source files to import"),
|
|
124
|
-
provider: z.string().optional().describe("AI provider (\"anthropic\", \"openai\", \"openrouter\" or \"ollama\")"),
|
|
125
|
-
model: z.string().optional().describe("AI model")
|
|
126
|
-
}
|
|
127
|
-
}, async (args) => {
|
|
128
|
-
try {
|
|
129
|
-
const written = await specbook.import(args);
|
|
130
|
-
return { content: [{ type: "text", text: written.length > 0 ?
|
|
131
|
-
`imported into artifact file(s): ${written.join(", ")}` :
|
|
132
|
-
"no artifact files were changed" }] };
|
|
133
|
-
}
|
|
134
|
-
catch (err) {
|
|
135
|
-
return errorResult(err);
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
server.registerTool("specbook_edit", {
|
|
139
|
-
title: "Edit Specification",
|
|
140
|
-
description: "Apply a free-text edit request to the configured specification artifact files " +
|
|
141
|
-
"below the base directory, using an LLM.",
|
|
142
|
-
inputSchema: {
|
|
143
|
-
config: z.string().describe("YAML schema configuration file"),
|
|
144
|
-
basedir: z.string().optional().describe("base directory of the specification Markdown files (default: \".\")"),
|
|
145
|
-
query: z.string().describe("free-text edit request"),
|
|
146
|
-
provider: z.string().optional().describe("AI provider (\"anthropic\", \"openai\", \"openrouter\" or \"ollama\")"),
|
|
147
|
-
model: z.string().optional().describe("AI model")
|
|
148
|
-
}
|
|
149
|
-
}, async (args) => {
|
|
150
|
-
try {
|
|
151
|
-
const written = await specbook.edit(args);
|
|
152
|
-
return { content: [{ type: "text", text: written.length > 0 ?
|
|
153
|
-
`edited artifact file(s): ${written.join(", ")}` :
|
|
154
|
-
"no artifact files were changed" }] };
|
|
155
|
-
}
|
|
156
|
-
catch (err) {
|
|
157
|
-
return errorResult(err);
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
121
|
await server.connect(new StdioServerTransport());
|
|
161
122
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Spec, type SpecObject, type SpecProperty } from "./specbook-format-spec.js";
|
|
2
2
|
import { type Diagnostic } from "./specbook-diagnostic.js";
|
|
3
3
|
import { type LinkIndex } from "./specbook-link.js";
|
|
4
4
|
export interface SourceFile {
|
|
@@ -6,7 +6,7 @@ export interface SourceFile {
|
|
|
6
6
|
text: string;
|
|
7
7
|
}
|
|
8
8
|
export interface ParseResult {
|
|
9
|
-
specification:
|
|
9
|
+
specification: Spec;
|
|
10
10
|
diagnostics: Diagnostic[];
|
|
11
11
|
}
|
|
12
12
|
export interface ObjectMeta {
|
|
@@ -16,11 +16,13 @@ export interface ObjectMeta {
|
|
|
16
16
|
export declare class ParseContext {
|
|
17
17
|
diagnostics: Diagnostic[];
|
|
18
18
|
objectMeta: WeakMap<SpecObject, ObjectMeta>;
|
|
19
|
-
propMeta: WeakMap<
|
|
19
|
+
propMeta: WeakMap<SpecProperty, {
|
|
20
20
|
line: number;
|
|
21
21
|
}>;
|
|
22
22
|
linkIndex: LinkIndex;
|
|
23
23
|
diagnose(file: string, line: number, message: string): void;
|
|
24
24
|
}
|
|
25
25
|
export declare const embeddingRegex: RegExp;
|
|
26
|
+
export declare const embeddingThemes: string[];
|
|
27
|
+
export declare const embeddingVariants: (reference: string) => string[];
|
|
26
28
|
export declare const embeddingMimeType: (reference: string) => string | undefined;
|
|
@@ -16,6 +16,14 @@ export class ParseContext {
|
|
|
16
16
|
}
|
|
17
17
|
/* the Markdown image embedding syntax ("") */
|
|
18
18
|
export const embeddingRegex = /!\[([^\]]*)\]\(([^()]+)\)/g;
|
|
19
|
+
/* the theme variants of a theme-aware image embedding */
|
|
20
|
+
export const embeddingThemes = ["light", "dark"];
|
|
21
|
+
/* expand an image embedding reference into its variants: a reference
|
|
22
|
+
carrying the "{theme}" placeholder yields one variant per theme,
|
|
23
|
+
while every other reference stays its own single variant */
|
|
24
|
+
export const embeddingVariants = (reference) => reference.includes("{theme}") ?
|
|
25
|
+
embeddingThemes.map((theme) => reference.replace(/\{theme\}/g, theme)) :
|
|
26
|
+
[reference];
|
|
19
27
|
/* the embeddable file types and their MIME types */
|
|
20
28
|
const embeddingTypes = {
|
|
21
29
|
svg: "image/svg+xml",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type
|
|
1
|
+
import { type Spec } from "./specbook-format-spec.js";
|
|
2
|
+
import { type Schema } from "./specbook-format-schema.js";
|
|
3
3
|
import { ParseContext } from "./specbook-parse-common.js";
|
|
4
|
-
export declare const validate: (ctx: ParseContext, specification:
|
|
5
|
-
export declare const validateReferences: (ctx: ParseContext, specification:
|
|
4
|
+
export declare const validate: (ctx: ParseContext, specification: Spec, config: Schema) => void;
|
|
5
|
+
export declare const validateReferences: (ctx: ParseContext, specification: Spec) => void;
|
|
@@ -5,21 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { referenceRegex, resolveUnique, resolveSet } from "./specbook-link.js";
|
|
7
7
|
import { compileValueExpr, splitItems } from "./specbook-parse-value.js";
|
|
8
|
-
/* strip a trailing parenthesized annotation from a
|
|
9
|
-
property key (e.g. "WHEN (Context)") */
|
|
10
|
-
const plainKey = (key) => key.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
|
11
8
|
/* strip the inline code markup of a name or property value
|
|
12
9
|
(preserved in the AST for rendering) for matching purposes */
|
|
13
10
|
const plainText = (text) => text.replace(/`/g, "");
|
|
14
11
|
/* compile a configured pattern into an anchored regular expression */
|
|
15
12
|
const anchored = (pattern, flags = "") => new RegExp(`^(?:${pattern})$`, flags);
|
|
16
|
-
/* match a single inline token against a value expression
|
|
17
|
-
(case-insensitively, as inline tokens are decorations) */
|
|
18
|
-
const tokenMatches = (expr, token) => expr.kind === "regex" ?
|
|
19
|
-
anchored(expr.source, "i").test(token) :
|
|
20
|
-
expr.kind === "enum" ?
|
|
21
|
-
expr.members.some((member) => member.toLowerCase() === token.toLowerCase()) :
|
|
22
|
-
false;
|
|
23
13
|
/* match a full property value directly against a value expression
|
|
24
14
|
(defined for the regex and enum kinds only) */
|
|
25
15
|
const directMatches = (expr, text) => expr.kind === "regex" ?
|
|
@@ -51,56 +41,7 @@ const matchAlternatives = (ctx, alternatives, item) => {
|
|
|
51
41
|
return false;
|
|
52
42
|
};
|
|
53
43
|
/* find a property of an object by its key (case-sensitive) */
|
|
54
|
-
const findProp = (object, name) => object.properties.find((p) =>
|
|
55
|
-
/* distribute the inline tokens split off the multi-token property values
|
|
56
|
-
of an object across its configured properties and report the
|
|
57
|
-
unassignable ones (the properties assigned a token are returned) */
|
|
58
|
-
const assignInlineTokens = (ctx, object, props, meta) => {
|
|
59
|
-
const tokens = new Array();
|
|
60
|
-
const tokenAssigned = new Set();
|
|
61
|
-
/* explode a multi-token property value whose direct pattern check
|
|
62
|
-
fails: keep the token matching the own pattern and distribute
|
|
63
|
-
the remaining tokens across the sibling properties */
|
|
64
|
-
for (const prop of props) {
|
|
65
|
-
if (prop.value === undefined)
|
|
66
|
-
continue;
|
|
67
|
-
const expr = compileValueExpr(prop.value);
|
|
68
|
-
if (expr.kind !== "regex" && expr.kind !== "enum")
|
|
69
|
-
continue;
|
|
70
|
-
const match = findProp(object, prop.name);
|
|
71
|
-
if (match === undefined || directMatches(expr, plainText(match.value)))
|
|
72
|
-
continue;
|
|
73
|
-
const parts = plainText(match.value).split(/\s+/);
|
|
74
|
-
if (parts.length < 2)
|
|
75
|
-
continue;
|
|
76
|
-
const idx = parts.findIndex((part) => tokenMatches(expr, part));
|
|
77
|
-
if (idx < 0)
|
|
78
|
-
continue;
|
|
79
|
-
match.value = parts[idx];
|
|
80
|
-
tokenAssigned.add(prop.name);
|
|
81
|
-
parts.splice(idx, 1);
|
|
82
|
-
tokens.push(...parts);
|
|
83
|
-
}
|
|
84
|
-
/* assign the collected tokens to the still unset configured
|
|
85
|
-
properties whose pattern they match */
|
|
86
|
-
for (const prop of props) {
|
|
87
|
-
if (prop.value === undefined || findProp(object, prop.name) !== undefined)
|
|
88
|
-
continue;
|
|
89
|
-
const expr = compileValueExpr(prop.value);
|
|
90
|
-
if (expr.kind !== "regex" && expr.kind !== "enum")
|
|
91
|
-
continue;
|
|
92
|
-
const idx = tokens.findIndex((token) => tokenMatches(expr, token));
|
|
93
|
-
if (idx >= 0) {
|
|
94
|
-
object.properties.push({ key: prop.name, value: tokens[idx] });
|
|
95
|
-
tokenAssigned.add(prop.name);
|
|
96
|
-
tokens.splice(idx, 1);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
/* report the tokens which no configured property accepts */
|
|
100
|
-
for (const token of tokens)
|
|
101
|
-
ctx.diagnose(meta.file, meta.line, `unassignable inline token "${token}" on ${object.kind} "${object.name}"`);
|
|
102
|
-
return tokenAssigned;
|
|
103
|
-
};
|
|
44
|
+
const findProp = (object, name) => object.properties.find((p) => p.key === name);
|
|
104
45
|
/* check a property value against the compiled value expression of its
|
|
105
46
|
configured schema property */
|
|
106
47
|
const checkPropValue = (ctx, prop, expr, property, meta) => {
|
|
@@ -152,8 +93,6 @@ const validateObject = (ctx, object, schema, level) => {
|
|
|
152
93
|
input, via either "{{<id>}}" or "(<id>)" */
|
|
153
94
|
if (schema.id !== undefined && (object.anchor ?? object.paren) !== schema.id)
|
|
154
95
|
ctx.diagnose(meta.file, meta.line, `configured id "${schema.id}" not explicitly specified on ${object.kind} "${object.name}"`);
|
|
155
|
-
/* distribute the inline tokens split off the multi-token property values */
|
|
156
|
-
const tokenAssigned = assignInlineTokens(ctx, object, props, meta);
|
|
157
96
|
/* check the configured properties */
|
|
158
97
|
let parenConsumed = false;
|
|
159
98
|
for (const prop of props) {
|
|
@@ -170,12 +109,12 @@ const validateObject = (ctx, object, schema, level) => {
|
|
|
170
109
|
if (prop.optional !== true)
|
|
171
110
|
ctx.diagnose(meta.file, meta.line, `required property "${prop.name}" missing on ${object.kind} "${object.name}"`);
|
|
172
111
|
}
|
|
173
|
-
else if (expr !== undefined
|
|
112
|
+
else if (expr !== undefined)
|
|
174
113
|
checkPropValue(ctx, prop, expr, match, { file: meta.file, line: ctx.propMeta.get(match)?.line ?? meta.line });
|
|
175
114
|
}
|
|
176
115
|
/* report the properties not configured by the schema */
|
|
177
116
|
for (const property of object.properties)
|
|
178
|
-
if (!props.some((p) => p.name ===
|
|
117
|
+
if (!props.some((p) => p.name === property.key))
|
|
179
118
|
ctx.diagnose(meta.file, ctx.propMeta.get(property)?.line ?? meta.line, `unknown property "${property.key}" on ${object.kind} "${object.name}"`);
|
|
180
119
|
/* a parenthesized token not consumed as a property value acts as
|
|
181
120
|
the implicit anchor (an explicit "{{...}}" anchor takes precedence) */
|
|
@@ -206,7 +145,7 @@ const validateObject = (ctx, object, schema, level) => {
|
|
|
206
145
|
};
|
|
207
146
|
object.childs.sort((a, b) => kindPos(a.kind) - kindPos(b.kind));
|
|
208
147
|
const propPos = (key) => {
|
|
209
|
-
const i = props.findIndex((p) => p.name ===
|
|
148
|
+
const i = props.findIndex((p) => p.name === key);
|
|
210
149
|
return i >= 0 ? i : props.length;
|
|
211
150
|
};
|
|
212
151
|
object.properties.sort((a, b) => propPos(a.key) - propPos(b.key));
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type SpecArtifact } from "./specbook-format-spec.js";
|
|
2
2
|
import { ParseContext, type SourceFile } from "./specbook-parse-common.js";
|
|
3
|
-
export declare const parseFile: (ctx: ParseContext, source: SourceFile) =>
|
|
3
|
+
export declare const parseFile: (ctx: ParseContext, source: SourceFile) => SpecArtifact[];
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { marked } from "marked";
|
|
9
|
-
import { embeddingRegex, embeddingMimeType } from "./specbook-parse-common.js";
|
|
9
|
+
import { embeddingRegex, embeddingMimeType, embeddingVariants } from "./specbook-parse-common.js";
|
|
10
10
|
/* split a description text into statement and optional rationale */
|
|
11
11
|
const splitDescription = (text) => {
|
|
12
12
|
const m = text.match(/,\s*(?:\*\*BECAUSE\*\*|BECAUSE)\s+/);
|
|
@@ -101,7 +101,9 @@ const parseFrontmatter = (text) => {
|
|
|
101
101
|
};
|
|
102
102
|
/* recursively load the image files embedded via "" into the
|
|
103
103
|
description and the property values of an object (SVG as-is, PNG/JPEG
|
|
104
|
-
as base64 data: URLs), resolving the references relative to the source
|
|
104
|
+
as base64 data: URLs), resolving the references relative to the source
|
|
105
|
+
file and expanding a "{theme}" reference into its theme variants,
|
|
106
|
+
which are loaded into consecutive embedding entries */
|
|
105
107
|
const embed = (ctx, object, file) => {
|
|
106
108
|
const load = (target, text, line) => {
|
|
107
109
|
for (const m of text.matchAll(embeddingRegex)) {
|
|
@@ -109,15 +111,17 @@ const embed = (ctx, object, file) => {
|
|
|
109
111
|
const type = embeddingMimeType(reference);
|
|
110
112
|
if (type === undefined)
|
|
111
113
|
continue;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
(
|
|
114
|
+
for (const variant of embeddingVariants(reference)) {
|
|
115
|
+
try {
|
|
116
|
+
const data = fs.readFileSync(path.resolve(path.dirname(file), variant));
|
|
117
|
+
target.embedding ??= [];
|
|
118
|
+
target.embedding.push(type === "image/svg+xml" ?
|
|
119
|
+
data.toString("utf8") : `data:${type};base64,${data.toString("base64")}`);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
ctx.diagnose(file, line, `unreadable embedding file "${variant}": ` +
|
|
123
|
+
(err instanceof Error ? err.message : String(err)));
|
|
124
|
+
}
|
|
121
125
|
}
|
|
122
126
|
}
|
|
123
127
|
};
|
|
@@ -283,13 +287,13 @@ export const parseFile = (ctx, source) => {
|
|
|
283
287
|
if (heading.malformed !== undefined)
|
|
284
288
|
ctx.diagnose(source.file, line, `malformed anchor "${heading.malformed}" in heading`);
|
|
285
289
|
if (heading.name === "" && depth > 1) {
|
|
286
|
-
/* a grouping container heading (e.g. "###
|
|
287
|
-
objects of its
|
|
290
|
+
/* a grouping container heading (e.g. "### STATE") collects
|
|
291
|
+
objects of its kind below the parent object */
|
|
288
292
|
const parent = stack[depth - 2];
|
|
289
293
|
if (parent === undefined)
|
|
290
294
|
ctx.diagnose(source.file, line, `heading level ${depth} without parent object`);
|
|
291
295
|
else {
|
|
292
|
-
group = { parent, kind: heading.kind
|
|
296
|
+
group = { parent, kind: heading.kind };
|
|
293
297
|
current = parent;
|
|
294
298
|
}
|
|
295
299
|
}
|
package/dst/specbook-parse.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Schema } from "./specbook-format-schema.js";
|
|
2
2
|
import { type SourceFile, type ParseResult } from "./specbook-parse-common.js";
|
|
3
|
-
export { type SourceFile, type ParseResult, embeddingRegex, embeddingMimeType } from "./specbook-parse-common.js";
|
|
3
|
+
export { type SourceFile, type ParseResult, embeddingRegex, embeddingMimeType, embeddingThemes, embeddingVariants } from "./specbook-parse-common.js";
|
|
4
4
|
export declare class Parser {
|
|
5
5
|
private ctx;
|
|
6
|
-
parse(sources: SourceFile[], config?:
|
|
6
|
+
parse(sources: SourceFile[], config?: Schema): ParseResult;
|
|
7
7
|
}
|
|
8
|
-
export declare const parseSpecification: (sources: SourceFile[], config?:
|
|
8
|
+
export declare const parseSpecification: (sources: SourceFile[], config?: Schema) => ParseResult;
|
package/dst/specbook-parse.js
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
6
|
import * as v from "valibot";
|
|
7
|
-
import {
|
|
7
|
+
import { Spec } from "./specbook-format-spec.js";
|
|
8
8
|
import { buildLinkIndex } 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
13
|
/* re-export the public parsing types and embedding helpers */
|
|
14
|
-
export { embeddingRegex, embeddingMimeType } from "./specbook-parse-common.js";
|
|
14
|
+
export { embeddingRegex, embeddingMimeType, embeddingThemes, embeddingVariants } from "./specbook-parse-common.js";
|
|
15
15
|
/* parser for a set of specification Markdown files */
|
|
16
16
|
export class Parser {
|
|
17
17
|
ctx = new ParseContext();
|
|
@@ -33,7 +33,7 @@ export class Parser {
|
|
|
33
33
|
validateReferences(this.ctx, specification);
|
|
34
34
|
if (config !== undefined)
|
|
35
35
|
validateDiagrams(this.ctx, specification, config);
|
|
36
|
-
const result = v.safeParse(
|
|
36
|
+
const result = v.safeParse(Spec, specification);
|
|
37
37
|
if (!result.success)
|
|
38
38
|
for (const issue of result.issues) {
|
|
39
39
|
const path = (issue.path ?? []).map((item) => String(item.key)).join(".");
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Specification Book (SpecBook)
|
|
3
|
+
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
/* the sentinel characters delimiting a marked literal value, taken
|
|
7
|
+
from the Unicode private use area, so they never clash with the
|
|
8
|
+
regular content of a verbose message */
|
|
9
|
+
const begin = "\uE000";
|
|
10
|
+
const end = "\uE001";
|
|
11
|
+
/* mark a literal value (path, number, format, ...) inside a verbose
|
|
12
|
+
message, so the output sink can style it without having to guess */
|
|
13
|
+
export const literal = (value) => `${begin}${value}${end}`;
|
|
14
|
+
/* render a verbose message by either styling its marked literal values
|
|
15
|
+
with the given styler or just stripping their markers away */
|
|
16
|
+
export const renderVerbose = (msg, style) => msg.replace(/\uE000([^\uE001]*)\uE001/g, (_, value) => style !== undefined ? style(value) : value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rse/specbook",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
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.5.0",
|
|
26
26
|
"@rse/mrcs": "0.9.2",
|
|
27
|
-
"@rse/gradia": "1.
|
|
27
|
+
"@rse/gradia": "1.1.4",
|
|
28
28
|
"@toon-format/toon": "4.1.1",
|
|
29
29
|
"valibot": "1.4.2",
|
|
30
30
|
"nunjucks": "3.2.4",
|
|
@@ -33,13 +33,9 @@
|
|
|
33
33
|
"@swc/html": "1.16.1",
|
|
34
34
|
"tokenizr": "2.2.1",
|
|
35
35
|
"playwright": "1.62.1",
|
|
36
|
-
"ai": "7.0.69",
|
|
37
|
-
"@ai-sdk/anthropic": "4.0.39",
|
|
38
|
-
"@ai-sdk/openai": "4.0.43",
|
|
39
|
-
"@openrouter/ai-sdk-provider": "3.0.0",
|
|
40
|
-
"ollama-ai-provider-v2": "4.0.1",
|
|
41
36
|
"source-code-error": "1.2.0",
|
|
42
37
|
"commander": "15.0.0",
|
|
38
|
+
"chalk": "6.0.0",
|
|
43
39
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
44
40
|
"zod": "4.4.3"
|
|
45
41
|
},
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { type SchemaSpecification } from "./specbook-struct-schema.js";
|
|
2
|
-
import { type AiOptions } from "./specbook-llm.js";
|
|
3
|
-
export interface EditOptions extends AiOptions {
|
|
4
|
-
config: SchemaSpecification;
|
|
5
|
-
basedir?: string;
|
|
6
|
-
query: string;
|
|
7
|
-
verbose: (msg: string) => void;
|
|
8
|
-
}
|
|
9
|
-
export declare const editSpecification: (options: EditOptions) => Promise<string[]>;
|
package/dst/specbook-cmd-edit.js
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
** Specification Book (SpecBook)
|
|
3
|
-
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
-
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
-
*/
|
|
6
|
-
import * as fs from "node:fs";
|
|
7
|
-
import * as path from "node:path";
|
|
8
|
-
import textframe from "textframe";
|
|
9
|
-
import { describeConfiguration } from "./specbook-cmd-describe.js";
|
|
10
|
-
import { complete, timestamp, renderFileBlock, parseFileBlocks } from "./specbook-llm.js";
|
|
11
|
-
/* the LLM instruction for the edit command */
|
|
12
|
-
const instruction = (description, now) => textframe `
|
|
13
|
-
You are the edit engine of SpecBook, a Markdown-based specification
|
|
14
|
-
format. Apply the given edit request to the Markdown specification
|
|
15
|
-
artifact files of the project. Strictly honor the following rules:
|
|
16
|
-
|
|
17
|
-
- Surgical changes: change only what the edit request actually
|
|
18
|
-
demands; do not rewrite unrelated parts of an artifact.
|
|
19
|
-
- No fabrication: never invent content the edit request does not
|
|
20
|
-
support; leave gaps open instead of guessing.
|
|
21
|
-
- Format conformance: strictly follow the specification format
|
|
22
|
-
described below.
|
|
23
|
-
- Markdown generation: MIRROR the format each existing object already
|
|
24
|
-
uses (keep Complex Format objects complex and Concise Format
|
|
25
|
-
objects concise); for entirely new objects emit the Complex Format
|
|
26
|
-
on levels 1-3 ("#"/"##"/"###") and the Concise Format on level 4
|
|
27
|
-
and deeper.
|
|
28
|
-
- Timestamps: refresh the "Modified:" frontmatter of every changed
|
|
29
|
-
file to "${now}"; keep "Created:" untouched.
|
|
30
|
-
- Files: update ONLY the configured artifact files listed in the
|
|
31
|
-
format description below -- never any other file.
|
|
32
|
-
- Output: return ONLY the actually changed files, each as a block of
|
|
33
|
-
the exact form below, and no other text at all:
|
|
34
|
-
|
|
35
|
-
<<<FILE: <name>>>>
|
|
36
|
-
<content>
|
|
37
|
-
<<<END-FILE>>>
|
|
38
|
-
|
|
39
|
-
${description}
|
|
40
|
-
`;
|
|
41
|
-
/* apply an edit request to the specification artifact files */
|
|
42
|
-
export const editSpecification = async (options) => {
|
|
43
|
-
const basedir = options.basedir ?? ".";
|
|
44
|
-
const files = options.config.map((artifact) => artifact.file).filter((file) => file !== undefined);
|
|
45
|
-
/* ensure a non-empty edit request was given */
|
|
46
|
-
if (options.query.trim() === "")
|
|
47
|
-
throw new Error("no edit request given");
|
|
48
|
-
/* gather the existing artifact files */
|
|
49
|
-
const existing = files
|
|
50
|
-
.filter((file) => fs.existsSync(path.join(basedir, file)))
|
|
51
|
-
.map((file) => renderFileBlock(file, fs.readFileSync(path.join(basedir, file), "utf8")));
|
|
52
|
-
if (existing.length === 0)
|
|
53
|
-
throw new Error(`no configured artifact files found in "${basedir}"`);
|
|
54
|
-
/* let the LLM apply the edit request */
|
|
55
|
-
options.verbose(`editing artifact files in "${basedir}"`);
|
|
56
|
-
const system = instruction(describeConfiguration(options.config), timestamp());
|
|
57
|
-
const prompt = "EXISTING ARTIFACT FILES:\n\n" + existing.join("\n\n") +
|
|
58
|
-
"\n\nEDIT REQUEST:\n\n" + options.query;
|
|
59
|
-
const response = await complete(options, system, prompt);
|
|
60
|
-
/* write back all returned artifact files */
|
|
61
|
-
const written = new Array();
|
|
62
|
-
for (const block of parseFileBlocks(response)) {
|
|
63
|
-
if (!files.includes(block.name)) {
|
|
64
|
-
options.verbose(`ignoring unconfigured file "${block.name}"`);
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
const file = path.join(basedir, block.name);
|
|
68
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
69
|
-
fs.writeFileSync(file, block.content, "utf8");
|
|
70
|
-
options.verbose(`wrote artifact file "${block.name}"`);
|
|
71
|
-
written.push(block.name);
|
|
72
|
-
}
|
|
73
|
-
return written;
|
|
74
|
-
};
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { type SchemaSpecification } from "./specbook-struct-schema.js";
|
|
2
|
-
import { type AiOptions } from "./specbook-llm.js";
|
|
3
|
-
export interface ImportOptions extends AiOptions {
|
|
4
|
-
config: SchemaSpecification;
|
|
5
|
-
basedir?: string;
|
|
6
|
-
inputs: string[];
|
|
7
|
-
verbose: (msg: string) => void;
|
|
8
|
-
}
|
|
9
|
-
export declare const importSpecification: (options: ImportOptions) => Promise<string[]>;
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
** Specification Book (SpecBook)
|
|
3
|
-
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
-
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
-
*/
|
|
6
|
-
import * as fs from "node:fs";
|
|
7
|
-
import * as path from "node:path";
|
|
8
|
-
import textframe from "textframe";
|
|
9
|
-
import { describeConfiguration } from "./specbook-cmd-describe.js";
|
|
10
|
-
import { complete, timestamp, renderFileBlock, parseFileBlocks } from "./specbook-llm.js";
|
|
11
|
-
/* the LLM instruction for the import command */
|
|
12
|
-
const instruction = (description, now) => textframe `
|
|
13
|
-
You are the import engine of SpecBook, a Markdown-based specification
|
|
14
|
-
format. Import the information of the given foreign sources into the
|
|
15
|
-
Markdown specification artifact files of the project, generating or
|
|
16
|
-
updating the artifact files so they faithfully reflect the imported
|
|
17
|
-
information. Strictly honor the following rules:
|
|
18
|
-
|
|
19
|
-
- Faithful reflection: the artifacts must reflect the imported
|
|
20
|
-
sources exactly -- no more, no less.
|
|
21
|
-
- No fabrication: never invent content the sources do not support;
|
|
22
|
-
leave gaps open instead of guessing.
|
|
23
|
-
- Surgical updates: when updating an existing artifact, change only
|
|
24
|
-
what the imported information actually requires.
|
|
25
|
-
- Level-appropriate translation: re-express the imported facts at the
|
|
26
|
-
artifact's level of abstraction; do not copy verbatim.
|
|
27
|
-
- Format conformance: strictly follow the specification format
|
|
28
|
-
described below.
|
|
29
|
-
- Markdown generation: emit the Complex Format on levels 1-3
|
|
30
|
-
("#"/"##"/"###") and the Concise Format on level 4 and deeper.
|
|
31
|
-
- Timestamps: use "${now}" for the "Created:" and "Modified:"
|
|
32
|
-
frontmatter of generated files, and refresh "Modified:" to
|
|
33
|
-
"${now}" on updated files.
|
|
34
|
-
- Files: generate or update ONLY the configured artifact files listed
|
|
35
|
-
in the format description below -- never any other file.
|
|
36
|
-
- Output: return ONLY the generated or updated files, each as a block
|
|
37
|
-
of the exact form below, and no other text at all:
|
|
38
|
-
|
|
39
|
-
<<<FILE: <name>>>>
|
|
40
|
-
<content>
|
|
41
|
-
<<<END-FILE>>>
|
|
42
|
-
|
|
43
|
-
${description}
|
|
44
|
-
`;
|
|
45
|
-
/* import foreign sources into the specification artifact files */
|
|
46
|
-
export const importSpecification = async (options) => {
|
|
47
|
-
const basedir = options.basedir ?? ".";
|
|
48
|
-
const files = options.config.map((artifact) => artifact.file).filter((file) => file !== undefined);
|
|
49
|
-
/* ensure at least one foreign source was given */
|
|
50
|
-
if (options.inputs.length === 0)
|
|
51
|
-
throw new Error("no foreign sources given to import");
|
|
52
|
-
/* gather the existing artifact files and the foreign sources */
|
|
53
|
-
const existing = files
|
|
54
|
-
.filter((file) => fs.existsSync(path.join(basedir, file)))
|
|
55
|
-
.map((file) => renderFileBlock(file, fs.readFileSync(path.join(basedir, file), "utf8")));
|
|
56
|
-
const sources = options.inputs
|
|
57
|
-
.map((input) => renderFileBlock(input, fs.readFileSync(input, "utf8")));
|
|
58
|
-
/* let the LLM generate or update the artifact files */
|
|
59
|
-
options.verbose(`importing ${options.inputs.length} foreign source(s) into "${basedir}"`);
|
|
60
|
-
const system = instruction(describeConfiguration(options.config), timestamp());
|
|
61
|
-
const prompt = "EXISTING ARTIFACT FILES:\n\n" + (existing.length > 0 ? existing.join("\n\n") : "(none)") +
|
|
62
|
-
"\n\nFOREIGN SOURCES TO IMPORT:\n\n" + sources.join("\n\n");
|
|
63
|
-
const response = await complete(options, system, prompt);
|
|
64
|
-
/* write back all returned artifact files */
|
|
65
|
-
const written = new Array();
|
|
66
|
-
for (const block of parseFileBlocks(response)) {
|
|
67
|
-
if (!files.includes(block.name)) {
|
|
68
|
-
options.verbose(`ignoring unconfigured file "${block.name}"`);
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
const file = path.join(basedir, block.name);
|
|
72
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
73
|
-
fs.writeFileSync(file, block.content, "utf8");
|
|
74
|
-
options.verbose(`wrote artifact file "${block.name}"`);
|
|
75
|
-
written.push(block.name);
|
|
76
|
-
}
|
|
77
|
-
return written;
|
|
78
|
-
};
|