@rse/specbook 1.4.0 → 1.4.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/README.md CHANGED
@@ -324,6 +324,10 @@ validated (an unknown entry or a wrongly typed value fails the command),
324
324
  and both an explicitly supplied option and its environment variable
325
325
  win over its entry.
326
326
 
327
+ `specbook init` creates such a file in the current working directory
328
+ (unless one already applies), so the options of the initialization
329
+ stick for all subsequent commands.
330
+
327
331
  The default value of every CLI option `--xxx` can be overridden
328
332
  by a corresponding `SPECBOOK_XXX` environment variable (e.g.
329
333
  `SPECBOOK_BASEDIR`, `SPECBOOK_CONFIG`, `SPECBOOK_OUTPUT`,
@@ -28,6 +28,10 @@ export interface ProjectOptions {
28
28
  basedir?: string;
29
29
  cwd?: string;
30
30
  }
31
+ export interface InitResult {
32
+ files: string[];
33
+ project?: string;
34
+ }
31
35
  export interface ExportRequest extends ProjectOptions {
32
36
  formats?: ExportFormat[];
33
37
  realtime?: boolean;
@@ -46,7 +50,7 @@ export declare class SpecBook {
46
50
  private project;
47
51
  private configFiles;
48
52
  private requireConfig;
49
- init(options: ProjectOptions): Promise<string[]>;
53
+ init(options: ProjectOptions): Promise<InitResult>;
50
54
  lint(options: ProjectOptions & {
51
55
  gitignore?: boolean;
52
56
  }): Promise<LintResult>;
@@ -8,7 +8,7 @@ import * as path from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { glob } from "glob";
10
10
  import { loadConfig, configStatistics } from "./specbook-config.js";
11
- import { loadProject, projectFile } from "./specbook-project.js";
11
+ import { loadProject, writeProject, projectFile } from "./specbook-project.js";
12
12
  import { renderDiagnostic, renderDiagnosticVerbose } from "./specbook-diagnostic.js";
13
13
  import { initSpecification } from "./specbook-cmd-init.js";
14
14
  import { lint } from "./specbook-cmd-lint.js";
@@ -62,6 +62,7 @@ export class SpecBook {
62
62
  const given = (value) => options.cwd !== undefined ? path.resolve(cwd, value) : value;
63
63
  const derived = (value) => options.cwd !== undefined ? value : path.relative(cwd, value) || ".";
64
64
  return {
65
+ file: project?.file,
65
66
  config: config !== undefined && config.length > 0 ?
66
67
  config.map((pattern) => pattern === "std" ? pattern : given(pattern)) :
67
68
  project?.config?.map((pattern) => pattern === "std" ? pattern : derived(pattern)),
@@ -105,13 +106,24 @@ export class SpecBook {
105
106
  `and ${literal(stats.links)} link relationship(s)`);
106
107
  return config;
107
108
  }
108
- /* initialize the configured specification artifact files
109
- below the base directory */
109
+ /* initialize the configured specification artifact files below the
110
+ base directory and persist the effective project options into a
111
+ new project configuration file in the working directory, unless
112
+ one already applies (as a new one would shadow it) */
110
113
  async init(options) {
111
114
  const verbose = this.verboseOf("init");
112
115
  const project = this.project(options, verbose);
113
- return initSpecification({ config: this.requireConfig(await this.configFiles(project.config), verbose),
116
+ const files = initSpecification({ config: this.requireConfig(await this.configFiles(project.config), verbose),
114
117
  basedir: project.basedir ?? ".", verbose });
118
+ if (project.file !== undefined) {
119
+ verbose(`skipping existing project configuration "${literal(project.file)}"`);
120
+ return { files };
121
+ }
122
+ const cwd = path.resolve(options.cwd ?? ".");
123
+ const relative = (value) => path.relative(cwd, path.resolve(cwd, value)) || ".";
124
+ const file = writeProject(cwd, project.config?.map((pattern) => pattern === "std" ? pattern : relative(pattern)) ?? ["std"], relative(project.basedir ?? "."));
125
+ verbose(`created project configuration "${literal(file)}"`);
126
+ return { files, project: options.cwd !== undefined ? file : path.relative(process.cwd(), file) };
115
127
  }
116
128
  /* lint the specification Markdown files below the base directory,
117
129
  where "gitignore" treats an artifact file Git excludes from its
@@ -130,15 +130,18 @@ withVerboseOption(program.command("mcp"))
130
130
  verboseOf(opts)("mcp", "starting MCP server on stdio", "notice");
131
131
  await serveMcp(verboseOf(opts, "mcp"));
132
132
  });
133
- /* the init command creates the configured artifact files */
133
+ /* the init command creates the configured artifact files and,
134
+ unless one already applies, the project configuration file */
134
135
  withCommonOptions(program.command("init"))
135
136
  .description("initialize the configured specification artifact files below the base directory")
136
137
  .action(async (opts) => {
137
138
  const specbook = new SpecBook({ verbose: verboseOf(opts) });
138
- const created = await specbook.init({ config: configOf(opts), basedir: opts.basedir });
139
- await writeStdout(created.length > 0 ?
140
- `initialized artifact file(s): ${created.join(", ")}\n` :
141
- "no artifact files were created\n");
139
+ const result = await specbook.init({ config: configOf(opts), basedir: opts.basedir });
140
+ await writeStdout((result.files.length > 0 ?
141
+ `initialized artifact file(s): ${result.files.join(", ")}\n` :
142
+ "no artifact files were created\n") +
143
+ (result.project !== undefined ?
144
+ `created project configuration file: ${result.project}\n` : ""));
142
145
  });
143
146
  /* the lint command reports all diagnostics and fails on any error */
144
147
  withGitignoreOption(withCommonOptions(program.command("lint")))
@@ -7,8 +7,22 @@ import * as fs from "node:fs";
7
7
  import * as path from "node:path";
8
8
  import { literal } from "./specbook-verbose.js";
9
9
  import { formatTimestamp } from "./specbook-export-md.js";
10
+ import { titleProperties } from "./specbook-export-common.js";
11
+ /* render the property list of the title artifact, seeding the properties
12
+ the exports interpret themselves with their rendering defaults (as far
13
+ as the schema configures them, as any other is reported as unknown) */
14
+ const titleList = (artifact) => {
15
+ if (artifact.kind !== "META" || artifact.name?.toUpperCase() !== "TITLE")
16
+ return "";
17
+ const props = titleProperties.filter((prop) => artifact.props?.some((p) => p.name === prop.name) === true);
18
+ if (props.length === 0)
19
+ return "";
20
+ const width = Math.max(...props.map((prop) => prop.name.length)) + 2;
21
+ return props.map((prop) => `- ${`${prop.name}:`.padEnd(width)}${prop.init}`.trimEnd() + "\n").join("") + "\n";
22
+ };
10
23
  /* initialize the configured specification artifact files below the
11
- base directory with their frontmatter and artifact headings, where
24
+ base directory with their frontmatter and artifact headings (plus
25
+ the title properties), where
12
26
  all artifacts configured onto the same file reside in it side by
13
27
  side, following each other on level 1 */
14
28
  export const initSpecification = (options) => {
@@ -38,7 +52,7 @@ export const initSpecification = (options) => {
38
52
  const headings = artifacts.map((artifact) => {
39
53
  const name = artifact.name ?? "";
40
54
  const paren = artifact.id !== undefined ? ` (${artifact.id})` : "";
41
- return `# ${artifact.kind}: ${name}${paren}\n\n`;
55
+ return `# ${artifact.kind}: ${name}${paren}\n\n` + titleList(artifact);
42
56
  }).join("");
43
57
  const text = "---\n" +
44
58
  `Created: ${now}\n` +
@@ -344,14 +344,10 @@ const deriveCenter = (ctx, object, diagram, nodes, nodeSet, errors) => {
344
344
  return undefined;
345
345
  }
346
346
  centerUrl = `#${anchors.get(source) ?? source.id}`;
347
- if (centerCfg.property !== undefined) {
348
- const value = propValue(parenProps, source, centerCfg.property);
349
- if (value === undefined) {
350
- errors.push({ reason: `diagram "center" source lacks the property "${centerCfg.property}"` });
351
- return undefined;
352
- }
347
+ const value = centerCfg.property !== undefined ?
348
+ propValue(parenProps, source, centerCfg.property) : undefined;
349
+ if (value !== undefined)
353
350
  label = plainText(expandReferences(value, (ref) => resolveUnique(index, ref, source).target?.name ?? ref));
354
- }
355
351
  else if (label === undefined)
356
352
  label = plainText(source.name);
357
353
  }
@@ -31,6 +31,10 @@ export declare const paperSizeDefault = "A4";
31
31
  export declare const paperSetup: (paper: string) => PaperSetup;
32
32
  export declare const paperLength: (setup: PaperSetup, value: number) => string;
33
33
  export declare const documentPaperSize: (specification: Spec) => string;
34
+ export declare const titleProperties: {
35
+ name: string;
36
+ init: string;
37
+ }[];
34
38
  export declare const paperStylesheet: (paper: string) => string;
35
39
  export declare const charsetCodepoints: (charset: string) => number[] | undefined;
36
40
  export declare const subsetStylesheet: (charset?: string) => Promise<string>;
@@ -6,6 +6,7 @@
6
6
  import fs from "node:fs";
7
7
  import { embeddingThemes, darkMark } from "./specbook-parse-common.js";
8
8
  import { plainText } from "./specbook-link.js";
9
+ import { themeToneDefault } from "./specbook-theme.js";
9
10
  /* escape a text for embedding into template HTML (text and attributes) */
10
11
  export const escapeHtml = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
11
12
  .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -111,6 +112,21 @@ export const documentPaperSize = (specification) => {
111
112
  `(expected ${paperSizes.join(", ")})`);
112
113
  return paper;
113
114
  };
115
+ /* the properties of the title object the exports interpret themselves,
116
+ each with the value "init" seeds it with (its rendering default, an
117
+ empty value counting as an absent property) */
118
+ export const titleProperties = [
119
+ { name: "LOGO", init: "" },
120
+ { name: "TITLE", init: "Specification" },
121
+ { name: "SUBTITLE", init: "" },
122
+ { name: "AUTHOR", init: "" },
123
+ { name: "VERSION", init: "" },
124
+ { name: "LANG", init: "en" },
125
+ { name: "CHARSET", init: "UTF-8" },
126
+ { name: "THEME-STYLE", init: "Light" },
127
+ { name: "THEME-TONE", init: themeToneDefault },
128
+ { name: "PAPER-SIZE", init: paperSizeDefault }
129
+ ];
114
130
  /* the vertical room (in rem) the introducing heading of a diagram
115
131
  claims above it on the same page: without this reserve a maximally
116
132
  sized diagram could not share its page with the heading, which
@@ -8,7 +8,7 @@ import { markedSmartypants } from "marked-smartypants";
8
8
  import { resolveUnique, expandReferences } from "./specbook-link.js";
9
9
  import { compileValueExpr, splitItems } from "./specbook-parse-value.js";
10
10
  import { embeddingRegex, embeddingCount, embeddingThemes, darkMark } from "./specbook-parse-common.js";
11
- import { escapeHtml, fallbackLogo, imageDark, isTitleObject } from "./specbook-export-common.js";
11
+ import { escapeHtml, fallbackLogo, imageDark, isTitleObject, titleProperties } from "./specbook-export-common.js";
12
12
  import { isDocument } from "./specbook-export-image.js";
13
13
  import { embeddedSource, fenceLanguage, diagramKey, omittedDiagram } from "./specbook-export-diagram.js";
14
14
  import { embeddedListing, fencedListing } from "./specbook-export-code.js";
@@ -680,8 +680,7 @@ export const renderTitlePage = (object, created, modified) => {
680
680
  const value = prop(name);
681
681
  return value !== undefined && value.trim() !== "" ? inline(value) : "";
682
682
  };
683
- const rest = object.properties.filter((property) => !["LOGO", "TITLE", "SUBTITLE", "AUTHOR", "VERSION",
684
- "LANG", "CHARSET", "PAPER-SIZE", "THEME-STYLE", "THEME-TONE"].includes(property.key));
683
+ const rest = object.properties.filter((property) => !titleProperties.some((prop) => prop.name === property.key));
685
684
  /* the logo is rendered above the title, from the embedded image content
686
685
  of the LOGO property or, for a non-embeddable reference, as its inline
687
686
  Markdown; without a (non-empty) LOGO property the built-in SpecBook
@@ -323,7 +323,8 @@ type SchemaGradiaConfig = Partial<{
323
323
  - `SchemaDiagram.center?: string | SchemaDiagramCenter`:
324
324
  object a `hub` diagram is projected onto (default: `self`), or a
325
325
  synthetic center node, labeled from a referenced `source` object (or
326
- one of its properties, via `property`) or a literal `label`, typed
326
+ one of its properties, via `property`, falling back onto the `label`
327
+ or the object name while absent) or a literal `label`, typed
327
328
  (under `qualified`) by `kind`, and linked to the `source` object,
328
329
  BECAUSE the focus of a hub is not always a specification object --
329
330
  the solution itself, e.g., sits in the middle of a context diagram
@@ -572,7 +573,8 @@ Instead of naming an existing object, `center` can also declare a
572
573
  *synthetic* center node, which represents something no specification
573
574
  object models -- like the solution itself in the middle of a context
574
575
  diagram. Such a center is labeled from a referenced `source` object (or
575
- one of its properties, via `property`) or a literal `label`, and links
576
+ one of its properties, via `property`, falling back onto the `label` or
577
+ the object name while the property is absent) or a literal `label`, and links
576
578
  to the `source` object. As the nodes carry no `[[xxx]]` reference to a
577
579
  synthetic center, `centerEdges` synthesizes the edges from a direction
578
580
  `property` of the node objects: its `inbound` value maps onto a
@@ -93,7 +93,9 @@
93
93
  #
94
94
  # - All artifacts are optional, so that the schema can back not only
95
95
  # a full specification but also the individual document types of
96
- # the modeling coverage matrix above on their own.
96
+ # the modeling coverage matrix above on their own. Likewise, the
97
+ # properties and child objects of all artifacts are optional, so
98
+ # that a freshly initialized specification already lints cleanly.
97
99
  #
98
100
  # - The traditional hard Software Requirements Specification (SRS)
99
101
  # vs. Architecture Description (AD) document split is
@@ -128,6 +130,7 @@
128
130
  optional: true
129
131
  - name: TITLE
130
132
  desc: The title of the specification.
133
+ optional: true
131
134
  - name: SUBTITLE
132
135
  desc: The subtitle of the specification.
133
136
  optional: true
@@ -182,17 +185,21 @@
182
185
  - name: PURPOSE
183
186
  desc: >
184
187
  Why the solution exists.
188
+ optional: true
185
189
  - name: TARGET-AUDIENCE
186
190
  desc: >
187
191
  Whom the solution serves.
192
+ optional: true
188
193
  - name: SOLUTION
189
194
  desc: >
190
195
  What the solution is, in a single sentence naming its kind or
191
196
  category (e.g. a self-hosted, browser-based streaming platform).
197
+ optional: true
192
198
  - name: VALUE-PROPOSITION
193
199
  desc: >
194
200
  The benefit the solution offers its target audience, stated in
195
201
  absolute terms without reference to alternatives.
202
+ optional: true
196
203
  - name: DIFFERENTIATORS
197
204
  desc: >
198
205
  How the solution stands apart from the alternatives, naming the
@@ -266,6 +273,7 @@
266
273
  enforces itself is a Domain Rule or a Non-Functional Requirement instead) and
267
274
  states a fact or event, not a decision (a deliberate exclusion is the `SCOPE` of
268
275
  the Solution Vision or a `WONT` requirement instead).
276
+ optional: true
269
277
  format:
270
278
  type: compact
271
279
  maxTableColumns: 5
@@ -341,6 +349,7 @@
341
349
  (the `BROADER` term where one exists) and adds the characteristics delimiting the
342
350
  term from its siblings (e.g. `A Message submitted by an Attendee as ...`), so that
343
351
  the hierarchy and the definitions cross-check each other.
352
+ optional: true
344
353
  format:
345
354
  type: compact
346
355
  maxTableColumns: 5
@@ -438,6 +447,7 @@
438
447
  exactly this part and hold exactly the rights the Authorization Model grants to it
439
448
  (plus those of the roles it `INHERITS`), while a persona holding several roles
440
449
  holds the union of them.
450
+ optional: true
441
451
  format:
442
452
  maxTableColumns: 5
443
453
  referenced:
@@ -482,6 +492,7 @@
482
492
  persona's own voice, like a "tagline", "mantra", or "defining statement" -- that
483
493
  captures their core attitude, motivation, frustration, or need in a single memorable
484
494
  line. Non-user stakeholders are modeled as personas of type `Stakeholder`.
495
+ optional: true
485
496
  format:
486
497
  maxTableColumns: 5
487
498
  props:
@@ -573,6 +584,7 @@
573
584
  desc: >
574
585
  The end-to-end journey of a single persona (or of multiple personas sharing the same
575
586
  experience as role variants), named with a 2-5 word summary of the pursued scenario.
587
+ optional: true
576
588
  props:
577
589
  - name: ACTORS
578
590
  desc: >
@@ -660,6 +672,7 @@
660
672
  event-driven (`When <trigger>, the system MUST ...`), state-driven (`While <state>,
661
673
  the system MUST ...`), optional-feature (`Where <feature>, the system MUST ...`), or
662
674
  unwanted-behavior (`If <condition>, then the system MUST ...`).
675
+ optional: true
663
676
  format:
664
677
  maxTableColumns: 6
665
678
  referenced:
@@ -734,6 +747,7 @@
734
747
  them (a `WONT` requirement, which the architecture rightly ignores, is the tolerated
735
748
  exception the lint warning cannot tell apart) and its hub diagram shows what
736
749
  answers it.
750
+ optional: true
737
751
  referenced:
738
752
  - "[[TACTIC:*]]"
739
753
  - "[[DECISION:*]]"
@@ -838,6 +852,7 @@
838
852
  <outcome>` item per case, applied in order with the first matching case winning,
839
853
  and a closing sentence stating the outcome of the remaining cases ("otherwise")
840
854
  and carrying the rationale.
855
+ optional: true
841
856
  format:
842
857
  maxTableColumns: 5
843
858
  referenced:
@@ -950,6 +965,7 @@
950
965
  cases, a use case spans several activities, and an activity performed outside the
951
966
  solution is covered by none at all -- so the workflow is no mere sequence of use
952
967
  cases and cannot be assembled from them.
968
+ optional: true
953
969
  automaton:
954
970
  nodes: ACTIVITY
955
971
  edges: HANDOFF
@@ -1123,6 +1139,7 @@
1123
1139
  relate it n:m to the Domain Workflow activities -- a use case spans several
1124
1140
  activities and an activity is covered by several use cases -- so the two decompose
1125
1141
  the same domain independently instead of nesting into each other.
1142
+ optional: true
1126
1143
  diagram:
1127
1144
  type: hub
1128
1145
  center: self
@@ -1295,6 +1312,7 @@
1295
1312
  A named entity of the data model, carrying a Pascal-cased, 1-3 word long,
1296
1313
  per-artifact unique name, and described by a 1-sentence description ("what") plus a
1297
1314
  1-sentence rationale ("why").
1315
+ optional: true
1298
1316
  diagram:
1299
1317
  type: hub
1300
1318
  center: self
@@ -1462,6 +1480,7 @@
1462
1480
  state machine: exactly one initial state, at least one final state, every state
1463
1481
  reachable from the initial state, and some final state reachable from every state
1464
1482
  (a state from which no final state can be reached anymore is a livelock).
1483
+ optional: true
1465
1484
  automaton:
1466
1485
  nodes: STATE
1467
1486
  edges: TRANSITION
@@ -1625,6 +1644,7 @@
1625
1644
  they are granted, so that the permissions of all roles form the access matrix of
1626
1645
  roles against entities. Any operation, state, or condition not covered by a
1627
1646
  permission of the role (or of a role it `INHERITS`) is forbidden.
1647
+ optional: true
1628
1648
  format:
1629
1649
  type: compact
1630
1650
  maxTableColumns: 8
@@ -1738,6 +1758,7 @@
1738
1758
  belongs to, where the kind-specific parts (HTTP method, GraphQL operation, topic
1739
1759
  direction, command) are carried by the `OPERATION` and `LOCATOR` properties of the
1740
1760
  endpoints only.
1761
+ optional: true
1741
1762
  diagram:
1742
1763
  type: hub
1743
1764
  center: self
@@ -1993,6 +2014,7 @@
1993
2014
  Endpoints form a flat list: a hierarchy of sub-commands or sub-resources
1994
2015
  is expressed by a path-style `LOCATOR` (e.g. `remote add`,
1995
2016
  `/events/{eventId}/chat`) only.
2017
+ optional: true
1996
2018
  diagram:
1997
2019
  type: hub
1998
2020
  center: self
@@ -2226,6 +2248,7 @@
2226
2248
  is a Dialog Pattern instead), decides between design alternatives (the alternative
2227
2249
  it rejects is its `TRADE-OFF`), and is not verifiable by a metric (a measurable
2228
2250
  quality bound is a Non-Functional Requirement instead).
2251
+ optional: true
2229
2252
  referenced:
2230
2253
  - "[[DIALOG-PATTERN:*]]"
2231
2254
  format:
@@ -2325,6 +2348,7 @@
2325
2348
  solution phrases what it says, not what it does (a behavior is an Interaction
2326
2349
  Concept principle instead) and not what a term means (a definition is a Domain
2327
2350
  Glossary term instead).
2351
+ optional: true
2328
2352
  format:
2329
2353
  type: compact
2330
2354
  diagram:
@@ -2410,6 +2434,7 @@
2410
2434
  occurring in a single flow is a Dialog Storyboard frame instead) and is bound to a
2411
2435
  particular kind of screen or flow (a solution-wide rule independent of any screen
2412
2436
  is an Interaction Concept principle instead).
2437
+ optional: true
2413
2438
  diagram:
2414
2439
  type: hub
2415
2440
  center: self
@@ -2500,6 +2525,7 @@
2500
2525
  one flow, as seen by its actors (the intent and the information exchanged are a Use
2501
2526
  Cases scenario instead, and a structure recurring across flows is a Dialog Pattern
2502
2527
  instead).
2528
+ optional: true
2503
2529
  diagram:
2504
2530
  type: hub
2505
2531
  center: self
@@ -2611,6 +2637,7 @@
2611
2637
  `MOCKUP` or a Dialog Storyboard frame instead), and is a decision rather than a
2612
2638
  list of token values (the values are in the design system) or a measurable bound
2613
2639
  (a bound is a Non-Functional Requirement instead).
2640
+ optional: true
2614
2641
  format:
2615
2642
  type: compact
2616
2643
  maxTableColumns: 5
@@ -2670,6 +2697,7 @@
2670
2697
  is a reference rendering of the final look of one screen (a low-fidelity sketch of
2671
2698
  a screen within its flow is a Dialog Storyboard frame instead) and shows rules
2672
2699
  applied rather than stating them (a rule is an `ELEMENT` instead).
2700
+ optional: true
2673
2701
  format:
2674
2702
  type: complex
2675
2703
  diagram:
@@ -2754,6 +2782,7 @@
2754
2782
  solution deploys and operates itself (e.g. a managed database of its own) is a
2755
2783
  Deployment View node instead -- only a party or system the solution merely consumes
2756
2784
  or serves belongs to the system context.
2785
+ optional: true
2757
2786
  format:
2758
2787
  type: compact
2759
2788
  maxTableColumns: 5
@@ -2872,6 +2901,7 @@
2872
2901
  otherwise) and which elaborates how the component fulfills it and collaborates
2873
2902
  with the components it depends on, plus a 1-sentence rationale ("why") for the
2874
2903
  component's existence and boundary.
2904
+ optional: true
2875
2905
  format:
2876
2906
  type: compact
2877
2907
  maxTableColumns: 5
@@ -2993,6 +3023,7 @@
2993
3023
  or a shared module used by client and server), so a product
2994
3024
  used on several tiers is a single component here instead of
2995
3025
  one per tier; referenced by no Deployment View tier.
3026
+ optional: true
2996
3027
  diagram:
2997
3028
  type: hub
2998
3029
  center: self
@@ -3355,6 +3386,7 @@
3355
3386
  addressed by a Quality Perspectives tactic referencing the aspect through
3356
3387
  `AFFECTS`, too, so the hub diagram of an aspect shows the decision and the tactic
3357
3388
  behind it.
3389
+ optional: true
3358
3390
  diagram:
3359
3391
  type: hub
3360
3392
  center: self
@@ -3415,6 +3447,7 @@
3415
3447
  Model interface (e.g. of kind `MQTT` or `WebSocket`), and the platform element
3416
3448
  hosting a managed broker a Deployment View `Managed` node -- only the executing
3417
3449
  structure itself belongs to the Concurrency View.
3450
+ optional: true
3418
3451
  diagram:
3419
3452
  type: hub
3420
3453
  center: self
@@ -3515,6 +3548,7 @@
3515
3548
  `TIER`, a location-level tier (e.g. a data center) groups the tiers referencing it
3516
3549
  via `PART-OF`, and a single deployment unit within a tier is a `NODE` -- only the
3517
3550
  partition itself belongs here.
3551
+ optional: true
3518
3552
  format:
3519
3553
  type: concise
3520
3554
  diagram:
@@ -3594,6 +3628,7 @@
3594
3628
  itself belongs to the Deployment View, with the tier and area the node belongs to
3595
3629
  and the network segments it is attached to as its `TIER`, `AREA`, and `NETWORK`
3596
3630
  siblings.
3631
+ optional: true
3597
3632
  diagram:
3598
3633
  type: hub
3599
3634
  center: self
@@ -3764,6 +3799,7 @@
3764
3799
  deployable artifact is a `Build` aspect of the Development Aspects, and the products
3765
3800
  used are Technology Stack `Operate-Time` components referenced through `TOOLS` --
3766
3801
  only the procedure itself, its trigger, and its responsible operator belong here.
3802
+ optional: true
3767
3803
  diagram:
3768
3804
  type: hub
3769
3805
  center: self
@@ -3885,6 +3921,7 @@
3885
3921
  (e.g. a hosting or budget mandate) protects no quality and is a Design Decisions
3886
3922
  decision alone -- only the mechanism, the quality it achieves, and the elements it
3887
3923
  shapes belong here.
3924
+ optional: true
3888
3925
  format:
3889
3926
  type: compact
3890
3927
  diagram:
@@ -4046,6 +4083,7 @@
4046
4083
  decision is under-justified. The rationale should make the decision feel earned,
4047
4084
  not asserted - if you delete the rationale and the decision suddenly looks
4048
4085
  arbitrary, the rationale was doing its job.
4086
+ optional: true
4049
4087
  format:
4050
4088
  type: complex
4051
4089
  diagram:
@@ -4253,6 +4291,7 @@
4253
4291
  `TOOLS`, and the rule about the source layout, build, or process it lives in is a
4254
4292
  Development Aspects aspect -- only the recurring code shape, the elements written
4255
4293
  that way, and the deviation ruled out belong here.
4294
+ optional: true
4256
4295
  format:
4257
4296
  type: complex
4258
4297
  diagram:
@@ -4380,6 +4419,7 @@
4380
4419
  concurrency unit when its collaboration or contention is exercised, an aspect of
4381
4420
  the Development Aspects or a Design Decisions decision when its rule is checked by
4382
4421
  an architectural fitness function (e.g. no dependency edge against the layering).
4422
+ optional: true
4383
4423
  format:
4384
4424
  type: compact
4385
4425
  diagram:
@@ -46,14 +46,18 @@ export const serveMcp = async (verbose) => {
46
46
  server.registerTool("specbook_init", {
47
47
  title: "Initialize Specification",
48
48
  description: "Initialize the configured specification artifact files below the base directory " +
49
- "with their frontmatter and artifact heading, skipping already existing files.",
49
+ "with their frontmatter and artifact heading, skipping already existing files, and " +
50
+ `create the project configuration file "${projectFile}" in the working directory, ` +
51
+ "persisting the effective schema configuration and base directory, unless one already applies.",
50
52
  inputSchema: { config, basedir, cwd }
51
53
  }, async (args) => {
52
54
  try {
53
- const created = await specbook.init(args);
54
- return { content: [{ type: "text", text: created.length > 0 ?
55
- `initialized artifact file(s): ${created.join(", ")}` :
56
- "no artifact files were created" }] };
55
+ const result = await specbook.init(args);
56
+ return { content: [{ type: "text", text: (result.files.length > 0 ?
57
+ `initialized artifact file(s): ${result.files.join(", ")}` :
58
+ "no artifact files were created") +
59
+ (result.project !== undefined ?
60
+ `\ncreated project configuration file: ${result.project}` : "") }] };
57
61
  }
58
62
  catch (err) {
59
63
  return errorResult(err);
@@ -5,3 +5,4 @@ export interface Project {
5
5
  basedir?: string;
6
6
  }
7
7
  export declare const loadProject: (dir: string) => Project | undefined;
8
+ export declare const writeProject: (dir: string, config: string[], basedir: string) => string;
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import * as fs from "node:fs";
7
7
  import * as path from "node:path";
8
- import { parseDocument, LineCounter } from "yaml";
8
+ import { parseDocument, stringify, LineCounter } from "yaml";
9
9
  import * as v from "valibot";
10
10
  import { lineColOfPath } from "./specbook-config.js";
11
11
  import { renderDiagnostic } from "./specbook-diagnostic.js";
@@ -104,3 +104,12 @@ export const loadProject = (dir) => {
104
104
  basedir: basedir !== undefined ? path.resolve(base, basedir) : undefined
105
105
  };
106
106
  };
107
+ /* write a new project configuration file into the directory, with the
108
+ schema configuration patterns and the base directory given relative
109
+ to it, never overwriting an existing file */
110
+ export const writeProject = (dir, config, basedir) => {
111
+ const file = path.join(dir, projectFile);
112
+ const text = stringify({ config: config.length === 1 ? config[0] : config, basedir });
113
+ fs.writeFileSync(file, text, { encoding: "utf8", flag: "wx" });
114
+ return file;
115
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rse/specbook",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "Markdown-based Specification Format (API, CLI and MCP service)",
6
6
  "license": "Apache-2.0",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "files": [ "dst" ],
19
19
  "dependencies": {
20
- "marked": "18.0.14",
20
+ "marked": "18.1.0",
21
21
  "marked-smartypants": "1.1.12",
22
22
  "yaml": "2.9.1",
23
23
  "json5": "2.2.3",
@@ -32,14 +32,14 @@
32
32
  "shiki": "4.5.0",
33
33
  "pdfjs-dist": "6.4.299",
34
34
  "@napi-rs/canvas": "1.0.10",
35
- "@toon-format/toon": "4.1.1",
35
+ "@toon-format/toon": "4.4.0",
36
36
  "valibot": "1.5.0",
37
37
  "nunjucks": "3.2.4",
38
38
  "@rse/nunjucks-addons": "2.2.0",
39
39
  "textframe": "1.2.1",
40
40
  "@swc/html": "1.16.13",
41
41
  "tokenizr": "2.2.1",
42
- "playwright": "1.63.0",
42
+ "playwright": "1.64.0",
43
43
  "source-code-error": "1.2.0",
44
44
  "commander": "15.0.0",
45
45
  "chalk": "6.0.1",
@@ -49,14 +49,14 @@
49
49
  "es-toolkit": "1.52.0",
50
50
  "fastify": "5.12.5",
51
51
  "@fastify/websocket": "11.3.3",
52
- "@modelcontextprotocol/sdk": "1.32.0",
52
+ "@modelcontextprotocol/sdk": "1.32.1",
53
53
  "zod": "4.6.5"
54
54
  },
55
55
  "devDependencies": {
56
56
  "chokidar-cli": "3.0.0",
57
57
  "eslint": "10.9.0",
58
58
  "@eslint/js": "10.0.1",
59
- "typescript-eslint": "8.71.0",
59
+ "typescript-eslint": "8.71.1",
60
60
  "eslint-plugin-promise": "7.3.0",
61
61
  "neostandard": "0.14.0-next.1",
62
62
  "globals": "17.13.0",
@@ -73,7 +73,7 @@
73
73
  "source-sans": "3.52.0",
74
74
  "@fontsource/dejavu-mono": "5.3.0",
75
75
  "@fontsource/noto-sans-symbols": "5.3.0",
76
- "postcss": "8.5.28",
76
+ "postcss": "8.5.29",
77
77
  "postcss-cli": "12.0.0",
78
78
  "postcss-url": "10.1.4",
79
79
  "stylus": "0.64.0",