@markdstage/markdstage 3.1.0 → 3.3.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.
- package/package.json +1 -1
- package/shared/README.md +61 -9
- package/shared/architecture-editor/editor.js +1 -1
- package/shared/architecture-reference.mjs +150 -0
- package/shared/architecture-validation.mjs +309 -0
- package/shared/markdstage-guide.mjs +62 -142
- package/shared/renderer/architecture-contract.mjs +13707 -0
- package/shared/renderer/architecture-diagnostics.mjs +426 -0
- package/shared/renderer/architecture-scene.mjs +312 -0
- package/shared/renderer/architecture.mjs +221 -217
- package/shared/renderer/index.html +9 -1
- package/shared/renderer/mermaid-scene.mjs +1248 -0
- package/shared/renderer/renderer.js +429 -232
- package/shared/renderer/scene-graph.mjs +739 -0
- package/shared/renderer/scene-pptx.mjs +265 -0
- package/shared/renderer/scene-svg.mjs +321 -0
- package/shared/renderer/slides.css +23 -0
- package/shared/renderer/theme.mjs +54 -0
- package/shared/runtime/architecture-editor-server.mjs +5 -0
- package/shared/runtime/architecture-source.mjs +9 -1
- package/shared/runtime/pptx-package.mjs +4 -3
- package/shared/schema/README.md +60 -5
- package/shared/schema/architecture-contract.mjs +355 -0
- package/shared/scripts/generate-architecture-contract.mjs +41 -0
- package/src/commands/validate.mjs +39 -3
- package/src/runtime.mjs +4 -0
- package/src/skills.mjs +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markdstage/markdstage",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Present, validate, inspect, capture, and export MarkdStage Markdown decks from the command line — no Copilot canvas required.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "runceel",
|
package/shared/README.md
CHANGED
|
@@ -108,6 +108,16 @@ The themed slide is displayed and updates automatically
|
|
|
108
108
|
notes are converted from Markdown to readable plain text in the corresponding
|
|
109
109
|
PowerPoint notes pane. AI may call `export_pptx` with another
|
|
110
110
|
workspace-confined `.pptx` path.
|
|
111
|
+
- **Both export buttons show a visible notification outside More controls.**
|
|
112
|
+
Progress remains visible while saving, and both buttons are disabled until
|
|
113
|
+
the operation finishes. Export buttons return to their normal appearance and
|
|
114
|
+
tooltips after success or failure; the notification carries the result.
|
|
115
|
+
Success appears only after the file has been saved,
|
|
116
|
+
with its file name and selectable save location. It dismisses after eight
|
|
117
|
+
seconds, pausing while hovered or keyboard-focused. Errors remain until
|
|
118
|
+
dismissed or another export starts. Success and error notifications also have
|
|
119
|
+
a dismiss button and are announced to screen readers. Notifications never
|
|
120
|
+
appear in exported files or the external presentation window.
|
|
111
121
|
- Use **More controls > Output preview** to letterbox the current slide inside
|
|
112
122
|
the canvas with
|
|
113
123
|
the same fixed 1280×720 typography, spacing, diagram limits, and clipping used
|
|
@@ -206,13 +216,48 @@ OS file dialog is used.
|
|
|
206
216
|
The extension provides `markdstage_guide`. Before authoring Markdown for the
|
|
207
217
|
MarkdStage canvas, AI should request the required `overview`, `slide-format`,
|
|
208
218
|
`themes`, `custom-themes`, `theme-schema`, `architecture-dsl`, or
|
|
209
|
-
`architecture-schema` topic.
|
|
210
|
-
`
|
|
211
|
-
|
|
219
|
+
`architecture-schema` topic. **Before drafting Architecture DSL, request
|
|
220
|
+
`architecture-schema` first.** It contains the schema-derived element fields,
|
|
221
|
+
fixed versus parent-managed placement requirements, and a self-contained example
|
|
222
|
+
within an 8 KiB UTF-8 response budget. Use `architecture-dsl` for detailed behavior.
|
|
223
|
+
Runtime guidance is generated from this README and the bundled schema contract,
|
|
224
|
+
so user-scoped extension installs expose the same vocabulary.
|
|
212
225
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
226
|
+
Use the standalone **`markdstage_validate`** tool before opening a newly authored
|
|
227
|
+
diagram. Its input is explicitly one of:
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
{ "format": "dsl", "source": "{\"elements\":[]}" }
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{ "format": "slides", "slides": ["## Diagram\n\n```architecture\n{\"elements\":[]}\n```"] }
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Each `slides` entry is one slide fragment, not an entire Markdown deck. The tool
|
|
238
|
+
does not require a canvas and never changes the displayed deck, page, files, or
|
|
239
|
+
editor save state. Optional `maxDiagnostics` is an integer from 1 to 100
|
|
240
|
+
(default 50). Inspection examines at most 200 slide positions and 200 blocks,
|
|
241
|
+
262,144 UTF-16 code units per source/slide input, and 2,097,152 code units across
|
|
242
|
+
slides. These inspection budgets do not replace the existing DSL source and
|
|
243
|
+
element limits. Reports distinguish API `ok`, content `valid`, validation `complete`,
|
|
244
|
+
and `truncated` collection, with per-block stage status and stable-coded
|
|
245
|
+
diagnostics. JSON Pointers identify fields; slide inputs add page/block positions.
|
|
246
|
+
Repair suggestions are contextual and never applied automatically.
|
|
247
|
+
|
|
248
|
+
Fix reported independent problems together, then validate again and pass
|
|
249
|
+
**the same content** to saving and `open_canvas` or `load_deck`. Check
|
|
250
|
+
`inspect_layout` separately afterward. A valid DSL does not guarantee asset
|
|
251
|
+
existence, readable labels, or slide fit. Invalid JSON stops dependent checks in
|
|
252
|
+
that block, not other readable blocks; bounded checks never claim skipped work
|
|
253
|
+
succeeded. The existing canvas still permits invalid diagrams to appear as
|
|
254
|
+
inline errors rather than rejecting an entire load.
|
|
255
|
+
|
|
256
|
+
Authoring guidance is carried by tool/canvas descriptions, the overview, and
|
|
257
|
+
Agent Skills. The extension does not register advisory SDK session hooks:
|
|
258
|
+
some hosts cannot provide a hook processor, and optional guide reminders must
|
|
259
|
+
not prevent tools or canvases from starting. This does not change the host's
|
|
260
|
+
permission handling or file-hook settings.
|
|
216
261
|
|
|
217
262
|
### Markdown file syntax
|
|
218
263
|
|
|
@@ -451,7 +496,8 @@ When writing explicit JSON, `elements` is required by the JSON Schema.
|
|
|
451
496
|
unsupported elements, styles, or colors render an inline diagram error while
|
|
452
497
|
preserving other slide content. DSL values never generate HTML, script, or
|
|
453
498
|
event attributes. Generated asset URLs stay on same-origin `/assets/...`.
|
|
454
|
-
- `version` is currently `1` and defaults to v1. Limits include
|
|
499
|
+
- `version` is currently `1` and defaults to v1. Limits include 65,536 source
|
|
500
|
+
code units (UTF-16),
|
|
455
501
|
200 total elements, 100 connectors, four nesting levels, 12 polyline
|
|
456
502
|
intermediate points, 20,000 total text characters, and 200-character icon/src
|
|
457
503
|
references.
|
|
@@ -996,13 +1042,13 @@ fails with `invalid_input`; pass the complete `slides` array or call
|
|
|
996
1042
|
| `load_deck` | `{ slides: string[], index?: number, theme?: "dark" | "light" | "microsoft" | "custom", sourceName?: string }`. Replace/reload the deck for mid-presentation content or theme changes. `index` defaults to `0`; theme defaults to `dark`. `sourceName` is metadata only and never reads or watches Markdown. Appends one back cover without duplication. Returns `{ ok, version, index, total, theme, validationFeedback? }`. Missing front matter or Architecture errors do not prevent display; remediation is returned in `validationFeedback` and logged for open. |
|
|
997
1043
|
| `goto_slide` | `{ index: number }`. Select a clamped zero-based index. Intended for explicit chat requests, not normal navigation. Returns `{ ok, changed, version, index, total }`. |
|
|
998
1044
|
| `show_slide` | `{ markdown: string }`. Temporarily replace the current slide. Supports front matter keys `deck`, `kicker`, `page`, `total`, `title`, `layout`, `size`, and `theme`. Omitted theme inherits the deck theme. The override is included in output snapshots until navigation or deck replacement resumes the registered deck. |
|
|
999
|
-
| `get_architecture_errors` | `{ index?: number }`. Validate the complete deck or one zero-based slide, including temporary content.
|
|
1045
|
+
| `get_architecture_errors` | `{ index?: number }`. Validate the complete deck or one zero-based slide, including temporary content. Preserves `{ ok, scope, index?, page?, total, errorCount, errors }` and legacy block errors; adds `valid`, `complete`, `truncated`, detailed `diagnostics`, and block-stage results from the same validator as `markdstage_validate`. No deck and out-of-range indexes are errors. |
|
|
1000
1046
|
| `open_presenter` | No input. Start one synchronized movable/resizable 1280×720 Chromium app-mode window. Use `F11` on Windows for full screen. Returns `{ ok, started, alreadyRunning, browser?, pid? }`. |
|
|
1001
1047
|
| `close_presenter` | No input. Stop presenter and remove its temporary profile. Returns `{ ok, stopped }`. |
|
|
1002
1048
|
| `inspect_layout` | `{ index?: number, includeFits?: boolean }`. Render the registered in-memory PDF snapshot with the fixed 1280×720 output layout; this does not inspect the source file on disk. Omit `index` for one preferred whole-deck inspection. Serialize targeted calls because PDF, layout, and PNG jobs are exclusive. By default, return only clipped pages; `includeFits` includes successful pages. Returns dimensions, issue counts, overflow measurements, nested scroll containers, and a bounded list of element hints. Requires Edge, Chrome, or Chromium. |
|
|
1003
1049
|
| `capture_slides` | `{ indexes?: number[], outputDirectory?: string, theme?: "dark" | "light" | "microsoft" | "custom" }`. Generate PDF-equivalent 1280×720 PNGs for at most 10 zero-based indexes. When `indexes` is omitted, inspect the deck and capture only clipped pages. Paths stay inside the workspace; results contain paths and layout summaries, not image bytes. Requires Edge, Chrome, or Chromium. |
|
|
1004
1050
|
| `export_pdf` | `{ outputPath?: string, theme?: "dark" | "light" | "microsoft" | "custom" }`. Export one 16:9 page per slide. Relative paths use workspace root; default is `markdstage.pdf`. Theme affects PDF only. Reject paths outside workspace and non-`.pdf` files. Temporary slide replacement and the automatic back cover are included. Returns `{ ok, path, total, theme, bytes }`. Requires Edge, Chrome, or Chromium. |
|
|
1005
|
-
| `export_pptx` | `{ outputPath?: string, theme?: "dark" | "light" | "microsoft" | "custom" }`. Export a hybrid editable 16:9 PowerPoint deck. Supported text, lists, links, tables, fenced code blocks, raster images, and Architecture DSL objects remain native. Code blocks preserve syntax-colored editable runs, whitespace, monospace typography, and block decoration; unsupported effects such as shadows remain reported background fallbacks. Speaker-note Markdown is exported as readable plain text in the corresponding PowerPoint notes pane. Architecture nodes, groups, and connector-label pills are visible AutoShapes with integrated text; icons are foreground pictures. Mermaid and unsupported visuals become reported background fallbacks. Relative paths use workspace root; default is `markdstage.pptx`. Reject paths outside workspace and non-`.pptx` files. Temporary slide replacement and the automatic back cover are included. Returns `{ ok, path, total, theme, bytes, format, fallbackCount, fallbacks }`. Requires Edge, Chrome, or Chromium. |
|
|
1051
|
+
| `export_pptx` | `{ outputPath?: string, theme?: "dark" | "light" | "microsoft" | "custom" }`. Export a hybrid editable 16:9 PowerPoint deck. Supported text, lists, links, tables, fenced code blocks, raster images, Mermaid diagrams, and Architecture DSL objects remain native. Code blocks preserve syntax-colored editable runs, whitespace, monospace typography, and block decoration; unsupported effects such as shadows remain reported background fallbacks. Speaker-note Markdown is exported as readable plain text in the corresponding PowerPoint notes pane. Architecture nodes, groups, and connector-label pills are visible AutoShapes with integrated text; icons are foreground pictures. Mermaid diagrams export as editable shapes, text, and connectors with unsupported SVG details preserved as per-element fallback pictures. Unsupported visuals become reported background fallbacks. Relative paths use workspace root; default is `markdstage.pptx`. Reject paths outside workspace and non-`.pptx` files. Temporary slide replacement and the automatic back cover are included. Returns `{ ok, path, total, theme, bytes, format, fallbackCount, fallbacks }`. Requires Edge, Chrome, or Chromium. |
|
|
1006
1052
|
| `edit_architecture` | `{ enabled: boolean }`. Toggle placement editing. Imported decks also write to the source fence; direct decks write to canvas state. Presenter/print omit UI. Mode is not persisted and `reset` disables it. Returns `{ ok, enabled, version }`. |
|
|
1007
1053
|
| `reset` | No input. Clear deck/slide state, disable editing, and return to the waiting view. |
|
|
1008
1054
|
|
|
@@ -1037,6 +1083,8 @@ fails with `invalid_input`; pass the complete `slides` array or call
|
|
|
1037
1083
|
.github/extensions/markdstage/
|
|
1038
1084
|
extension.mjs # Canvas declaration, loopback server, and actions
|
|
1039
1085
|
architecture-canvas.mjs # Canvas adapter for the shared Architecture Editor
|
|
1086
|
+
architecture-reference.mjs # Compact schema-derived authoring reference
|
|
1087
|
+
architecture-validation.mjs # Pure unloaded-input and slide validation
|
|
1040
1088
|
architecture-editor/
|
|
1041
1089
|
index.html # Full diagram-editor canvas shell
|
|
1042
1090
|
editor.css # Workspace, tree, and inspector styles
|
|
@@ -1044,6 +1092,7 @@ fails with `invalid_input`; pass the complete `slides` array or call
|
|
|
1044
1092
|
copilot-extension.json # Manifest for Gist sharing
|
|
1045
1093
|
markdown-deck.mjs # Raw Markdown splitting for canvas import
|
|
1046
1094
|
scripts/
|
|
1095
|
+
generate-architecture-contract.mjs # Generate/check browser-safe schema metadata
|
|
1047
1096
|
markdown-blocks.mjs # Scan and replace architecture fences
|
|
1048
1097
|
markdown-files.mjs # Scan workspace Markdown
|
|
1049
1098
|
markdown-watcher.mjs # Watch and debounce source-backed Markdown
|
|
@@ -1054,6 +1103,8 @@ fails with `invalid_input`; pass the complete `slides` array or call
|
|
|
1054
1103
|
slides.css # Built-in dark/light/microsoft themes and navigation UI
|
|
1055
1104
|
renderer.js # Front matter, marked, Mermaid, Architecture, SSE, controls
|
|
1056
1105
|
architecture.mjs # Validate JSON DSL and create safe SVG DOM
|
|
1106
|
+
architecture-contract.mjs # Generated structural vocabulary; no runtime dependencies
|
|
1107
|
+
architecture-diagnostics.mjs # Bounded structured diagnostics and reference checks
|
|
1057
1108
|
architecture-edit.mjs # DOM-independent move/detach/Undo/Redo/serialization
|
|
1058
1109
|
architecture-editor.mjs # Placement UI and Advanced editing entry point
|
|
1059
1110
|
architecture-document.mjs # Full-editor command/session API
|
|
@@ -1062,6 +1113,7 @@ fails with `invalid_input`; pass the complete `slides` array or call
|
|
|
1062
1113
|
architecture-source.mjs # Atomic Architecture block persistence and conflicts
|
|
1063
1114
|
schema/
|
|
1064
1115
|
architecture-v1.schema.json # Architecture DSL v1 JSON Schema (draft 2020-12)
|
|
1116
|
+
architecture-contract.mjs # Derive metadata from Schema references and conditions
|
|
1065
1117
|
README.md # Schema use, versioning, and migration policy
|
|
1066
1118
|
examples/ # Samples with relative $schema references
|
|
1067
1119
|
vendor/
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { architectureContract } from "./renderer/architecture-contract.mjs";
|
|
2
|
+
|
|
3
|
+
const annotations = new Set(["$schema", "$id", "$anchor", "$comment", "title", "description", "examples"]);
|
|
4
|
+
const schemaMaps = new Set(["properties", "patternProperties", "dependentSchemas", "$defs"]);
|
|
5
|
+
const schemaArrays = new Set(["allOf", "anyOf", "oneOf", "prefixItems"]);
|
|
6
|
+
const schemaValues = new Set([
|
|
7
|
+
"items", "contains", "additionalProperties", "unevaluatedProperties", "unevaluatedItems",
|
|
8
|
+
"propertyNames", "not", "if", "then", "else", "contentSchema",
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
function assertions(schema) {
|
|
12
|
+
if (typeof schema === "boolean") return schema;
|
|
13
|
+
return Object.fromEntries(Object.entries(schema)
|
|
14
|
+
.filter(([key]) => !annotations.has(key))
|
|
15
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
16
|
+
.map(([key, value]) => [
|
|
17
|
+
key,
|
|
18
|
+
schemaMaps.has(key)
|
|
19
|
+
? Object.fromEntries(Object.keys(value).sort().map((name) => [name, assertions(value[name])]))
|
|
20
|
+
: schemaArrays.has(key) ? value.map(assertions)
|
|
21
|
+
: schemaValues.has(key) ? assertions(value) : value,
|
|
22
|
+
]));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const minimalExample = {
|
|
26
|
+
version: 1,
|
|
27
|
+
title: "Request path",
|
|
28
|
+
canvas: { width: 1600, height: 900 },
|
|
29
|
+
elements: [
|
|
30
|
+
{ type: "node", id: "client", x: 160, y: 300, width: 280, height: 140, text: "Client" },
|
|
31
|
+
{ type: "node", id: "api", x: 860, y: 300, width: 280, height: 140, text: "API" },
|
|
32
|
+
{ type: "connector", from: "client", to: "api", label: "HTTPS", arrow: true },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A complete, bounded authoring reference, not a second list of permitted fields.
|
|
38
|
+
* Names and constraints come from the generated contract; prose explains runtime semantics.
|
|
39
|
+
*/
|
|
40
|
+
export function architectureSchemaReference(contract = architectureContract) {
|
|
41
|
+
const aliases = new Map();
|
|
42
|
+
const used = new Set();
|
|
43
|
+
for (const name of Object.keys(contract.definitions).sort((a, b) => a.length - b.length || (a < b ? -1 : 1))) {
|
|
44
|
+
const signature = JSON.stringify(assertions(contract.definitions[name]));
|
|
45
|
+
if (!aliases.has(signature)) aliases.set(signature, name);
|
|
46
|
+
}
|
|
47
|
+
function describe(schema, defining) {
|
|
48
|
+
if (typeof schema === "boolean") return schema ? "any" : "never";
|
|
49
|
+
const alias = aliases.get(JSON.stringify(assertions(schema)));
|
|
50
|
+
if (alias && alias !== defining) {
|
|
51
|
+
used.add(alias);
|
|
52
|
+
return alias;
|
|
53
|
+
}
|
|
54
|
+
if (schema.$ref) {
|
|
55
|
+
const name = schema.$ref.slice("#/$defs/".length).replace(/~1/g, "/").replace(/~0/g, "~");
|
|
56
|
+
const definition = contract.definitions[name];
|
|
57
|
+
if (!definition) throw new Error(`Unresolved reference in Architecture reference: ${schema.$ref}`);
|
|
58
|
+
if (definition.properties?.type?.enum?.every((type) => contract.elements[type])) return "element";
|
|
59
|
+
used.add(name);
|
|
60
|
+
return name;
|
|
61
|
+
}
|
|
62
|
+
const parts = [];
|
|
63
|
+
const rendered = new Set();
|
|
64
|
+
if (Object.hasOwn(schema, "const")) {
|
|
65
|
+
parts.push(JSON.stringify(schema.const));
|
|
66
|
+
rendered.add("const");
|
|
67
|
+
} else if (schema.enum) {
|
|
68
|
+
parts.push(schema.enum.map((value) => JSON.stringify(value)).join("|"));
|
|
69
|
+
rendered.add("enum");
|
|
70
|
+
} else if (schema.type === "object" || schema.properties) {
|
|
71
|
+
parts.push(`object{${propertyList(schema.properties ?? {})}}`);
|
|
72
|
+
rendered.add("properties");
|
|
73
|
+
} else if (schema.type === "array") {
|
|
74
|
+
parts.push(schema.items ? `array<${describe(schema.items)}>` : "array");
|
|
75
|
+
rendered.add("items");
|
|
76
|
+
} else if (schema.type) {
|
|
77
|
+
parts.push([schema.type].flat().join("|"));
|
|
78
|
+
}
|
|
79
|
+
rendered.add("type");
|
|
80
|
+
for (const key of ["anyOf", "oneOf", "allOf"]) {
|
|
81
|
+
if (!schema[key]) continue;
|
|
82
|
+
const operator = key === "allOf" ? " & " : key === "oneOf" ? " XOR " : " | ";
|
|
83
|
+
parts.push(`(${schema[key].map((child) => describe(child)).join(operator)})`);
|
|
84
|
+
rendered.add(key);
|
|
85
|
+
}
|
|
86
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
87
|
+
if (rendered.has(key) || annotations.has(key)) continue;
|
|
88
|
+
parts.push(`${key}=${JSON.stringify(schemaValues.has(key) ? assertions(value) : value)}`);
|
|
89
|
+
}
|
|
90
|
+
return parts.join(" ");
|
|
91
|
+
}
|
|
92
|
+
function propertyList(properties) {
|
|
93
|
+
return Object.entries(properties)
|
|
94
|
+
.map(([name, schema]) => `${JSON.stringify(name)}: ${describe(schema)}`).join("; ");
|
|
95
|
+
}
|
|
96
|
+
const lines = [
|
|
97
|
+
"# Architecture DSL v1 authoring reference",
|
|
98
|
+
"",
|
|
99
|
+
"Schema-derived structural contract. Every permitted field is listed; unspecified fields are optional unless required below. Unknown fields are rejected, including inside style, canvas, point and layout objects. Do not invent aliases or CSS.",
|
|
100
|
+
"",
|
|
101
|
+
"## Root",
|
|
102
|
+
`Required: ${JSON.stringify(contract.root.required)}.`,
|
|
103
|
+
propertyList(contract.root.properties),
|
|
104
|
+
"",
|
|
105
|
+
"## Element fields and requirements",
|
|
106
|
+
"fixed = immediate parent has no layout (including the root). flow = immediate parent group has layout. A group's OWN layout places its children; it does NOT waive that group's fixed box requirements. Grandchildren follow their own parent's layout.",
|
|
107
|
+
"",
|
|
108
|
+
];
|
|
109
|
+
for (const [type, element] of Object.entries(contract.elements)) {
|
|
110
|
+
const conditions = Object.fromEntries(["if", "then", "else", "allOf"]
|
|
111
|
+
.filter((key) => Object.hasOwn(element, key)).map((key) => [key, element[key]]));
|
|
112
|
+
lines.push(
|
|
113
|
+
`### ${type}`,
|
|
114
|
+
`Required fixed: ${JSON.stringify(element.required.fixed)}; flow: ${JSON.stringify(element.required.flow)}.`,
|
|
115
|
+
propertyList(element.properties),
|
|
116
|
+
...(Object.keys(conditions).length ? [`Conditional constraints: ${describe(conditions)}.`] : []),
|
|
117
|
+
"",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
lines.push("## Shared value schemas");
|
|
121
|
+
// Definitions are discovered transitively from the property descriptors, not a curated list.
|
|
122
|
+
for (const name of used) {
|
|
123
|
+
lines.push(`- ${name}${name === "iconName" ? ' ("builtIn")' : ""}: ${describe(contract.definitions[name], name)}`);
|
|
124
|
+
}
|
|
125
|
+
lines.push(
|
|
126
|
+
"",
|
|
127
|
+
"## Authoring rules",
|
|
128
|
+
'Visible node text uses "text" (use \\n for multiple lines); group headings use "title"; connector annotations use "label". Root "title" is the accessible diagram name. A connector has no "id". Group children are elements.',
|
|
129
|
+
"Conditional constraints are emitted from Schema alongside the fields and shared values. A field permitted in one routing/layout mode is not automatically permitted in every mode.",
|
|
130
|
+
"For flow children omit x/y: runtime ignores them. Width/height are optional there and must fit the calculated cells. Use node.icon for built-in icons or assets/ paths; image.src is an asset path, not a URL. Theme tokens adapt; literal colors and image artwork do not.",
|
|
131
|
+
"",
|
|
132
|
+
"## Runtime checks",
|
|
133
|
+
"Schema validity is necessary for authoring, not sufficient for rendering. parseArchitecture also checks unique IDs across the tree, existing non-connector endpoints, no self-links, flattened element/connector/text limits, nesting depth and layout fit. Assets need separate existence/content checks; inspect visual clipping separately. Existing v1 runtime compatibility is preserved: ignored flow x/y and root $schema can differ from the stricter authoring schema. Never resolve $schema at runtime.",
|
|
134
|
+
"",
|
|
135
|
+
"## Complete minimal example",
|
|
136
|
+
"Two nodes and one connector; no assets or schema URL required. Paste into an architecture fence:",
|
|
137
|
+
"```architecture",
|
|
138
|
+
JSON.stringify(minimalExample, null, 2),
|
|
139
|
+
"```",
|
|
140
|
+
"",
|
|
141
|
+
"## Details",
|
|
142
|
+
"Full structural schema: bundled schema/architecture-v1.schema.json (offline editor completion). For layout semantics, constraints, examples and editing request markdstage_guide topic=architecture-dsl; this compact contract is topic=architecture-schema. See schema/README.md for schema/runtime differences and v1 compatibility.",
|
|
143
|
+
);
|
|
144
|
+
const reference = lines.join("\n");
|
|
145
|
+
const bytes = new TextEncoder().encode(reference).byteLength;
|
|
146
|
+
if (bytes > 8192) {
|
|
147
|
+
throw new RangeError(`Architecture reference is ${bytes} UTF-8 bytes; the complete reference must fit within 8192 bytes.`);
|
|
148
|
+
}
|
|
149
|
+
return reference;
|
|
150
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { validateArchitecture } from "./renderer/architecture.mjs";
|
|
2
|
+
import { findArchitectureBlocks } from "./scripts/markdown-blocks.mjs";
|
|
3
|
+
|
|
4
|
+
export const ARCHITECTURE_VALIDATION_LIMITS = Object.freeze({
|
|
5
|
+
maxSourceChars: 262_144,
|
|
6
|
+
maxSlideChars: 262_144,
|
|
7
|
+
maxTotalChars: 2_097_152,
|
|
8
|
+
maxSlides: 200,
|
|
9
|
+
maxBlocks: 200,
|
|
10
|
+
});
|
|
11
|
+
export const UNCLOSED_ARCHITECTURE_MESSAGE =
|
|
12
|
+
"The architecture code fence is not closed. Add ``` at the end.";
|
|
13
|
+
|
|
14
|
+
const STAGES = ["json", "structure", "semantic", "layout"];
|
|
15
|
+
const skippedStages = () => Object.fromEntries(STAGES.map((stage) => [stage, "skipped"]));
|
|
16
|
+
|
|
17
|
+
export class ArchitectureValidationInputError extends TypeError {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "ArchitectureValidationInputError";
|
|
21
|
+
this.code = "invalid_input";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function checkInput(input) {
|
|
26
|
+
const reject = (message) => {
|
|
27
|
+
throw new ArchitectureValidationInputError(message);
|
|
28
|
+
};
|
|
29
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
30
|
+
reject("Provide an object with an explicit format of 'dsl' or 'slides'.");
|
|
31
|
+
}
|
|
32
|
+
const allowed = new Set(["format", "source", "slides", "maxDiagnostics"]);
|
|
33
|
+
for (const key of Object.keys(input)) {
|
|
34
|
+
if (!allowed.has(key)) reject(`Unknown validation input field: ${key}.`);
|
|
35
|
+
}
|
|
36
|
+
if (!Object.hasOwn(input, "format") || (input.format !== "dsl" && input.format !== "slides")) {
|
|
37
|
+
reject("format must be explicitly set to 'dsl' or 'slides'.");
|
|
38
|
+
}
|
|
39
|
+
const hasSource = Object.hasOwn(input, "source");
|
|
40
|
+
const hasSlides = Object.hasOwn(input, "slides");
|
|
41
|
+
if (input.format === "dsl") {
|
|
42
|
+
if (!hasSource || hasSlides || typeof input.source !== "string") {
|
|
43
|
+
reject("format 'dsl' requires a string source and must not include slides.");
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
if (hasSource || !hasSlides || !Array.isArray(input.slides) || !input.slides.length) {
|
|
47
|
+
reject("format 'slides' requires a non-empty array of one-slide Markdown fragments and must not include source.");
|
|
48
|
+
}
|
|
49
|
+
const inspectableSlides = Math.min(input.slides.length, ARCHITECTURE_VALIDATION_LIMITS.maxSlides);
|
|
50
|
+
for (let index = 0; index < inspectableSlides; index += 1) {
|
|
51
|
+
if (typeof input.slides[index] !== "string") {
|
|
52
|
+
reject(`slides[${index}] must be a one-slide Markdown string.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const maxDiagnostics = Object.hasOwn(input, "maxDiagnostics") ? input.maxDiagnostics : 50;
|
|
57
|
+
if (!Number.isInteger(maxDiagnostics) || maxDiagnostics < 1 || maxDiagnostics > 100) {
|
|
58
|
+
reject("maxDiagnostics must be an integer from 1 to 100.");
|
|
59
|
+
}
|
|
60
|
+
return maxDiagnostics;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function blockPosition(slideIndex, block, lineCount) {
|
|
64
|
+
return {
|
|
65
|
+
slideIndex,
|
|
66
|
+
page: slideIndex + 1,
|
|
67
|
+
blockIndex: block.index,
|
|
68
|
+
architecture: block.index + 1,
|
|
69
|
+
openLine: block.open + 1,
|
|
70
|
+
closeLine: block.end < lineCount ? block.end + 1 : null,
|
|
71
|
+
endLine: block.end < lineCount ? block.end + 1 : lineCount,
|
|
72
|
+
closed: block.end < lineCount,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate explicit, unloaded content. Limits bound this inspection only; they
|
|
78
|
+
* do not add restrictions to the Architecture DSL parser or alter any source.
|
|
79
|
+
*/
|
|
80
|
+
export function validateArchitectureInput(input) {
|
|
81
|
+
const maxDiagnostics = checkInput(input);
|
|
82
|
+
const limits = { ...ARCHITECTURE_VALIDATION_LIMITS, maxDiagnostics };
|
|
83
|
+
const diagnostics = [];
|
|
84
|
+
const blocks = [];
|
|
85
|
+
const skipped = [];
|
|
86
|
+
const budget = {
|
|
87
|
+
inputChars: input.format === "dsl"
|
|
88
|
+
? input.source.length
|
|
89
|
+
: input.slides.length > limits.maxSlides
|
|
90
|
+
? null
|
|
91
|
+
: input.slides.reduce((total, slide) => total + slide.length, 0),
|
|
92
|
+
scannedChars: 0,
|
|
93
|
+
scannedSlides: 0,
|
|
94
|
+
processedBlocks: 0,
|
|
95
|
+
skippedSlides: 0,
|
|
96
|
+
skippedBlocks: 0,
|
|
97
|
+
limitsReached: [],
|
|
98
|
+
};
|
|
99
|
+
let truncated = false;
|
|
100
|
+
|
|
101
|
+
function reachLimit(reason, position = {}) {
|
|
102
|
+
truncated = true;
|
|
103
|
+
if (budget.limitsReached.includes(reason)) return;
|
|
104
|
+
budget.limitsReached.push(reason);
|
|
105
|
+
if (diagnostics.length < maxDiagnostics) {
|
|
106
|
+
diagnostics.push({
|
|
107
|
+
code: "validation_budget_exceeded",
|
|
108
|
+
category: "structure",
|
|
109
|
+
severity: "warning",
|
|
110
|
+
pointer: "",
|
|
111
|
+
message: `Validation stopped at the ${reason} inspection limit (${limits[reason]}). Unchecked content is not valid or complete.`,
|
|
112
|
+
suggestions: [{
|
|
113
|
+
action: "review",
|
|
114
|
+
message: "Validate smaller explicit inputs separately; no source was changed.",
|
|
115
|
+
automatic: false,
|
|
116
|
+
}],
|
|
117
|
+
...position,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function skipSlides(slideIndex, count, reason) {
|
|
123
|
+
if (!count) return;
|
|
124
|
+
reachLimit(reason, { slideIndex, page: slideIndex + 1 });
|
|
125
|
+
skipped.push({
|
|
126
|
+
slideIndex,
|
|
127
|
+
page: slideIndex + 1,
|
|
128
|
+
slideCount: count,
|
|
129
|
+
reason,
|
|
130
|
+
stages: skippedStages(),
|
|
131
|
+
});
|
|
132
|
+
budget.skippedSlides += count;
|
|
133
|
+
// Counting fences would itself inspect slides that were skipped.
|
|
134
|
+
budget.skippedBlocks = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function skipBlocks(position, count, reason) {
|
|
138
|
+
reachLimit(reason, position);
|
|
139
|
+
skipped.push({ ...position, blockCount: count, reason, stages: skippedStages() });
|
|
140
|
+
if (budget.skippedBlocks !== null) budget.skippedBlocks += count;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function inspect(source, position = {}) {
|
|
144
|
+
const remaining = maxDiagnostics - diagnostics.length;
|
|
145
|
+
const report = validateArchitecture(source, { maxDiagnostics: remaining });
|
|
146
|
+
const diagnosticStart = diagnostics.length;
|
|
147
|
+
diagnostics.push(...report.diagnostics.map((diagnostic) => ({ ...diagnostic, ...position })));
|
|
148
|
+
let blockTruncated = report.truncated;
|
|
149
|
+
const truncationReasons = new Set(report.truncated ? report.truncationReasons : []);
|
|
150
|
+
if (position.closed === false) {
|
|
151
|
+
if (diagnostics.length < maxDiagnostics) {
|
|
152
|
+
diagnostics.push({
|
|
153
|
+
code: "unclosed_architecture_fence",
|
|
154
|
+
category: "structure",
|
|
155
|
+
severity: "error",
|
|
156
|
+
pointer: "",
|
|
157
|
+
message: UNCLOSED_ARCHITECTURE_MESSAGE,
|
|
158
|
+
suggestions: [{
|
|
159
|
+
action: "review",
|
|
160
|
+
message: "Close the architecture fence with the same marker and at least its opening length.",
|
|
161
|
+
automatic: false,
|
|
162
|
+
}],
|
|
163
|
+
...position,
|
|
164
|
+
});
|
|
165
|
+
} else {
|
|
166
|
+
blockTruncated = true;
|
|
167
|
+
truncationReasons.add("maxDiagnostics");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (blockTruncated) {
|
|
171
|
+
truncated = true;
|
|
172
|
+
for (const reason of truncationReasons) {
|
|
173
|
+
// maxDiagnostics here is deck-wide, not this block's remaining allowance.
|
|
174
|
+
if (reason !== "maxDiagnostics" && Object.hasOwn(report.limits ?? {}, reason)) {
|
|
175
|
+
limits[reason] = report.limits[reason];
|
|
176
|
+
}
|
|
177
|
+
reachLimit(reason, position);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
blocks.push({
|
|
181
|
+
...position,
|
|
182
|
+
valid: report.valid && position.closed !== false && !blockTruncated,
|
|
183
|
+
dslValid: report.valid,
|
|
184
|
+
complete: report.complete && !blockTruncated,
|
|
185
|
+
truncated: blockTruncated,
|
|
186
|
+
...(blockTruncated ? { truncationReasons: [...truncationReasons] } : {}),
|
|
187
|
+
stages: report.stages,
|
|
188
|
+
diagnosticStart,
|
|
189
|
+
diagnosticCount: diagnostics.length - diagnosticStart,
|
|
190
|
+
});
|
|
191
|
+
budget.processedBlocks += 1;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (input.format === "dsl") {
|
|
195
|
+
if (input.source.length > limits.maxSourceChars) {
|
|
196
|
+
skipBlocks({}, 1, "maxSourceChars");
|
|
197
|
+
} else {
|
|
198
|
+
budget.scannedChars = input.source.length;
|
|
199
|
+
inspect(input.source);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
for (let slideIndex = 0; slideIndex < input.slides.length; slideIndex += 1) {
|
|
203
|
+
const remainingSlides = input.slides.length - slideIndex;
|
|
204
|
+
if (slideIndex >= limits.maxSlides) {
|
|
205
|
+
skipSlides(slideIndex, remainingSlides, "maxSlides");
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
if (diagnostics.length >= maxDiagnostics) {
|
|
209
|
+
skipSlides(slideIndex, remainingSlides, "maxDiagnostics");
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
if (budget.processedBlocks >= limits.maxBlocks) {
|
|
213
|
+
skipSlides(slideIndex, remainingSlides, "maxBlocks");
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
const slide = input.slides[slideIndex];
|
|
217
|
+
if (slide.length > limits.maxSlideChars) {
|
|
218
|
+
skipSlides(slideIndex, 1, "maxSlideChars");
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (budget.scannedChars + slide.length > limits.maxTotalChars) {
|
|
222
|
+
skipSlides(slideIndex, remainingSlides, "maxTotalChars");
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
budget.scannedChars += slide.length;
|
|
226
|
+
budget.scannedSlides += 1;
|
|
227
|
+
const found = findArchitectureBlocks(slide);
|
|
228
|
+
const lineCount = slide.split(/\r\n?|\n/).length;
|
|
229
|
+
for (let index = 0; index < found.length; index += 1) {
|
|
230
|
+
const position = blockPosition(slideIndex, found[index], lineCount);
|
|
231
|
+
const reason = diagnostics.length >= maxDiagnostics
|
|
232
|
+
? "maxDiagnostics"
|
|
233
|
+
: budget.processedBlocks >= limits.maxBlocks ? "maxBlocks" : null;
|
|
234
|
+
if (reason) {
|
|
235
|
+
skipBlocks(position, found.length - index, reason);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
inspect(found[index].body, position);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const complete = !truncated && blocks.every((block) => block.complete);
|
|
244
|
+
const valid = complete && blocks.every((block) => block.valid);
|
|
245
|
+
const stages = Object.fromEntries(STAGES.map((stage) => {
|
|
246
|
+
const failed = blocks.some((block) => block.stages[stage] === "failed") ||
|
|
247
|
+
diagnostics.some((diagnostic) => diagnostic.category === stage && diagnostic.severity === "error");
|
|
248
|
+
const unchecked = skipped.length > 0 || blocks.some((block) => block.stages[stage] === "skipped");
|
|
249
|
+
return [stage, failed ? "failed" : unchecked ? "skipped" : "passed"];
|
|
250
|
+
}));
|
|
251
|
+
return {
|
|
252
|
+
ok: true,
|
|
253
|
+
format: input.format,
|
|
254
|
+
scope: input.format === "dsl" ? "dsl" : "deck",
|
|
255
|
+
...(input.format === "slides" ? { total: input.slides.length } : {}),
|
|
256
|
+
valid,
|
|
257
|
+
complete,
|
|
258
|
+
truncated,
|
|
259
|
+
stages,
|
|
260
|
+
diagnostics,
|
|
261
|
+
diagnosticCount: diagnostics.length,
|
|
262
|
+
blocks,
|
|
263
|
+
skipped,
|
|
264
|
+
limits,
|
|
265
|
+
budget,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function createArchitectureValidationTool() {
|
|
270
|
+
return {
|
|
271
|
+
name: "markdstage_validate",
|
|
272
|
+
description:
|
|
273
|
+
"Read-only preflight of explicit, unloaded Architecture DSL or one-slide Markdown fragments. Call markdstage_guide with architecture-schema BEFORE drafting DSL, then call this tool BEFORE display. Requires format 'dsl' with source OR format 'slides' with slides, never both. Does not open, inspect, navigate, or modify a canvas or file. Returns bounded canonical diagnostics; ok describes invocation, while valid/complete/truncated describe the content check.",
|
|
274
|
+
parameters: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: {
|
|
277
|
+
format: { type: "string", enum: ["dsl", "slides"] },
|
|
278
|
+
source: { type: "string", description: "Raw Architecture DSL JSON, only with format 'dsl'." },
|
|
279
|
+
slides: {
|
|
280
|
+
type: "array",
|
|
281
|
+
items: { type: "string" },
|
|
282
|
+
minItems: 1,
|
|
283
|
+
description: "One-slide Markdown fragments, only with format 'slides'; this does not split whole Markdown files.",
|
|
284
|
+
},
|
|
285
|
+
maxDiagnostics: { type: "integer", minimum: 1, maximum: 100, default: 50 },
|
|
286
|
+
},
|
|
287
|
+
required: ["format"],
|
|
288
|
+
additionalProperties: false,
|
|
289
|
+
oneOf: [
|
|
290
|
+
{ properties: { format: { const: "dsl" } }, required: ["source"], not: { required: ["slides"] } },
|
|
291
|
+
{ properties: { format: { const: "slides" } }, required: ["slides"], not: { required: ["source"] } },
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
handler: async (input) => {
|
|
295
|
+
try {
|
|
296
|
+
return {
|
|
297
|
+
textResultForLlm: JSON.stringify(validateArchitectureInput(input)),
|
|
298
|
+
resultType: "success",
|
|
299
|
+
};
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (!(error instanceof ArchitectureValidationInputError)) throw error;
|
|
302
|
+
return {
|
|
303
|
+
textResultForLlm: JSON.stringify({ ok: false, error: error.code, message: error.message }),
|
|
304
|
+
resultType: "failure",
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|