@markdstage/markdstage 3.0.0 → 3.2.0

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.
@@ -1,17 +1,16 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { parseArchitecture } from "./renderer/architecture.mjs";
4
+ import { architectureSchemaReference } from "./architecture-reference.mjs";
5
+ import {
6
+ UNCLOSED_ARCHITECTURE_MESSAGE,
7
+ validateArchitectureInput,
8
+ } from "./architecture-validation.mjs";
5
9
 
6
10
  const EXT_DIR = dirname(fileURLToPath(import.meta.url));
7
11
  const README_PATH = join(EXT_DIR, "README.md");
8
- const SCHEMA_PATH = join(EXT_DIR, "schema", "architecture-v1.schema.json");
9
12
  const THEME_GUIDE_PATH = join(EXT_DIR, "docs", "custom-theme-authoring.md");
10
13
  const THEME_SCHEMA_PATH = join(EXT_DIR, "schema", "theme-v1.json");
11
- const GUIDE_POINTER =
12
- "Use the markdstage_guide tool to review the format and schemas before using the MarkdStage canvas.";
13
- const PRESENTATION_PROMPT =
14
- /\bpresent(?:ation|er|ing)?\b|\bslides?\b|slides?\.md|\bdeck\b/i;
15
14
 
16
15
  function section(markdown, heading) {
17
16
  const lines = markdown.split(/\r?\n/);
@@ -38,44 +37,6 @@ function section(markdown, heading) {
38
37
  return lines.slice(start, end).join("\n").trim();
39
38
  }
40
39
 
41
- function architectureSchemaSummary(schema) {
42
- const defs = schema.$defs;
43
- const summary = {
44
- root: {
45
- required: schema.required,
46
- properties: Object.keys(schema.properties),
47
- elementTypes: [
48
- defs.nodeBase.properties.type.const,
49
- defs.groupBase.properties.type.const,
50
- defs.connector.properties.type.const,
51
- ],
52
- },
53
- shape: defs.nodeBase.properties.shape.enum,
54
- icon: {
55
- builtIn: defs.iconName.enum,
56
- assetPath: defs.iconAsset.description,
57
- },
58
- connector: {
59
- labelLayer: defs.connector.properties.labelLayer,
60
- },
61
- style: {
62
- keys: Object.keys(defs.style.properties),
63
- colors: defs.color.description,
64
- themeTokens: defs.themeToken.enum,
65
- literalColors: defs.literalColor.description,
66
- },
67
- };
68
- return [
69
- "# Architecture DSL v1 schema summary",
70
- "",
71
- "Key details extracted at runtime from the bundled `schema/architecture-v1.schema.json`.",
72
- "",
73
- "```json",
74
- JSON.stringify(summary, null, 2),
75
- "```",
76
- ].join("\n");
77
- }
78
-
79
40
  function themeSchemaSummary(schema) {
80
41
  return [
81
42
  "# Custom presentation theme v1 schema",
@@ -103,6 +64,7 @@ function themeSchemaSummary(schema) {
103
64
  }
104
65
 
105
66
  export async function readGuide(topic = "overview") {
67
+ if (topic === "architecture-schema") return architectureSchemaReference();
106
68
  const readme = await readFile(README_PATH, "utf8");
107
69
  switch (topic) {
108
70
  case "overview":
@@ -111,6 +73,7 @@ export async function readGuide(topic = "overview") {
111
73
  "",
112
74
  "Users can load workspace Markdown directly with **More controls > Open Markdown** (deterministic splitting without AI; natural-language summarization remains the AI's responsibility). The workspace root is the Git repository root when available, otherwise the folder opened for the current session.",
113
75
  "Use **More controls > Shape editing** to adjust the placement of an existing Architecture diagram. In the CLI, run `markdstage preview slides.md --watch`; it starts in viewing mode and enables the same placement editor plus the detailed Architecture designer. CLI `preview` without `--watch` is read-only. Comprehensive edits affect the source Markdown only when explicitly saved.",
76
+ "Before drafting Architecture DSL, request `architecture-schema` for the generated authoring contract. Before displaying a diagram, call `markdstage_validate` with explicit `format: \"dsl\"` and `source`, or `format: \"slides\"` and one-slide `slides` fragments. This read-only preflight needs no open canvas and does not read or change files.",
114
77
  "",
115
78
  "For details, request `slide-format`, `themes`, `custom-themes`, `theme-schema`, `architecture-dsl`, or `architecture-schema`.",
116
79
  ].join("\n");
@@ -133,71 +96,17 @@ export async function readGuide(topic = "overview") {
133
96
  }
134
97
  case "architecture-dsl":
135
98
  return section(readme, "## Architecture DSL v1");
136
- case "architecture-schema": {
137
- const schema = JSON.parse(await readFile(SCHEMA_PATH, "utf8"));
138
- return architectureSchemaSummary(schema);
139
- }
140
99
  default:
141
100
  throw new Error(`unknown MarkdStage guide topic: ${topic}`);
142
101
  }
143
102
  }
144
103
 
145
- export function createMarkdStageHooks() {
146
- const primed = new Set();
147
- return {
148
- onSessionStart: ({ sessionId }) => {
149
- primed.delete(sessionId);
150
- },
151
- onSessionEnd: ({ sessionId }) => {
152
- primed.delete(sessionId);
153
- },
154
- onUserPromptSubmitted: ({ sessionId, prompt }) => {
155
- if (primed.has(sessionId) || !PRESENTATION_PROMPT.test(prompt ?? "")) return;
156
- primed.add(sessionId);
157
- return { additionalContext: GUIDE_POINTER };
158
- },
159
- onPostToolUse: ({ sessionId, toolName }) => {
160
- if (toolName === "markdstage_guide") primed.add(sessionId);
161
- },
162
- };
163
- }
164
-
165
104
  export function hasFrontMatter(markdown) {
166
105
  const normalized = markdown.replace(/\r\n?/g, "\n").replace(/^[\n \t\uFEFF]+/, "");
167
106
  if (!normalized.startsWith("---\n")) return false;
168
107
  return normalized.split("\n").slice(1).some((line) => line.trim() === "---");
169
108
  }
170
109
 
171
- function architectureSources(markdown) {
172
- const sources = [];
173
- let fence = "";
174
- let lines = [];
175
- for (const line of markdown.split(/\r?\n/)) {
176
- if (!fence) {
177
- const opening = line.match(/^\s*(`{3,}|~{3,})architecture\s*$/i);
178
- if (opening) {
179
- fence = opening[1];
180
- lines = [];
181
- }
182
- continue;
183
- }
184
- const closing = new RegExp(`^${fence[0]}{${fence.length},}\\s*$`);
185
- if (closing.test(line.trim())) {
186
- sources.push(lines.join("\n"));
187
- fence = "";
188
- lines = [];
189
- } else {
190
- lines.push(line);
191
- }
192
- }
193
- return {
194
- sources,
195
- unclosed: Boolean(fence),
196
- unclosedBlockIndex: sources.length,
197
- unclosedSource: fence ? lines.join("\n") : "",
198
- };
199
- }
200
-
201
110
  function architectureError(slideIndex, blockIndex, code, message) {
202
111
  return {
203
112
  slideIndex,
@@ -209,56 +118,59 @@ function architectureError(slideIndex, blockIndex, code, message) {
209
118
  };
210
119
  }
211
120
 
212
- export function architectureValidationErrors(slides, { index } = {}) {
213
- const targets = index === undefined
214
- ? slides.map((slide, slideIndex) => ({ slide, slideIndex }))
215
- : [{ slide: slides[index], slideIndex: index }];
216
- const errors = [];
121
+ export function architectureValidationReport(slides, { index, maxDiagnostics } = {}) {
122
+ if (index !== undefined &&
123
+ (!Number.isInteger(index) || index < 0 || index >= slides.length)) {
124
+ throw new RangeError("index must identify a slide in the provided array.");
125
+ }
126
+ const report = validateArchitectureInput({
127
+ format: "slides",
128
+ slides: index === undefined ? slides : [slides[index]],
129
+ ...(maxDiagnostics === undefined ? {} : { maxDiagnostics }),
130
+ });
131
+ if (index === undefined) return report;
132
+ const rebase = (item) => typeof item.slideIndex === "number"
133
+ ? { ...item, slideIndex: item.slideIndex + index, page: item.page + index }
134
+ : item;
135
+ return {
136
+ ...report,
137
+ scope: "slide",
138
+ index,
139
+ page: index + 1,
140
+ total: slides.length,
141
+ diagnostics: report.diagnostics.map(rebase),
142
+ blocks: report.blocks.map(rebase),
143
+ skipped: report.skipped.map(rebase),
144
+ };
145
+ }
217
146
 
218
- for (const { slide, slideIndex } of targets) {
219
- const architecture = architectureSources(slide);
220
- for (const [blockIndex, source] of architecture.sources.entries()) {
221
- try {
222
- parseArchitecture(source);
223
- } catch (error) {
224
- errors.push(
225
- architectureError(
226
- slideIndex,
227
- blockIndex,
228
- "invalid_architecture",
229
- error?.message || String(error),
230
- ),
231
- );
147
+ export function architectureValidationErrors(slides, { index, validation } = {}) {
148
+ if (Array.isArray(slides) && slides.length === 0 && index === undefined && !validation) return [];
149
+ const report = validation ?? architectureValidationReport(slides, { index });
150
+ const errors = [];
151
+ for (const block of report.blocks) {
152
+ if (!block.dslValid) {
153
+ const primary = report.diagnostics
154
+ .slice(block.diagnosticStart, block.diagnosticStart + block.diagnosticCount)
155
+ .find((diagnostic) => diagnostic.severity === "error");
156
+ if (primary) {
157
+ errors.push(architectureError(
158
+ block.slideIndex, block.blockIndex, "invalid_architecture", primary.message,
159
+ ));
232
160
  }
233
161
  }
234
- if (architecture.unclosed) {
235
- try {
236
- parseArchitecture(architecture.unclosedSource);
237
- } catch (error) {
238
- errors.push(
239
- architectureError(
240
- slideIndex,
241
- architecture.unclosedBlockIndex,
242
- "invalid_architecture",
243
- error?.message || String(error),
244
- ),
245
- );
246
- }
247
- errors.push(
248
- architectureError(
249
- slideIndex,
250
- architecture.unclosedBlockIndex,
251
- "unclosed_architecture_fence",
252
- "The architecture code fence is not closed. Add ``` at the end.",
253
- ),
254
- );
162
+ if (block.closed === false) {
163
+ errors.push(architectureError(
164
+ block.slideIndex, block.blockIndex, "unclosed_architecture_fence",
165
+ UNCLOSED_ARCHITECTURE_MESSAGE,
166
+ ));
255
167
  }
256
168
  }
257
-
258
169
  return errors;
259
170
  }
260
171
 
261
- export function deckValidationFeedback(slides) {
172
+ export function deckValidationFeedback(slides, { validation } = {}) {
173
+ if (Array.isArray(slides) && slides.length === 0 && !validation) return undefined;
262
174
  const warnings = [];
263
175
  slides.forEach((slide, slideIndex) => {
264
176
  if (!hasFrontMatter(slide)) {
@@ -267,7 +179,8 @@ export function deckValidationFeedback(slides) {
267
179
  );
268
180
  }
269
181
  });
270
- for (const error of architectureValidationErrors(slides)) {
182
+ const report = validation ?? architectureValidationReport(slides);
183
+ for (const error of report.diagnostics) {
271
184
  if (error.code === "unclosed_architecture_fence") {
272
185
  warnings.push(
273
186
  `slide ${error.page}: ${error.message}`,
@@ -275,7 +188,14 @@ export function deckValidationFeedback(slides) {
275
188
  continue;
276
189
  }
277
190
  warnings.push(
278
- `slide ${error.page}, architecture ${error.architecture}: ${error.message}. Review architecture-dsl and architecture-schema in markdstage_guide.`,
191
+ `${error.page ? `slide ${error.page}` : "Architecture validation"}${error.architecture ? `, architecture ${error.architecture}` : ""}: ${error.message} [${error.code}${error.pointer ? ` at ${error.pointer}` : ""}]. Review architecture-dsl and architecture-schema in markdstage_guide.`,
192
+ );
193
+ }
194
+ if (!report.complete) {
195
+ warnings.push(
196
+ report.truncated
197
+ ? `Architecture validation is incomplete: inspection limits were reached (${report.budget.limitsReached.join(", ")}). Validate smaller inputs; unchecked blocks are not valid.`
198
+ : "Architecture validation is incomplete: later stages were skipped after earlier errors. Fix the reported issues and validate again.",
279
199
  );
280
200
  }
281
201
  return warnings.length ? `Slide validation feedback:\n- ${warnings.join("\n- ")}` : undefined;