@kamishibai/sdk 0.1.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.
Files changed (136) hide show
  1. package/README.md +192 -0
  2. package/package.json +54 -0
  3. package/src/blocks/board.js +210 -0
  4. package/src/blocks/callout.js +63 -0
  5. package/src/blocks/code.js +28 -0
  6. package/src/blocks/deck.js +76 -0
  7. package/src/blocks/diagram.js +265 -0
  8. package/src/blocks/element.js +51 -0
  9. package/src/blocks/graph.js +264 -0
  10. package/src/blocks/grid.js +156 -0
  11. package/src/blocks/index.js +106 -0
  12. package/src/blocks/list.js +50 -0
  13. package/src/blocks/placement.js +119 -0
  14. package/src/blocks/prose.js +28 -0
  15. package/src/blocks/quote.js +25 -0
  16. package/src/blocks/raw.js +47 -0
  17. package/src/blocks/registry.js +158 -0
  18. package/src/blocks/schema-parts.js +19 -0
  19. package/src/blocks/section.js +53 -0
  20. package/src/blocks/slide.js +104 -0
  21. package/src/blocks/stat.js +83 -0
  22. package/src/blocks/table.js +50 -0
  23. package/src/blocks/timeline.js +80 -0
  24. package/src/cli/commands/close.js +51 -0
  25. package/src/cli/commands/comments.js +73 -0
  26. package/src/cli/commands/debug.js +34 -0
  27. package/src/cli/commands/example.js +22 -0
  28. package/src/cli/commands/export.js +10 -0
  29. package/src/cli/commands/init.js +53 -0
  30. package/src/cli/commands/lint.js +93 -0
  31. package/src/cli/commands/list.js +54 -0
  32. package/src/cli/commands/open.js +31 -0
  33. package/src/cli/commands/promote.js +38 -0
  34. package/src/cli/commands/render.js +31 -0
  35. package/src/cli/commands/replay.js +49 -0
  36. package/src/cli/commands/schema.js +10 -0
  37. package/src/cli/commands/serve.js +178 -0
  38. package/src/cli/commands/setup.js +64 -0
  39. package/src/cli/commands/snapshot.js +29 -0
  40. package/src/cli/commands/templates.js +75 -0
  41. package/src/cli/deliver.js +54 -0
  42. package/src/cli/emit.js +29 -0
  43. package/src/cli/format.js +153 -0
  44. package/src/cli/index.js +365 -0
  45. package/src/cli/registry.js +18 -0
  46. package/src/core/blocks.js +147 -0
  47. package/src/core/diagram.js +282 -0
  48. package/src/core/errors.js +156 -0
  49. package/src/core/example.js +109 -0
  50. package/src/core/ir.js +62 -0
  51. package/src/core/lint-gates.js +427 -0
  52. package/src/core/lint.js +281 -0
  53. package/src/core/scan.js +84 -0
  54. package/src/core/schema.js +88 -0
  55. package/src/core/spec-check.js +33 -0
  56. package/src/core/validate.js +44 -0
  57. package/src/core/version.js +18 -0
  58. package/src/core/vocabulary.js +140 -0
  59. package/src/delivery/atomic.js +71 -0
  60. package/src/delivery/comments.js +180 -0
  61. package/src/delivery/home.js +70 -0
  62. package/src/delivery/open.js +31 -0
  63. package/src/delivery/project.js +141 -0
  64. package/src/delivery/read.js +109 -0
  65. package/src/delivery/run.js +95 -0
  66. package/src/delivery/scaffold-blueprints.js +728 -0
  67. package/src/delivery/store.js +219 -0
  68. package/src/delivery/template-extensions.js +183 -0
  69. package/src/delivery/template-format.js +112 -0
  70. package/src/delivery/template-package.js +376 -0
  71. package/src/delivery/template-promote.js +240 -0
  72. package/src/delivery/template-scaffold.js +181 -0
  73. package/src/delivery/templates.js +192 -0
  74. package/src/delivery/toml.js +195 -0
  75. package/src/delivery/write.js +35 -0
  76. package/src/export/browser.js +130 -0
  77. package/src/export/index.js +96 -0
  78. package/src/export/pdf.js +25 -0
  79. package/src/export/png.js +40 -0
  80. package/src/export/pptx.js +48 -0
  81. package/src/export/slides.js +33 -0
  82. package/src/export/snapshot.js +33 -0
  83. package/src/layouts/article.js +103 -0
  84. package/src/layouts/canvas.js +144 -0
  85. package/src/layouts/card.js +128 -0
  86. package/src/layouts/deck.js +88 -0
  87. package/src/layouts/index.js +90 -0
  88. package/src/layouts/one-page.js +161 -0
  89. package/src/layouts/registry.js +251 -0
  90. package/src/layouts/resume.js +172 -0
  91. package/src/layouts/template-index.js +78 -0
  92. package/src/parser/artifact.js +38 -0
  93. package/src/parser/container.js +103 -0
  94. package/src/parser/index.js +223 -0
  95. package/src/parser/tokens.js +265 -0
  96. package/src/render/board-filter.client.js +80 -0
  97. package/src/render/compile.js +29 -0
  98. package/src/render/context.js +98 -0
  99. package/src/render/element.js +32 -0
  100. package/src/render/fonts.js +129 -0
  101. package/src/render/graph-hover.client.js +148 -0
  102. package/src/render/html.js +52 -0
  103. package/src/render/index.js +241 -0
  104. package/src/render/measure.js +60 -0
  105. package/src/render/placement.js +136 -0
  106. package/src/render/playback.client.js +74 -0
  107. package/src/render/scale-to-fit.client.js +136 -0
  108. package/src/render/scale.js +41 -0
  109. package/src/render/skeleton.js +131 -0
  110. package/src/render/ssr.js +24 -0
  111. package/src/render/styles.js +56 -0
  112. package/src/render/templates.js +191 -0
  113. package/src/serve/daemon.js +117 -0
  114. package/src/serve/overlay.js +213 -0
  115. package/src/serve/protocol.js +36 -0
  116. package/src/serve/server.js +264 -0
  117. package/templates/kami/cards/components.js +40 -0
  118. package/templates/kami/cards/index.js +25 -0
  119. package/templates/kami/cards/manifest.js +67 -0
  120. package/templates/kami/cards/styles.css +389 -0
  121. package/templates/kami/long-form/components.js +102 -0
  122. package/templates/kami/long-form/index.js +25 -0
  123. package/templates/kami/long-form/manifest.js +87 -0
  124. package/templates/kami/long-form/styles.css +481 -0
  125. package/templates/kami/one-page/components.js +48 -0
  126. package/templates/kami/one-page/index.js +27 -0
  127. package/templates/kami/one-page/manifest.js +65 -0
  128. package/templates/kami/one-page/styles.css +375 -0
  129. package/templates/kami/resume/components.js +51 -0
  130. package/templates/kami/resume/index.js +27 -0
  131. package/templates/kami/resume/manifest.js +65 -0
  132. package/templates/kami/resume/styles.css +424 -0
  133. package/templates/kami/slides/components.js +41 -0
  134. package/templates/kami/slides/index.js +26 -0
  135. package/templates/kami/slides/manifest.js +64 -0
  136. package/templates/kami/slides/styles.css +406 -0
@@ -0,0 +1,282 @@
1
+ import { DIAGRAM_KINDS, diagramProblems } from '../blocks/diagram.js'
2
+
3
+ /**
4
+ * `diagram` geometry — the layout half of the block (SPEC §3.3).
5
+ *
6
+ * What a diagram *is* (its spec, its schema, its markup) lives in
7
+ * `src/blocks/diagram.js`; what it *measures out to* lives here, because layout
8
+ * is not a block-level fact: it depends on how much width the page gives the
9
+ * block, which only the layer above knows (wayfinder issue 13 試金石).
10
+ *
11
+ * **Layout is pure integer arithmetic.** No clock, no randomness, no measured
12
+ * text: the same spec and the same available width must yield byte-identical
13
+ * SVG (CONTRACT D1), because that is what makes `replay` and the export chain
14
+ * verifiable. The cost is honest ugliness — a long label overflows its box
15
+ * rather than being re-flowed by something we cannot reproduce.
16
+ */
17
+
18
+ export { DIAGRAM_KINDS, diagramProblems }
19
+
20
+ /**
21
+ * Geometry, in SVG user units. Even numbers keep every midpoint an integer.
22
+ *
23
+ * Layers run **downwards**, siblings run across. The direction is not a taste
24
+ * call: an artifact's SVG is scaled to the text measure, so a left-to-right
25
+ * chain of six nodes produced a 1500-unit canvas squeezed to half size — a
26
+ * picture whose labels could no longer be read. Stacking layers vertically
27
+ * keeps a chain one column wide and therefore legible at 1:1, and it is the
28
+ * conventional reading direction for a flow anyway.
29
+ */
30
+ const NODE_W = 180
31
+ const NODE_H = 56
32
+ /** Vertical distance between one layer and the next. */
33
+ const LAYER_GAP = 48
34
+ /** Horizontal distance between two nodes sharing a layer. */
35
+ const ROW_GAP = 28
36
+ const PAD = 24
37
+ /** How far a non-forward edge bulges out to the right of the boxes it joins. */
38
+ const BACK_DIP = 44
39
+ /** Extra canvas reserved on the right when non-forward edges exist. */
40
+ const BACK_BAND = BACK_DIP + 40
41
+
42
+ /**
43
+ * Available width, in CSS pixels, when nobody says otherwise.
44
+ *
45
+ * `NODE_W` was tuned against the long-form text measure of 740px, and that
46
+ * assumption used to be invisible — the layout took no width at all, so a
47
+ * diagram placed in a narrower column was simply squeezed until its labels
48
+ * stopped being readable (量測報告 §4.2). The assumption is now a parameter
49
+ * with the old value as its default, which is what keeps every existing
50
+ * artifact byte-identical while making the narrow case expressible.
51
+ */
52
+ export const DEFAULT_MEASURE = 740
53
+
54
+ /**
55
+ * How many boxes fit across the available width — at least one, because a
56
+ * single box cannot be narrower than a box.
57
+ */
58
+ const rowCapacity = (measure) =>
59
+ Math.max(1, Math.floor((measure - PAD * 2 + ROW_GAP) / (NODE_W + ROW_GAP)))
60
+
61
+ /**
62
+ * Edge indices that close a cycle, found by an iterative DFS in declaration
63
+ * order (so the answer is a deterministic function of the spec).
64
+ *
65
+ * Cycles must be *removed before layering*, not merely survived. A `store →
66
+ * render` back edge — which every real architecture diagram has — makes plain
67
+ * longest-path relaxation push its whole cycle one layer further on every
68
+ * pass: capping the passes terminates, but the picture it terminates on is a
69
+ * nonsense 5000-unit-wide canvas. Breaking the cycle first keeps the layering
70
+ * a property of the structure rather than of the iteration count.
71
+ *
72
+ * The DFS is iterative because the stack depth would otherwise be the node
73
+ * count, and a spec is user input.
74
+ */
75
+ const findBackEdges = (nodes, edges) => {
76
+ const outgoing = new Map(nodes.map((node) => [node.id, []]))
77
+ for (const [index, edge] of edges.entries()) {
78
+ if (edge.from !== edge.to) outgoing.get(edge.from).push({ index, to: edge.to })
79
+ }
80
+
81
+ const UNSEEN = 0
82
+ const OPEN = 1
83
+ const DONE = 2
84
+ const state = new Map(nodes.map((node) => [node.id, UNSEEN]))
85
+ const back = new Set()
86
+
87
+ for (const root of nodes) {
88
+ if (state.get(root.id) !== UNSEEN) continue
89
+ const stack = [{ id: root.id, next: 0 }]
90
+ state.set(root.id, OPEN)
91
+ while (stack.length > 0) {
92
+ const frame = stack[stack.length - 1]
93
+ const edgesOut = outgoing.get(frame.id)
94
+ if (frame.next >= edgesOut.length) {
95
+ state.set(frame.id, DONE)
96
+ stack.pop()
97
+ continue
98
+ }
99
+ const { index, to } = edgesOut[frame.next]
100
+ frame.next += 1
101
+ const seen = state.get(to)
102
+ if (seen === OPEN) back.add(index)
103
+ else if (seen === UNSEEN) {
104
+ state.set(to, OPEN)
105
+ stack.push({ id: to, next: 0 })
106
+ }
107
+ }
108
+ }
109
+ return back
110
+ }
111
+
112
+ /** Longest-path layering over the acyclic remainder of the spec. */
113
+ const assignLayers = (nodes, edges) => {
114
+ const back = findBackEdges(nodes, edges)
115
+ const forward = edges.filter((edge, index) => edge.from !== edge.to && !back.has(index))
116
+ const layer = new Map(nodes.map((node) => [node.id, 0]))
117
+ for (let pass = 0; pass < nodes.length; pass += 1) {
118
+ let changed = false
119
+ for (const edge of forward) {
120
+ const want = layer.get(edge.from) + 1
121
+ if (want > layer.get(edge.to)) {
122
+ layer.set(edge.to, want)
123
+ changed = true
124
+ }
125
+ }
126
+ if (!changed) break
127
+ }
128
+ return layer
129
+ }
130
+
131
+ const half = (value) => Math.round(value / 2)
132
+
133
+ /**
134
+ * Labels sit *beside* their connector, anchored at the start, never centred on
135
+ * it: a centred label lands exactly on the line it describes.
136
+ */
137
+ const LABEL_ANCHOR = 'start'
138
+ const LABEL_INSET = 8
139
+
140
+ /** `M …` path for an edge that goes strictly downwards, layer to layer. */
141
+ const forwardPath = (a, b) => {
142
+ const x1 = a.cx
143
+ const y1 = a.y + NODE_H
144
+ const x2 = b.cx
145
+ const y2 = b.y
146
+ const bend = half(y2 - y1)
147
+ return {
148
+ d: `M ${x1} ${y1} C ${x1} ${y1 + bend} ${x2} ${y2 - bend} ${x2} ${y2}`,
149
+ labelX: half(x1 + x2) + LABEL_INSET,
150
+ labelY: half(y1 + y2) + 4,
151
+ }
152
+ }
153
+
154
+ /** `M …` path for a back edge or a same-layer edge: bulge out to the right. */
155
+ const backPath = (a, b) => {
156
+ const x1 = a.x + NODE_W
157
+ const y1 = a.cy
158
+ const x2 = b.x + NODE_W
159
+ const y2 = b.cy
160
+ const bulge = Math.max(x1, x2) + BACK_DIP
161
+ return {
162
+ d: `M ${x1} ${y1} C ${bulge} ${y1} ${bulge} ${y2} ${x2} ${y2}`,
163
+ labelX: bulge + LABEL_INSET,
164
+ labelY: half(y1 + y2) + 4,
165
+ }
166
+ }
167
+
168
+ /** `M …` path for a self-loop: a lobe off the node's right edge. */
169
+ const selfPath = (a) => {
170
+ const x1 = a.x + NODE_W
171
+ const y1 = a.y + Math.round(NODE_H / 3)
172
+ const y2 = a.y + Math.round((NODE_H * 2) / 3)
173
+ const out = x1 + BACK_DIP
174
+ return {
175
+ d: `M ${x1} ${y1} C ${out} ${y1} ${out} ${y2} ${x1} ${y2}`,
176
+ labelX: out + LABEL_INSET,
177
+ labelY: a.cy + 4,
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Place every node on a (layer, row) grid, wrapping a layer whose siblings do
183
+ * not fit across `capacity` columns onto further rows of the same layer.
184
+ *
185
+ * Wrapping rather than shrinking is the whole point of taking a width: a
186
+ * narrower column must not make the boxes — and therefore the labels — smaller,
187
+ * or the picture stops being readable at exactly the moment it gets tight.
188
+ */
189
+ const gridPositions = (nodes, layer, capacity) => {
190
+ const seenPerLayer = new Map()
191
+ const cells = new Map()
192
+ const rowsBefore = new Map()
193
+ let totalRows = 0
194
+ for (const node of nodes) {
195
+ const l = layer.get(node.id)
196
+ const seen = seenPerLayer.get(l) ?? 0
197
+ seenPerLayer.set(l, seen + 1)
198
+ cells.set(node.id, { layer: l, column: seen % capacity, wrap: Math.floor(seen / capacity) })
199
+ }
200
+ // Layers are laid out top to bottom, and a wrapped layer occupies more than
201
+ // one row, so a layer's vertical offset is the number of rows above it.
202
+ for (const l of [...seenPerLayer.keys()].sort((a, b) => a - b)) {
203
+ rowsBefore.set(l, totalRows)
204
+ totalRows += Math.ceil(seenPerLayer.get(l) / capacity)
205
+ }
206
+ const widestRow = Math.max(
207
+ ...[...seenPerLayer.values()].map((count) => Math.min(count, capacity)),
208
+ )
209
+ return { cells, rowsBefore, totalRows, widestRow }
210
+ }
211
+
212
+ /**
213
+ * Deterministic layered layout for a `kind: "graph"` diagram.
214
+ *
215
+ * Callers must have passed `diagramProblems` / `assertDiagrams` first; this
216
+ * function assumes a valid spec and does no error reporting of its own.
217
+ *
218
+ * @param {object} block a validated `diagram` block
219
+ * @param {{measure?: number}} [options] available width in CSS pixels
220
+ * @returns {{width: number, height: number,
221
+ * nodes: Array<{id: string, label: string, x: number, y: number, w: number, h: number,
222
+ * cx: number, cy: number}>,
223
+ * edges: Array<{from: string, to: string, label: string|undefined, d: string,
224
+ * labelX: number, labelY: number}>}}
225
+ */
226
+ export function layoutGraph(block, options = {}) {
227
+ const measure = Number.isFinite(options.measure) ? options.measure : DEFAULT_MEASURE
228
+ const nodes = block.nodes
229
+ const edges = block.edges ?? []
230
+ const layer = assignLayers(nodes, edges)
231
+ const { cells, rowsBefore, totalRows, widestRow } = gridPositions(
232
+ nodes,
233
+ layer,
234
+ rowCapacity(measure),
235
+ )
236
+
237
+ const placed = nodes.map((node) => {
238
+ const cell = cells.get(node.id)
239
+ const x = PAD + cell.column * (NODE_W + ROW_GAP)
240
+ const y = PAD + (rowsBefore.get(cell.layer) + cell.wrap) * (NODE_H + LAYER_GAP)
241
+ return {
242
+ id: node.id,
243
+ label: node.label,
244
+ x,
245
+ y,
246
+ w: NODE_W,
247
+ h: NODE_H,
248
+ cx: x + half(NODE_W),
249
+ cy: y + half(NODE_H),
250
+ }
251
+ })
252
+ const byId = new Map(placed.map((node) => [node.id, node]))
253
+
254
+ let hasBackEdge = false
255
+ const drawn = edges.map((edge) => {
256
+ const a = byId.get(edge.from)
257
+ const b = byId.get(edge.to)
258
+ let geometry
259
+ if (edge.from === edge.to) {
260
+ geometry = selfPath(a)
261
+ hasBackEdge = true
262
+ } else if (b.y > a.y) geometry = forwardPath(a, b)
263
+ else {
264
+ geometry = backPath(a, b)
265
+ hasBackEdge = true
266
+ }
267
+ return {
268
+ from: edge.from,
269
+ to: edge.to,
270
+ label: edge.label,
271
+ labelAnchor: LABEL_ANCHOR,
272
+ ...geometry,
273
+ }
274
+ })
275
+
276
+ return {
277
+ width: PAD * 2 + widestRow * (NODE_W + ROW_GAP) - ROW_GAP + (hasBackEdge ? BACK_BAND : 0),
278
+ height: PAD * 2 + totalRows * (NODE_H + LAYER_GAP) - LAYER_GAP,
279
+ nodes: placed,
280
+ edges: drawn,
281
+ }
282
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Error vocabulary. Every user-facing failure carries a KSB_ code and a block
3
+ * path so Agents get machine-actionable feedback (SPEC §10.3).
4
+ */
5
+
6
+ export const EXIT = Object.freeze({
7
+ OK: 0,
8
+ VALIDATION: 1,
9
+ USAGE: 2,
10
+ })
11
+
12
+ export const CODES = Object.freeze({
13
+ INPUT_NOT_FOUND: 'KSB_INPUT_NOT_FOUND',
14
+ INPUT_EMPTY: 'KSB_INPUT_EMPTY',
15
+ PARSE_FAILED: 'KSB_PARSE_FAILED',
16
+ TEMPLATE_NOT_FOUND: 'KSB_TEMPLATE_NOT_FOUND',
17
+ TEMPLATE_BLOCK_UNSUPPORTED: 'KSB_TEMPLATE_BLOCK_UNSUPPORTED',
18
+ WRITE_FAILED: 'KSB_WRITE_FAILED',
19
+ USAGE: 'KSB_USAGE',
20
+ ARTIFACT_UNREADABLE: 'KSB_ARTIFACT_UNREADABLE',
21
+ ARTIFACT_NOT_FOUND: 'KSB_ARTIFACT_NOT_FOUND',
22
+ OPEN_FAILED: 'KSB_OPEN_FAILED',
23
+ IR_MISSING: 'KSB_IR_MISSING',
24
+ IR_DUPLICATE: 'KSB_IR_DUPLICATE',
25
+ IR_UNPARSABLE: 'KSB_IR_UNPARSABLE',
26
+ IR_SCHEMA: 'KSB_IR_SCHEMA',
27
+ EXTERNAL_SCRIPT: 'KSB_EXTERNAL_SCRIPT',
28
+ EXTERNAL_STYLESHEET: 'KSB_EXTERNAL_STYLESHEET',
29
+ EXTERNAL_IMAGE: 'KSB_EXTERNAL_IMAGE',
30
+ EXTERNAL_CSS_IMPORT: 'KSB_EXTERNAL_CSS_IMPORT',
31
+ EXTERNAL_CSS_URL: 'KSB_EXTERNAL_CSS_URL',
32
+ BANNED_FONT: 'KSB_BANNED_FONT',
33
+ DIAGRAM_INVALID: 'KSB_DIAGRAM_INVALID',
34
+ /** F6a — a board card pointing at a lane nobody declared (silent card loss). */
35
+ BOARD_INVALID: 'KSB_BOARD_INVALID',
36
+ /** F6a — a graph edge pointing at a node nobody declared (silent edge loss). */
37
+ GRAPH_INVALID: 'KSB_GRAPH_INVALID',
38
+ EXPORT_FORMAT_MISMATCH: 'KSB_EXPORT_FORMAT_MISMATCH',
39
+ BROWSER_MISSING: 'KSB_BROWSER_MISSING',
40
+ EXPORT_FAILED: 'KSB_EXPORT_FAILED',
41
+ /** S5 — a comment anchored to a block the artifact's IR does not contain. */
42
+ BLOCK_NOT_FOUND: 'KSB_BLOCK_NOT_FOUND',
43
+ /** S5 — `comments resolve` naming a comment id no record carries. */
44
+ COMMENT_NOT_FOUND: 'KSB_COMMENT_NOT_FOUND',
45
+ /** S5 — the dev preview server could not be started (port taken, no handshake). */
46
+ SERVE_START_FAILED: 'KSB_SERVE_START_FAILED',
47
+ /** S5 — `--port` that is not a legal TCP port. */
48
+ SERVE_PORT_INVALID: 'KSB_SERVE_PORT_INVALID',
49
+ /** S5 — a recorded server could not be terminated. */
50
+ SERVE_CLOSE_FAILED: 'KSB_SERVE_CLOSE_FAILED',
51
+ /** F4 — a command module could not be loaded (broken or partial install). */
52
+ COMMAND_LOAD_FAILED: 'KSB_COMMAND_LOAD_FAILED',
53
+ /** F2a — a 24-grid placement that cannot exist on a 24-column grid. */
54
+ PLACEMENT_INVALID: 'KSB_PLACEMENT_INVALID',
55
+ /** F2a — a template naming a 文體 nobody registered. */
56
+ LAYOUT_NOT_FOUND: 'KSB_LAYOUT_NOT_FOUND',
57
+ /** F2a — canvas geometry that contradicts itself (measure wider than maxWidth). */
58
+ LAYOUT_CANVAS_INVALID: 'KSB_LAYOUT_CANVAS_INVALID',
59
+ /** F2a — a template filling a chrome slot its layout does not offer. */
60
+ LAYOUT_SLOT_UNKNOWN: 'KSB_LAYOUT_SLOT_UNKNOWN',
61
+ /** F2a — a template restating a root form its 文體 does not require. */
62
+ LAYOUT_ROOT_CONFLICT: 'KSB_LAYOUT_ROOT_CONFLICT',
63
+ /** F3 — a store template package that is incomplete or declares an illegal field. */
64
+ TEMPLATE_PACKAGE_INVALID: 'KSB_TEMPLATE_PACKAGE_INVALID',
65
+ /** F3 — `init` aimed at a package directory that already holds a package. */
66
+ TEMPLATE_PACKAGE_EXISTS: 'KSB_TEMPLATE_PACKAGE_EXISTS',
67
+ /**
68
+ * F3 — a package's JS extension (rotor override / plugin block) would not load
69
+ * or would not pass the same shape check the core registries apply.
70
+ *
71
+ * It gets its own code for the reason F4 B9 names: falling back to
72
+ * `KSB_PARSE_FAILED` would tell an agent its *document* is broken when the
73
+ * truth is a package on disk, and it would go rewrite a document that was
74
+ * never at fault.
75
+ */
76
+ TEMPLATE_EXTENSION_FAILED: 'KSB_TEMPLATE_EXTENSION_FAILED',
77
+ /** F3 — a store package squatting on a namespace the factory templates own. */
78
+ TEMPLATE_NAMESPACE_RESERVED: 'KSB_TEMPLATE_NAMESPACE_RESERVED',
79
+
80
+ /**
81
+ * F7b core — a `src` the browser would have to *fetch*.
82
+ *
83
+ * `KSB_EXTERNAL_IMAGE` only ever caught `src="http`, because CONTRACT A3's
84
+ * verbatim 禁形 list names that form. A **relative** `src="./fig.png"` breaks
85
+ * the same promise and passed every gate: the artifact renders on the machine
86
+ * that drew it and comes up broken everywhere else, which is the failure mode
87
+ * 「離線單檔」 exists to make impossible.
88
+ */
89
+ EXTERNAL_ASSET_SRC: 'KSB_EXTERNAL_ASSET_SRC',
90
+ /** F7b core — a `<script>` inside an `<svg>`: a figure that executes. */
91
+ SVG_SCRIPT: 'KSB_SVG_SCRIPT',
92
+ /** F7b core — an `on…=` event attribute inside an `<svg>`. */
93
+ SVG_EVENT_HANDLER: 'KSB_SVG_EVENT_HANDLER',
94
+ /** F7b core — a `javascript:` URL inside an `<svg>`. */
95
+ SVG_JAVASCRIPT_URL: 'KSB_SVG_JAVASCRIPT_URL',
96
+ /** F7b core — `rgba()` inside an `<svg>`: alpha the export chain drops. */
97
+ SVG_RGBA: 'KSB_SVG_RGBA',
98
+ /** F7b core — a marker reference no `<marker id>` answers (silent arrowhead loss). */
99
+ SVG_MARKER_DANGLING: 'KSB_SVG_MARKER_DANGLING',
100
+
101
+ /** F7b — a manifest `[gates]` section that declares something illegal. */
102
+ GATE_DECLARATION_INVALID: 'KSB_GATE_DECLARATION_INVALID',
103
+ /** F7b gate — body line-height above the ceiling the package declared. */
104
+ GATE_LINE_HEIGHT: 'KSB_GATE_LINE_HEIGHT',
105
+ /** F7b gate — a heading rule heavier than the package declared. */
106
+ GATE_HEADING_WEIGHT: 'KSB_GATE_HEADING_WEIGHT',
107
+ /** F7b gate — a box-shadow blurrier-than-declared-minimum (a hard shadow). */
108
+ GATE_SHADOW_BLUR: 'KSB_GATE_SHADOW_BLUR',
109
+ /** F7b gate — `font-style: italic` in a package that declared italic off. */
110
+ GATE_ITALIC: 'KSB_GATE_ITALIC',
111
+ /** F7b gate — a colour the package's own palette put on its banned list. */
112
+ GATE_BANNED_HEX: 'KSB_GATE_BANNED_HEX',
113
+ })
114
+
115
+ /** Root path token used when a finding cannot be tied to a single block. */
116
+ export const ROOT_PATH = '$'
117
+
118
+ export class KsbError extends Error {
119
+ constructor({ code, message, path = ROOT_PATH, exitCode = EXIT.VALIDATION }) {
120
+ super(message)
121
+ this.name = 'KsbError'
122
+ this.code = code
123
+ this.path = path
124
+ this.exitCode = exitCode
125
+ }
126
+
127
+ /** Serialisable finding, shape pinned by CONTRACT lint --json surface. */
128
+ toFinding() {
129
+ return { path: this.path, code: this.code, message: this.message }
130
+ }
131
+ }
132
+
133
+ export function usageError(message, code = CODES.USAGE) {
134
+ return new KsbError({ code, message, exitCode: EXIT.USAGE })
135
+ }
136
+
137
+ export function validationError(message, code, path = ROOT_PATH) {
138
+ return new KsbError({ code, message, path, exitCode: EXIT.VALIDATION })
139
+ }
140
+
141
+ /**
142
+ * A command module that would not load — a broken or partial install, never a
143
+ * problem with what the user typed.
144
+ *
145
+ * It gets its own code because the fallback, `KSB_PARSE_FAILED`, means "your
146
+ * input could not be parsed": an agent reading that after a half-installed
147
+ * package would go and rewrite a document that was never at fault. The
148
+ * resolver's own words are kept, since they name the missing file.
149
+ */
150
+ export function commandLoadError(command, cause) {
151
+ return new KsbError({
152
+ code: CODES.COMMAND_LOAD_FAILED,
153
+ message: `指令模組 \`${command}\` 載入失敗(安裝可能不完整):${cause?.message ?? String(cause)}`,
154
+ exitCode: EXIT.VALIDATION,
155
+ })
156
+ }
@@ -0,0 +1,109 @@
1
+ import { blockModule, blockTypes } from '../blocks/index.js'
2
+
3
+ /**
4
+ * The canonical Markdown-superset example. It must always be valid input to
5
+ * `render` — that round-trip is the SDK's anti-trial-and-error guarantee
6
+ * (SPEC §4「CLI 必須提供 example/schema,讓 Agent 免反覆試錯」).
7
+ *
8
+ * `doc` is the one kind with no block module: it is the envelope every block
9
+ * sits in, not a block. Every other example comes from its own module.
10
+ */
11
+ export const EXAMPLE_DOC = `---
12
+ title: 範例文件
13
+ kicker: KAMISHIBAI EXAMPLE
14
+ author: kamishibai
15
+ template: kami/long-form
16
+ ---
17
+
18
+ # 第一章 散文與引言
19
+
20
+ 這是 **prose** 區塊,支援行內 Markdown:粗體、\`行內程式碼\`、以及[連結](https://example.com/)。
21
+
22
+ > 這是 quote 區塊:引用一段話。
23
+
24
+ ## 第一節 程式碼、表格與提示框
25
+
26
+ \`\`\`js
27
+ export const hello = (name) => \`你好,\${name}\`
28
+ \`\`\`
29
+
30
+ | block | 用途 |
31
+ |-------|------|
32
+ | prose | 散文段落 |
33
+ | table | 表格資料 |
34
+
35
+ 清單是真正的 \`list\` block——項目內容是子 block,所以清單裡放 callout 不會拆掉外框:
36
+
37
+ - 項目一
38
+ - 項目二
39
+ :::note
40
+ 項目內的 callout 仍走同一個 Callout 元件。
41
+ :::
42
+ - 項目三
43
+
44
+ 1. 有序項目一
45
+ 2. 有序項目二
46
+
47
+ :::note
48
+ 這是 callout(note 型)。另有 warn 型。
49
+ :::
50
+
51
+ :::warn
52
+ 警示型 callout 長這樣。
53
+ :::
54
+
55
+ # 第二章 結構化圖表
56
+
57
+ 下面這個 diagram fence 的內容是 **JSON**,欄位就是 IR 的 diagram block;版面由 SDK
58
+ 算出,不必手畫座標。v1 只有一種 kind:有向的節點邊圖。
59
+
60
+ \`\`\`diagram
61
+ {
62
+ "kind": "graph",
63
+ "nodes": [
64
+ { "id": "input", "label": "來源語料" },
65
+ { "id": "ir", "label": "block tree" },
66
+ { "id": "artifact", "label": "離線產物" }
67
+ ],
68
+ "edges": [
69
+ { "from": "input", "to": "ir", "label": "parse" },
70
+ { "from": "ir", "to": "artifact", "label": "render" },
71
+ { "from": "artifact", "to": "ir", "label": "replay" }
72
+ ]
73
+ }
74
+ \`\`\`
75
+
76
+ # 第三章 raw 島嶼
77
+
78
+ 島嶼是逃生艙:模板敘述不出來的東西才手寫,並以 intent 記錄原因。
79
+
80
+ \`\`\`raw-html intent="示範 raw 島嶼的宣告方式"
81
+ <div class="example-island">手寫 HTML 島嶼</div>
82
+ \`\`\`
83
+ `
84
+
85
+ /** Re-exported for callers that want the deck teaching source by name. */
86
+ export { EXAMPLE_DECK } from '../blocks/deck.js'
87
+
88
+ /**
89
+ * `doc` plus every registered block type.
90
+ *
91
+ * A function, not a frozen array: the vocabulary grows when a plugin registers,
92
+ * and a list snapshotted at import would answer `unknown example kind "x-…"`
93
+ * for a block the renderer had just drawn.
94
+ */
95
+ export const exampleKinds = () => ['doc', ...blockTypes()]
96
+
97
+ /**
98
+ * @param {string} kind `doc`/`deck` for the Markdown superset, or a block type name
99
+ * @returns {{kind: string, example: string}|null} null when the kind is unknown
100
+ */
101
+ export function exampleFor(kind) {
102
+ if (kind === 'doc') return { kind, example: EXAMPLE_DOC }
103
+ const mod = blockModule(kind)
104
+ if (mod === undefined) return null
105
+ // A module may teach with a whole source file (the kinds that can be piped
106
+ // straight back into `render -`) or with one block, never both.
107
+ if (typeof mod.example.source === 'string') return { kind, example: mod.example.source }
108
+ return { kind, example: JSON.stringify(mod.example.block, null, 2) }
109
+ }
package/src/core/ir.js ADDED
@@ -0,0 +1,62 @@
1
+ import { engineVersion } from './version.js'
2
+
3
+ export const IR_VERSION = '1'
4
+ export const DEFAULT_GENERATOR = 'kamishibai-cli'
5
+ export const BUILD_TIME_ENV = 'KAMISHIBAI_BUILD_TIME'
6
+ /** SPEC §7.3 / issues/11 P4 — the embedding media type, verbatim. */
7
+ export const IR_SCRIPT_TYPE = 'application/kamishibai+json'
8
+
9
+ /**
10
+ * Deterministic build timestamp: KAMISHIBAI_BUILD_TIME wins so that repeated
11
+ * renders of the same input are byte-identical (CONTRACT A8).
12
+ */
13
+ export function resolveCreatedAt(env = {}, now = () => new Date()) {
14
+ const pinned = env[BUILD_TIME_ENV]
15
+ if (typeof pinned === 'string' && pinned.trim().length > 0) return pinned.trim()
16
+ return now().toISOString()
17
+ }
18
+
19
+ /**
20
+ * Timestamp for a replay. A pinned `KAMISHIBAI_BUILD_TIME` still wins, but the
21
+ * fallback is the *original* artifact's `createdAt` rather than "now": replay
22
+ * reproduces a document from its own embedded record, so an unpinned replay
23
+ * that stamped the current clock would silently break byte-identity (B6).
24
+ */
25
+ export function replayCreatedAt(env = {}, ir = {}) {
26
+ const pinned = env[BUILD_TIME_ENV]
27
+ if (typeof pinned === 'string' && pinned.trim().length > 0) return pinned.trim()
28
+ if (typeof ir.createdAt === 'string' && ir.createdAt.length > 0) return ir.createdAt
29
+ return resolveCreatedAt(env)
30
+ }
31
+
32
+ /** `<namespace>/<name>` — the template key an IR envelope was rendered with. */
33
+ export function templateKeyOf(ir) {
34
+ const t = ir?.template
35
+ if (!t || typeof t.namespace !== 'string' || typeof t.name !== 'string') return undefined
36
+ return `${t.namespace}/${t.name}`
37
+ }
38
+
39
+ /** `<namespace>/<name>@<version>` — the display form used by `list`. */
40
+ export function templateLabelOf(ir) {
41
+ const key = templateKeyOf(ir)
42
+ return key === undefined ? '' : `${key}@${ir.template.version}`
43
+ }
44
+
45
+ /**
46
+ * Assemble the embedded IR envelope (SPEC §7.3). Key order is fixed so that
47
+ * JSON.stringify output is stable.
48
+ */
49
+ export function buildIr({ doc, template, createdAt, generator = DEFAULT_GENERATOR }) {
50
+ return {
51
+ irVersion: IR_VERSION,
52
+ engine: engineVersion(),
53
+ template: {
54
+ namespace: template.namespace,
55
+ name: template.name,
56
+ version: template.version,
57
+ },
58
+ doc,
59
+ createdAt,
60
+ generator,
61
+ }
62
+ }