@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.
Files changed (52) hide show
  1. package/README.md +58 -328
  2. package/dst/specbook-api.d.ts +13 -20
  3. package/dst/specbook-api.js +28 -25
  4. package/dst/specbook-cli.js +34 -50
  5. package/dst/specbook-cmd-describe.d.ts +5 -2
  6. package/dst/specbook-cmd-describe.js +32 -91
  7. package/dst/specbook-cmd-export.d.ts +3 -3
  8. package/dst/specbook-cmd-export.js +4 -3
  9. package/dst/specbook-cmd-init.d.ts +2 -2
  10. package/dst/specbook-cmd-init.js +35 -13
  11. package/dst/specbook-cmd-lint.d.ts +6 -6
  12. package/dst/specbook-cmd-lint.js +36 -23
  13. package/dst/specbook-config.d.ts +2 -2
  14. package/dst/specbook-config.js +2 -2
  15. package/dst/specbook-diagram.d.ts +4 -4
  16. package/dst/specbook-diagram.js +21 -15
  17. package/dst/specbook-export-ast.d.ts +3 -3
  18. package/dst/specbook-export-common.d.ts +10 -9
  19. package/dst/specbook-export-common.js +23 -9
  20. package/dst/specbook-export-html.css +13 -3
  21. package/dst/specbook-export-html.d.ts +4 -4
  22. package/dst/specbook-export-html.js +61 -32
  23. package/dst/specbook-export-logo-dark.svg +35 -0
  24. package/dst/specbook-export-logo-light.svg +35 -0
  25. package/dst/specbook-export-md.d.ts +3 -3
  26. package/dst/{specbook-struct-schema.d.ts → specbook-format-schema.d.ts} +3 -2
  27. package/dst/{specbook-struct-schema.js → specbook-format-schema.js} +2 -1
  28. package/dst/specbook-format-spec.d.ts +31 -0
  29. package/dst/{specbook-struct-spec.js → specbook-format-spec.js} +10 -10
  30. package/dst/specbook-format.md +734 -0
  31. package/dst/specbook-link.d.ts +2 -2
  32. package/dst/specbook-mcp.d.ts +2 -1
  33. package/dst/specbook-mcp.js +13 -52
  34. package/dst/specbook-parse-common.d.ts +5 -3
  35. package/dst/specbook-parse-common.js +8 -0
  36. package/dst/specbook-parse-semantic.d.ts +4 -4
  37. package/dst/specbook-parse-semantic.js +4 -65
  38. package/dst/specbook-parse-syntax.d.ts +2 -2
  39. package/dst/specbook-parse-syntax.js +18 -14
  40. package/dst/specbook-parse.d.ts +4 -4
  41. package/dst/specbook-parse.js +3 -3
  42. package/dst/specbook-verbose.d.ts +2 -0
  43. package/dst/specbook-verbose.js +16 -0
  44. package/package.json +3 -7
  45. package/dst/specbook-cmd-edit.d.ts +0 -9
  46. package/dst/specbook-cmd-edit.js +0 -74
  47. package/dst/specbook-cmd-import.d.ts +0 -9
  48. package/dst/specbook-cmd-import.js +0 -78
  49. package/dst/specbook-export-logo.svg +0 -35
  50. package/dst/specbook-llm.d.ts +0 -13
  51. package/dst/specbook-llm.js +0 -51
  52. package/dst/specbook-struct-spec.d.ts +0 -31
@@ -1,4 +1,4 @@
1
- import type { Specification, Object as SpecObject } from "./specbook-struct-spec.js";
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: Specification) => LinkIndex;
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>;
@@ -1 +1,2 @@
1
- export declare const serveMcp: (verbose: (msg: string) => void) => Promise<void>;
1
+ import { type VerboseSink } from "./specbook-api.js";
2
+ export declare const serveMcp: (verbose: VerboseSink) => Promise<void>;
@@ -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 below the base directory against the " +
47
- "optional YAML schema configuration and return all diagnostics.",
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().optional().describe("YAML schema configuration file"),
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 below the base directory as JSON, JSON5, " +
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().optional().describe("YAML schema configuration file"),
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 specification format configured by the YAML schema configuration " +
98
- "as a Markdown document.",
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 Specification, type Object as SpecObject, type Property } from "./specbook-struct-spec.js";
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: 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<Property, {
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 ("![alt](file)") */
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 Specification } from "./specbook-struct-spec.js";
2
- import { type SchemaSpecification } from "./specbook-struct-schema.js";
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: Specification, config: SchemaSpecification) => void;
5
- export declare const validateReferences: (ctx: ParseContext, specification: Specification) => void;
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) => plainKey(p.key) === name);
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 && !tokenAssigned.has(prop.name))
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 === plainKey(property.key)))
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 === plainKey(key));
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 Artifact } from "./specbook-struct-spec.js";
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) => Artifact[];
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 "![alt](file)" 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 file */
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
- try {
113
- const data = fs.readFileSync(path.resolve(path.dirname(file), reference));
114
- target.embedding ??= [];
115
- target.embedding.push(type === "image/svg+xml" ?
116
- data.toString("utf8") : `data:${type};base64,${data.toString("base64")}`);
117
- }
118
- catch (err) {
119
- ctx.diagnose(file, line, `unreadable embedding file "${reference}": ` +
120
- (err instanceof Error ? err.message : String(err)));
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. "### STATES") collects
287
- objects of its singular kind below the parent object */
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.replace(/S$/, "") };
296
+ group = { parent, kind: heading.kind };
293
297
  current = parent;
294
298
  }
295
299
  }
@@ -1,8 +1,8 @@
1
- import { type SchemaSpecification } from "./specbook-struct-schema.js";
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?: SchemaSpecification): ParseResult;
6
+ parse(sources: SourceFile[], config?: Schema): ParseResult;
7
7
  }
8
- export declare const parseSpecification: (sources: SourceFile[], config?: SchemaSpecification) => ParseResult;
8
+ export declare const parseSpecification: (sources: SourceFile[], config?: Schema) => ParseResult;
@@ -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 { Specification } from "./specbook-struct-spec.js";
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(Specification, specification);
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,2 @@
1
+ export declare const literal: (value: string | number) => string;
2
+ export declare const renderVerbose: (msg: string, style?: (value: string) => string) => string;
@@ -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.5",
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.0.0",
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[]>;
@@ -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
- };