@cyber-dash-tech/revela 0.18.15 → 0.19.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.
Files changed (103) hide show
  1. package/README.md +48 -45
  2. package/README.zh-CN.md +48 -45
  3. package/assets/img/lucent-01.jpg +0 -0
  4. package/assets/img/lucent-02.jpg +0 -0
  5. package/assets/img/lucent-03.jpg +0 -0
  6. package/assets/img/lucent-dark-01.jpg +0 -0
  7. package/assets/img/lucent-dark-02.jpg +0 -0
  8. package/assets/img/lucent-dark-03.jpg +0 -0
  9. package/assets/img/monet-01.jpg +0 -0
  10. package/assets/img/monet-02.jpg +0 -0
  11. package/assets/img/monet-03.jpg +0 -0
  12. package/assets/img/starter-01.jpg +0 -0
  13. package/assets/img/starter-02.jpg +0 -0
  14. package/assets/img/starter-03.jpg +0 -0
  15. package/assets/img/summit-01.jpg +0 -0
  16. package/assets/img/summit-02.jpg +0 -0
  17. package/assets/img/summit-03.jpg +0 -0
  18. package/designs/lucent/DESIGN.md +108 -1
  19. package/designs/lucent/design.css +283 -0
  20. package/designs/lucent-dark/DESIGN.md +278 -0
  21. package/designs/lucent-dark/assets/card-lens.jpg +0 -0
  22. package/designs/lucent-dark/assets/closing-background.jpg +0 -0
  23. package/designs/lucent-dark/assets/cover-background.jpg +0 -0
  24. package/designs/lucent-dark/assets/report-visual.jpg +0 -0
  25. package/designs/lucent-dark/assets/soft-texture.jpg +0 -0
  26. package/designs/lucent-dark/assets/toc-orb.png +0 -0
  27. package/designs/lucent-dark/design.css +417 -0
  28. package/designs/monet/DESIGN.md +53 -9
  29. package/designs/monet/assets/card-lens.jpg +0 -0
  30. package/designs/monet/assets/closing-background.jpg +0 -0
  31. package/designs/monet/assets/cover-background.jpg +0 -0
  32. package/designs/monet/assets/report-visual.jpg +0 -0
  33. package/designs/monet/assets/soft-texture.jpg +0 -0
  34. package/designs/monet/assets/toc-orb.png +0 -0
  35. package/designs/monet/design.css +340 -0
  36. package/designs/starter/DESIGN.md +22 -5
  37. package/designs/starter/assets/card-lens.jpg +0 -0
  38. package/designs/starter/assets/closing-background.jpg +0 -0
  39. package/designs/starter/assets/cover-background.jpg +0 -0
  40. package/designs/starter/assets/report-visual.jpg +0 -0
  41. package/designs/starter/assets/soft-texture.jpg +0 -0
  42. package/designs/starter/assets/toc-orb.png +0 -0
  43. package/designs/starter/design.css +322 -0
  44. package/designs/summit/DESIGN.md +54 -9
  45. package/designs/summit/assets/card-lens.jpg +0 -0
  46. package/designs/summit/assets/closing-background.jpg +0 -0
  47. package/designs/summit/assets/cover-background.jpg +0 -0
  48. package/designs/summit/assets/report-visual.jpg +0 -0
  49. package/designs/summit/assets/soft-texture.jpg +0 -0
  50. package/designs/summit/assets/toc-orb.png +0 -0
  51. package/designs/summit/design.css +334 -0
  52. package/lib/commands/designs-new.ts +18 -21
  53. package/lib/commands/designs-preview.ts +3 -8
  54. package/lib/deck-html/foundation.ts +8 -8
  55. package/lib/design/designs.ts +385 -14
  56. package/lib/narrative-state/deck-plan-artifact.ts +40 -3
  57. package/lib/page-templates/built-in-preview.html +373 -0
  58. package/lib/page-templates/contracts.ts +2 -0
  59. package/lib/page-templates/css.ts +2 -0
  60. package/lib/page-templates/foundation.ts +41 -0
  61. package/lib/page-templates/index.ts +6 -0
  62. package/lib/page-templates/registry.ts +3 -0
  63. package/lib/page-templates/render.ts +1202 -0
  64. package/lib/page-templates/templates/agenda.ts +4 -0
  65. package/lib/page-templates/templates/chart-takeaways.ts +4 -0
  66. package/lib/page-templates/templates/claim-supporting-visual.ts +4 -0
  67. package/lib/page-templates/templates/closing.ts +4 -0
  68. package/lib/page-templates/templates/cover.ts +4 -0
  69. package/lib/page-templates/templates/executive-summary.ts +4 -0
  70. package/lib/page-templates/templates/index.ts +19 -0
  71. package/lib/page-templates/templates/key-message-evidence.ts +4 -0
  72. package/lib/page-templates/templates/metric-highlight.ts +4 -0
  73. package/lib/page-templates/templates/problem-context.ts +4 -0
  74. package/lib/page-templates/templates/process-steps.ts +4 -0
  75. package/lib/page-templates/templates/recommendation-decision.ts +4 -0
  76. package/lib/page-templates/templates/risks-tradeoffs.ts +4 -0
  77. package/lib/page-templates/templates/section-divider.ts +4 -0
  78. package/lib/page-templates/templates/shared.ts +11 -0
  79. package/lib/page-templates/templates/table-comparison.ts +4 -0
  80. package/lib/page-templates/templates/timeline-roadmap.ts +4 -0
  81. package/lib/page-templates/vocabulary.ts +158 -0
  82. package/lib/prompt-builder.ts +9 -5
  83. package/lib/qa/artifact.ts +117 -7
  84. package/lib/qa/checks.ts +1 -1
  85. package/lib/qa/compliance.ts +5 -1
  86. package/lib/qa/component-contracts.ts +90 -0
  87. package/lib/runtime/index.ts +99 -3
  88. package/package.json +7 -15
  89. package/plugins/revela/.codex-plugin/plugin.json +4 -4
  90. package/plugins/revela/hooks/revela_guard.ts +35 -0
  91. package/plugins/revela/hooks/revela_post_write_notice.ts +39 -9
  92. package/plugins/revela/mcp/revela-server.ts +103 -7
  93. package/plugins/revela/skills/revela/SKILL.md +3 -3
  94. package/plugins/revela/skills/revela-design/SKILL.md +25 -14
  95. package/plugins/revela/skills/revela-helper/SKILL.md +3 -3
  96. package/plugins/revela/skills/revela-make-deck/SKILL.md +27 -12
  97. package/plugins/revela/skills/revela-research/SKILL.md +1 -0
  98. package/skill/SKILL.md +11 -2
  99. package/designs/lucent/preview.html +0 -612
  100. package/designs/monet/preview.html +0 -2293
  101. package/designs/starter/preview.html +0 -314
  102. package/designs/summit/preview.html +0 -2284
  103. package/plugins/revela/skills/revela-review/SKILL.md +0 -46
@@ -3,7 +3,9 @@
3
3
  *
4
4
  * User designs are stored in ~/.config/revela/designs/<name>/.
5
5
  * Built-in designs are shipped read-only with this package under designs/<name>/.
6
- * Each design directory contains DESIGN.md (required) and optionally preview.html.
6
+ * Each design directory contains DESIGN.md (required), design.css for CSS-native styling,
7
+ * and optionally legacy preview.html. Current previews are generated from the built-in
8
+ * page-template fixture plus the design CSS.
7
9
  */
8
10
 
9
11
  import {
@@ -17,8 +19,9 @@ import {
17
19
  statSync,
18
20
  writeFileSync,
19
21
  } from "fs"
20
- import { dirname, join, resolve, basename, relative, sep } from "path"
22
+ import { dirname, join, normalize, resolve, basename, relative, sep } from "path"
21
23
  import { tmpdir } from "os"
24
+ import { pathToFileURL } from "url"
22
25
  import { parseFrontmatter } from "../frontmatter"
23
26
  import { collectDirectoryEntries, extractEntriesToDirectory, normalizePackageArchiveEntries, readTarArchive, writeTarArchive } from "./archive"
24
27
  import {
@@ -28,11 +31,14 @@ import {
28
31
  saveConfig,
29
32
  } from "../config"
30
33
  import { childLog } from "../log"
34
+ import { PAGE_TEMPLATE_CLASSES, listPageTemplateVocabulary, templateDeckCss } from "../page-templates"
31
35
 
32
36
  const designLog = childLog("designs")
33
37
 
34
38
  // Seed directory: built-in designs shipped with this package.
35
39
  const SEED_DIR = resolve(__dirname, "../..", "designs")
40
+ const BUILT_IN_PREVIEW_PATH = resolve(__dirname, "..", "page-templates", "built-in-preview.html")
41
+ const PREVIEW_FALLBACK_ASSET_DESIGN = "lucent"
36
42
 
37
43
  export interface DesignInfo {
38
44
  name: string
@@ -51,7 +57,8 @@ export interface CreateDesignPackageArgs {
51
57
  name: string
52
58
  base?: string
53
59
  designMd: string
54
- previewHtml: string
60
+ designCss?: string
61
+ previewHtml?: string
55
62
  assets?: DesignPackageAssetInput[]
56
63
  overwrite?: boolean
57
64
  }
@@ -85,6 +92,7 @@ export interface ValidateDesignPackageResult {
85
92
  name: string
86
93
  path: string
87
94
  hasDesignMd: boolean
95
+ hasDesignCss: boolean
88
96
  hasPreview: boolean
89
97
  hasMarkers: boolean
90
98
  sections: string[]
@@ -92,6 +100,7 @@ export interface ValidateDesignPackageResult {
92
100
  components: string[]
93
101
  assets: DesignPackageAssetInfo[]
94
102
  errors: string[]
103
+ warnings: string[]
95
104
  }
96
105
 
97
106
  export interface DesignPackageAssetInput {
@@ -143,6 +152,37 @@ export interface DesignPreviewInfo {
143
152
  hasPreview: boolean
144
153
  }
145
154
 
155
+ export interface MaterializeDesignPreviewArgs {
156
+ workspaceRoot: string
157
+ name: string
158
+ source?: "draft" | "installed" | "builtin"
159
+ }
160
+
161
+ export interface MaterializeDesignPreviewResult {
162
+ ok: true
163
+ name: string
164
+ source: "draft" | "installed" | "builtin"
165
+ designDir: string
166
+ previewDir: string
167
+ previewPath: string
168
+ previewUrl: string
169
+ designCssPath: string
170
+ files: string[]
171
+ warnings: string[]
172
+ }
173
+
174
+ export interface DesignCssSnapshotResult {
175
+ ok: true
176
+ design: string
177
+ sourcePath: string
178
+ snapshotDir: string
179
+ cssPath: string
180
+ href: string
181
+ assetCount: number
182
+ generatedFallback: boolean
183
+ warnings: string[]
184
+ }
185
+
146
186
  // ---------------------------------------------------------------------------
147
187
  // Seed
148
188
  // ---------------------------------------------------------------------------
@@ -299,6 +339,115 @@ export function resolveDesignPreview(name?: string): DesignPreviewInfo {
299
339
  }
300
340
  }
301
341
 
342
+ export function materializeDesignPreview(args: MaterializeDesignPreviewArgs): MaterializeDesignPreviewResult {
343
+ const name = normalizeDesignName(args.name)
344
+ const { designDir, source } = resolveDesignPreviewSourceDir(args.workspaceRoot, name, args.source)
345
+ const previewDir = resolve(args.workspaceRoot, ".revela", "previews", "designs", name)
346
+ const previewPath = join(previewDir, "preview.html")
347
+ const designCssPath = join(previewDir, "design.css")
348
+ const warnings: string[] = []
349
+
350
+ if (!existsSync(BUILT_IN_PREVIEW_PATH)) {
351
+ throw new Error(`Built-in design preview fixture is missing: ${BUILT_IN_PREVIEW_PATH}`)
352
+ }
353
+
354
+ rmSync(previewDir, { recursive: true, force: true })
355
+ mkdirSync(previewDir, { recursive: true })
356
+
357
+ const previewHtml = readFileSync(BUILT_IN_PREVIEW_PATH, "utf-8").replace(/data-design="built-in-preview"/g, `data-design="${name}"`)
358
+ writeFileSync(previewPath, previewHtml, "utf-8")
359
+
360
+ const cssRead = readDesignCssFromDir(name, designDir)
361
+ warnings.push(...cssRead.warnings)
362
+ writeFileSync(designCssPath, `${cssRead.css.trim()}\n`, "utf-8")
363
+
364
+ const sourceAssets = join(designDir, "assets")
365
+ const targetAssets = join(previewDir, "assets")
366
+ if (existsSync(sourceAssets)) cpSync(sourceAssets, targetAssets, { recursive: true })
367
+ copyMissingBuiltInPreviewAssets(previewHtml, targetAssets)
368
+
369
+ return {
370
+ ok: true,
371
+ name,
372
+ source,
373
+ designDir,
374
+ previewDir,
375
+ previewPath,
376
+ previewUrl: pathToFileURL(previewPath).href,
377
+ designCssPath,
378
+ files: listDesignPackageFiles(previewDir),
379
+ warnings,
380
+ }
381
+ }
382
+
383
+ export function resolveDesignPackageDir(name?: string): string {
384
+ const designName = normalizeDesignName(name || activeDesign())
385
+ const designDir = resolveDesignDir(designName)
386
+ if (!designDir) throw new Error(`Design '${designName}' is not installed`)
387
+ return designDir
388
+ }
389
+
390
+ export function readDesignCss(name?: string): { css: string; path?: string; generatedFallback: boolean; warnings: string[] } {
391
+ const designName = normalizeDesignName(name || activeDesign())
392
+ const designDir = resolveDesignPackageDir(designName)
393
+ const cssPath = join(designDir, "design.css")
394
+ if (existsSync(cssPath)) {
395
+ return { css: readFileSync(cssPath, "utf-8"), path: cssPath, generatedFallback: false, warnings: [] }
396
+ }
397
+
398
+ const foundation = getDesignSection("foundation", designName)
399
+ const cssBlocks = extractCssCodeBlocks(foundation)
400
+ if (cssBlocks.length === 0) throw new Error(`Design '${designName}' has no design.css and no CSS fallback in @design:foundation.`)
401
+ return {
402
+ css: [
403
+ "/* Generated compatibility CSS from DESIGN.md. Add design.css to make this design fully CSS-package native. */",
404
+ ...cssBlocks,
405
+ templateDeckCss({ designName, designAssetBasePath: "./assets" }),
406
+ ].join("\n\n"),
407
+ generatedFallback: true,
408
+ warnings: [`Design '${designName}' has no design.css; generated a deck-local compatibility CSS snapshot from DESIGN.md.`],
409
+ }
410
+ }
411
+
412
+ export function materializeDesignCssSnapshot(input: {
413
+ workspaceRoot: string
414
+ outputPath: string
415
+ designName?: string
416
+ }): DesignCssSnapshotResult {
417
+ const designName = normalizeDesignName(input.designName || activeDesign())
418
+ const designDir = resolveDesignPackageDir(designName)
419
+ const outputDir = dirname(normalize(input.outputPath))
420
+ const snapshotRelDir = normalize(join(outputDir, "_revela-design", designName)).replace(/\\/g, "/")
421
+ const snapshotDir = resolve(input.workspaceRoot, snapshotRelDir)
422
+ const cssPath = join(snapshotDir, "design.css")
423
+ const cssRead = readDesignCss(designName)
424
+
425
+ mkdirSync(snapshotDir, { recursive: true })
426
+ writeFileSync(cssPath, cssRead.css.endsWith("\n") ? cssRead.css : `${cssRead.css}\n`, "utf-8")
427
+
428
+ const assetsDir = join(designDir, "assets")
429
+ let assetCount = 0
430
+ if (existsSync(assetsDir) && statSync(assetsDir).isDirectory()) {
431
+ const targetAssets = join(snapshotDir, "assets")
432
+ rmSync(targetAssets, { recursive: true, force: true })
433
+ cpSync(assetsDir, targetAssets, { recursive: true })
434
+ assetCount = listDesignPackageFiles(targetAssets).length
435
+ }
436
+
437
+ const href = normalize(relative(outputDir || ".", normalize(join(snapshotRelDir, "design.css")))).replace(/\\/g, "/")
438
+ return {
439
+ ok: true,
440
+ design: designName,
441
+ sourcePath: cssRead.path || join(designDir, "DESIGN.md"),
442
+ snapshotDir,
443
+ cssPath,
444
+ href: href.startsWith(".") ? href : `./${href}`,
445
+ assetCount,
446
+ generatedFallback: cssRead.generatedFallback,
447
+ warnings: cssRead.warnings,
448
+ }
449
+ }
450
+
302
451
  /** Normalize and validate a design package name. */
303
452
  export function normalizeDesignName(name: string): string {
304
453
  const normalized = name.trim().toLowerCase()
@@ -312,10 +461,10 @@ export function normalizeDesignName(name: string): string {
312
461
  export function createDesignPackage(args: CreateDesignPackageArgs): CreateDesignPackageResult {
313
462
  const name = normalizeDesignName(args.name)
314
463
  const designMd = args.designMd?.trim()
464
+ const designCss = args.designCss?.trim()
315
465
  const previewHtml = args.previewHtml?.trim()
316
466
 
317
467
  if (!designMd) throw new Error("designMd is required")
318
- if (!previewHtml) throw new Error("previewHtml is required")
319
468
 
320
469
  const target = join(DESIGNS_DIR, name)
321
470
  const existed = existsSync(target)
@@ -329,7 +478,8 @@ export function createDesignPackage(args: CreateDesignPackageArgs): CreateDesign
329
478
  }
330
479
  mkdirSync(target, { recursive: true })
331
480
  writeFileSync(join(target, "DESIGN.md"), `${designMd}\n`, "utf-8")
332
- writeFileSync(join(target, "preview.html"), `${previewHtml}\n`, "utf-8")
481
+ if (designCss) writeFileSync(join(target, "design.css"), `${designCss}\n`, "utf-8")
482
+ if (previewHtml) writeFileSync(join(target, "preview.html"), `${previewHtml}\n`, "utf-8")
333
483
  writeDesignAssets(target, args.assets)
334
484
 
335
485
  const validation = validateDesignPackage(name)
@@ -352,10 +502,10 @@ export function createDesignPackage(args: CreateDesignPackageArgs): CreateDesign
352
502
  export function createDesignDraftPackage(args: CreateDesignDraftArgs): CreateDesignPackageResult {
353
503
  const name = normalizeDesignName(args.name)
354
504
  const designMd = args.designMd?.trim()
505
+ const designCss = args.designCss?.trim()
355
506
  const previewHtml = args.previewHtml?.trim()
356
507
 
357
508
  if (!designMd) throw new Error("designMd is required")
358
- if (!previewHtml) throw new Error("previewHtml is required")
359
509
 
360
510
  const target = designDraftDir(args.workspaceRoot, name)
361
511
  const existed = existsSync(target)
@@ -369,7 +519,8 @@ export function createDesignDraftPackage(args: CreateDesignDraftArgs): CreateDes
369
519
  }
370
520
  mkdirSync(target, { recursive: true })
371
521
  writeFileSync(join(target, "DESIGN.md"), `${designMd}\n`, "utf-8")
372
- writeFileSync(join(target, "preview.html"), `${previewHtml}\n`, "utf-8")
522
+ if (designCss) writeFileSync(join(target, "design.css"), `${designCss}\n`, "utf-8")
523
+ if (previewHtml) writeFileSync(join(target, "preview.html"), `${previewHtml}\n`, "utf-8")
373
524
  writeDesignAssets(target, args.assets)
374
525
 
375
526
  const validation = validateDesignDraftPackage(args.workspaceRoot, name)
@@ -551,6 +702,7 @@ export function validateDesignPackage(nameInput: string): ValidateDesignPackageR
551
702
  function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesignPackageResult {
552
703
  let name = nameInput
553
704
  const errors: string[] = []
705
+ const warnings: string[] = []
554
706
  try {
555
707
  name = normalizeDesignName(nameInput)
556
708
  } catch (e) {
@@ -558,8 +710,10 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
558
710
  }
559
711
 
560
712
  const mdPath = join(dir, "DESIGN.md")
713
+ const cssPath = join(dir, "design.css")
561
714
  const previewPath = join(dir, "preview.html")
562
715
  const hasDesignMd = existsSync(mdPath)
716
+ const hasDesignCss = existsSync(cssPath)
563
717
  const hasPreview = existsSync(previewPath)
564
718
  let hasMarkers = false
565
719
  let sections: string[] = []
@@ -569,7 +723,7 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
569
723
 
570
724
  if (!existsSync(dir)) errors.push(`Design directory does not exist: ${dir}`)
571
725
  if (!hasDesignMd) errors.push("DESIGN.md is missing")
572
- if (!hasPreview) errors.push("preview.html is missing")
726
+ if (!hasDesignCss) warnings.push("design.css is missing; DESIGN.md CSS fallback remains compatibility-only and should be migrated.")
573
727
  if (existsSync(dir)) assets = listDesignAssetsInDir(dir)
574
728
 
575
729
  if (hasDesignMd) {
@@ -593,11 +747,13 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
593
747
 
594
748
  if (hasPreview) {
595
749
  const preview = readFileSync(previewPath, "utf-8")
750
+ const designCss = hasDesignCss ? readFileSync(cssPath, "utf-8") : ""
596
751
  if (!preview.includes('<section class="slide"')) errors.push("preview.html must include slide sections")
597
752
  if (!preview.includes("slide-qa=")) errors.push("preview.html slides must include slide-qa attributes")
598
753
  if (!preview.includes("slide-canvas")) errors.push("preview.html must include .slide-canvas")
599
- if (!hasFixedSizeCssRule(preview, "slide-canvas")) {
600
- errors.push("preview.html must define .slide-canvas CSS with width: 1920px and height: 1080px")
754
+ if (hasDesignCss && !previewReferencesDesignCss(preview)) errors.push("preview.html must reference design.css")
755
+ if (!hasFixedSizeCssRule(`${preview}\n${designCss}`, "slide-canvas")) {
756
+ errors.push("preview.html or design.css must define .slide-canvas CSS with width: 1920px and height: 1080px")
601
757
  }
602
758
  if (!hasSlideRole(preview, "cover")) errors.push('preview.html must include a slide section with data-slide-role="cover"')
603
759
  if (!hasSlideRole(preview, "closing")) errors.push('preview.html must include a slide section with data-slide-role="closing"')
@@ -605,13 +761,28 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
605
761
  if (missingComponents.length > 0) {
606
762
  errors.push(`preview.html must showcase every @component; missing: ${missingComponents.join(", ")}`)
607
763
  }
764
+ const missingLayoutPreviews = layouts.filter((layout) => !hasDataAttribute(preview, "data-preview-layout", layout))
765
+ if (missingLayoutPreviews.length > 0) {
766
+ warnings.push(`preview.html should mark layout fixtures with data-preview-layout; missing: ${missingLayoutPreviews.join(", ")}`)
767
+ }
608
768
  }
769
+ if (hasDesignCss) {
770
+ const designCss = readFileSync(cssPath, "utf-8")
771
+ const missingCoreClasses = requiredDesignCssClasses().filter((className) => !cssTextHasClassSelector(designCss, className))
772
+ if (missingCoreClasses.length > 0) errors.push(`design.css must style core template classes; missing: ${missingCoreClasses.map((item) => `.${item}`).join(", ")}`)
773
+ errors.push(...missingDesignCssAssetErrors(designCss, dir))
774
+ }
775
+ const designText = hasDesignMd ? readFileSync(mdPath, "utf-8") : ""
776
+ const designCss = hasDesignCss ? readFileSync(cssPath, "utf-8") : ""
777
+ const tokenWarnings = designContractTokenWarnings(`${designText}\n${designCss}`)
778
+ warnings.push(...tokenWarnings)
609
779
 
610
780
  return {
611
781
  ok: errors.length === 0,
612
782
  name,
613
783
  path: dir,
614
784
  hasDesignMd,
785
+ hasDesignCss,
615
786
  hasPreview,
616
787
  hasMarkers,
617
788
  sections,
@@ -619,13 +790,141 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
619
790
  components,
620
791
  assets,
621
792
  errors,
793
+ warnings,
794
+ }
795
+ }
796
+
797
+ function designContractTokenWarnings(text: string): string[] {
798
+ const warnings: string[] = []
799
+ const checks = [
800
+ { label: "grid", pattern: /--grid-|grid columns|grid-column|column line|safe area|safe-area/i },
801
+ { label: "spacing", pattern: /--space-|spacing scale|baseline|rhythm unit|gap scale/i },
802
+ { label: "type scale", pattern: /--font-size-|type scale|typographic scale|line-height/i },
803
+ { label: "surface", pattern: /--surface|surface token|border token|shadow token/i },
804
+ ]
805
+ for (const check of checks) {
806
+ if (!check.pattern.test(text)) warnings.push(`DESIGN.md/design.css should document ${check.label} design tokens or an equivalent contract`)
622
807
  }
808
+ return warnings
809
+ }
810
+
811
+ function previewReferencesDesignCss(preview: string): boolean {
812
+ return /<link\b[^>]*href=["'][^"']*design\.css["'][^>]*>/i.test(preview)
813
+ }
814
+
815
+ function requiredDesignCssClasses(): string[] {
816
+ return [
817
+ "slide-canvas",
818
+ "template-slide",
819
+ "template-frame",
820
+ "template-title",
821
+ "template-card",
822
+ "template-visual-slot-panel",
823
+ ]
824
+ }
825
+
826
+ function cssTextHasClassSelector(css: string, className: string): boolean {
827
+ const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, "")
828
+ const ruleRe = /([^{}]+)\{[^{}]*\}/g
829
+ let match: RegExpExecArray | null
830
+ while ((match = ruleRe.exec(withoutComments)) !== null) {
831
+ if (cssRuleHasClassSelector(match[1] ?? "", className)) return true
832
+ }
833
+ return false
834
+ }
835
+
836
+ function missingDesignCssAssetErrors(css: string, designDir: string): string[] {
837
+ const errors: string[] = []
838
+ const seen = new Set<string>()
839
+ const urlRe = /url\(\s*["']?([^"')]+)["']?\s*\)/gi
840
+ let match: RegExpExecArray | null
841
+ while ((match = urlRe.exec(css)) !== null) {
842
+ const raw = match[1].trim()
843
+ if (!raw || raw.startsWith("data:") || /^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.startsWith("#")) continue
844
+ const normalized = raw.replace(/^\.\/+/, "")
845
+ if (!normalized.startsWith("assets/")) continue
846
+ if (seen.has(normalized)) continue
847
+ seen.add(normalized)
848
+ if (!existsSync(join(designDir, ...normalized.split("/")))) errors.push(`design.css references missing asset: ${normalized}`)
849
+ }
850
+ return errors
851
+ }
852
+
853
+ function extractCssCodeBlocks(markdown: string): string[] {
854
+ const blocks: string[] = []
855
+ const fenceRe = /```css\n([\s\S]*?)```/g
856
+ let match: RegExpExecArray | null
857
+ while ((match = fenceRe.exec(markdown)) !== null) {
858
+ const body = match[1].trim()
859
+ if (body) blocks.push(body)
860
+ }
861
+ return blocks
623
862
  }
624
863
 
625
864
  function designDraftDir(workspaceRoot: string, name: string): string {
626
865
  return resolve(workspaceRoot, ".revela", "drafts", "designs", name)
627
866
  }
628
867
 
868
+ function resolveDesignPreviewSourceDir(
869
+ workspaceRoot: string,
870
+ name: string,
871
+ requested?: "draft" | "installed" | "builtin",
872
+ ): { designDir: string; source: "draft" | "installed" | "builtin" } {
873
+ const draftDir = designDraftDir(workspaceRoot, name)
874
+ if (requested === "draft") {
875
+ if (!existsSync(draftDir)) throw new Error(`Design draft '${name}' does not exist: ${draftDir}`)
876
+ return { designDir: draftDir, source: "draft" }
877
+ }
878
+
879
+ const installedDir = resolveDesignDir(name)
880
+ if (requested === "installed" || requested === "builtin") {
881
+ if (!installedDir) throw new Error(`Design '${name}' is not installed`)
882
+ const source = requested === "builtin" ? "builtin" : installedDir.startsWith(SEED_DIR + sep) ? "builtin" : "installed"
883
+ return { designDir: installedDir, source }
884
+ }
885
+
886
+ if (existsSync(draftDir)) return { designDir: draftDir, source: "draft" }
887
+ if (!installedDir) throw new Error(`Design '${name}' is not available as a workspace draft or installed design`)
888
+ return { designDir: installedDir, source: installedDir.startsWith(SEED_DIR + sep) ? "builtin" : "installed" }
889
+ }
890
+
891
+ function readDesignCssFromDir(name: string, designDir: string): { css: string; warnings: string[] } {
892
+ const cssPath = join(designDir, "design.css")
893
+ if (existsSync(cssPath)) return { css: readFileSync(cssPath, "utf-8"), warnings: [] }
894
+
895
+ const mdPath = join(designDir, "DESIGN.md")
896
+ const markdown = existsSync(mdPath) ? readFileSync(mdPath, "utf-8") : ""
897
+ const cssBlocks = extractCssCodeBlocks(markdown)
898
+ const css = [
899
+ "/* Generated preview compatibility CSS from DESIGN.md. Add design.css to make this design CSS-native. */",
900
+ templateDeckCss(),
901
+ ...cssBlocks,
902
+ ].join("\n\n")
903
+ return {
904
+ css,
905
+ warnings: [`Design '${name}' has no design.css; generated preview CSS from DESIGN.md compatibility blocks.`],
906
+ }
907
+ }
908
+
909
+ function copyMissingBuiltInPreviewAssets(previewHtml: string, targetAssetsDir: string): void {
910
+ const fallbackAssetsDir = join(SEED_DIR, PREVIEW_FALLBACK_ASSET_DESIGN, "assets")
911
+ const assetRefs = new Set<string>()
912
+ const re = /(?:src|href)=["'](?:\.\/)?assets\/([^"']+)["']/g
913
+ let match: RegExpExecArray | null
914
+ while ((match = re.exec(previewHtml)) !== null) assetRefs.add(match[1])
915
+ if (assetRefs.size === 0) return
916
+
917
+ mkdirSync(targetAssetsDir, { recursive: true })
918
+ for (const rel of assetRefs) {
919
+ const target = resolve(targetAssetsDir, rel)
920
+ if (existsSync(target)) continue
921
+ const source = resolve(fallbackAssetsDir, rel)
922
+ if (!existsSync(source) || !statSync(source).isFile()) continue
923
+ mkdirSync(dirname(target), { recursive: true })
924
+ cpSync(source, target)
925
+ }
926
+ }
927
+
629
928
  function designDraftExists(workspaceRoot: string, name: string): boolean {
630
929
  const dir = designDraftDir(workspaceRoot, name)
631
930
  return existsSync(dir) && statSync(dir).isDirectory() && existsSync(join(dir, "DESIGN.md"))
@@ -764,6 +1063,23 @@ export interface DesignInventoryComponent {
764
1063
  acceptsChildren: boolean
765
1064
  allowedChildren?: string[]
766
1065
  }
1066
+ contract?: DesignComponentContract
1067
+ }
1068
+
1069
+ export interface DesignComponentContractVariant {
1070
+ name: string
1071
+ requiredDescendantClasses: string[]
1072
+ repeatedItemClass?: string
1073
+ requiredItemClasses?: string[]
1074
+ requireAlternatingClasses?: string[]
1075
+ }
1076
+
1077
+ export interface DesignComponentContract {
1078
+ component: string
1079
+ kind: "structure"
1080
+ requiredRootClasses: string[]
1081
+ variants: DesignComponentContractVariant[]
1082
+ guidance: string
767
1083
  }
768
1084
 
769
1085
  export interface DesignInventory {
@@ -771,6 +1087,12 @@ export interface DesignInventory {
771
1087
  sections: string[]
772
1088
  layouts: DesignInventoryLayout[]
773
1089
  components: DesignInventoryComponent[]
1090
+ pageTemplates: Array<{
1091
+ templateId: string
1092
+ slots: string[]
1093
+ requiredClasses: string[]
1094
+ contractNotes: string[]
1095
+ }>
774
1096
  assets: DesignPackageAssetInfo[]
775
1097
  hasMarkers: boolean
776
1098
  }
@@ -838,17 +1160,19 @@ export function generateComponentIndex(components: Record<string, string>): stri
838
1160
  const desc = firstLine
839
1161
  ? firstLine.replace(/^#+\s*/, "").replace(/\(.*?\)/, "").trim()
840
1162
  : ""
841
- return `| \`${name}\` | ${desc} |`
1163
+ const contract = inferComponentContract(name, body)
1164
+ const contractLabel = contract ? "✓" : "—"
1165
+ return `| \`${name}\` | ${contractLabel} | ${desc} |`
842
1166
  })
843
1167
 
844
1168
  return [
845
1169
  "### Component Index",
846
1170
  "",
847
- "| Component | Description |",
848
- "|---|---|",
1171
+ "| Component | Contract | Description |",
1172
+ "|---|---|---|",
849
1173
  ...rows,
850
1174
  "",
851
- "_Use `revela_design_read_component` with `component: \"<name>\"` to get full CSS/HTML for any component._",
1175
+ "_Use `revela_design_read_component` with `component: \"<name>\"` to get full CSS/HTML and any structure contract for a component._",
852
1176
  ].join("\n")
853
1177
  }
854
1178
 
@@ -908,12 +1232,55 @@ export function getDesignInventory(designName?: string): DesignInventory {
908
1232
  name: componentName,
909
1233
  description: designBlockDescription(content),
910
1234
  nesting: inferComponentNesting(componentName),
1235
+ contract: inferComponentContract(componentName, content),
1236
+ })),
1237
+ pageTemplates: listPageTemplateVocabulary().map((template) => ({
1238
+ templateId: template.templateId,
1239
+ slots: template.slots.map((slot) => slot.name),
1240
+ requiredClasses: template.requiredClasses,
1241
+ contractNotes: template.contractNotes,
911
1242
  })),
912
1243
  assets: listDesignAssetsInDir(designDir),
913
1244
  hasMarkers,
914
1245
  }
915
1246
  }
916
1247
 
1248
+ export function extractDesignComponentContracts(designName?: string): DesignComponentContract[] {
1249
+ const name = normalizeDesignName(designName || activeDesign())
1250
+ const designDir = resolveDesignDir(name)
1251
+ if (!designDir) return []
1252
+ const mdPath = join(designDir, "DESIGN.md")
1253
+ const text = readFileSync(mdPath, "utf-8")
1254
+ const { body } = parseFrontmatter(text)
1255
+ const { components, hasMarkers } = parseDesignSections(body)
1256
+ if (!hasMarkers) return []
1257
+ return Object.entries(components)
1258
+ .map(([componentName, content]) => inferComponentContract(componentName, content))
1259
+ .filter((contract): contract is DesignComponentContract => Boolean(contract))
1260
+ }
1261
+
1262
+ function inferComponentContract(name: string, content: string): DesignComponentContract | undefined {
1263
+ const mentions = (value: string) => content.includes(value)
1264
+
1265
+ if (name === "roadmap-vertical" && ["tjv-axis", "tjv-item", "tjv-axis-dot", "tjv-stem", "tjv-tip-dot", "tjv-label"].every(mentions)) {
1266
+ return {
1267
+ component: name,
1268
+ kind: "structure",
1269
+ requiredRootClasses: ["roadmap-vertical"],
1270
+ variants: [{
1271
+ name: "timeline-journey-vertical",
1272
+ requiredDescendantClasses: ["tjv-axis"],
1273
+ repeatedItemClass: "tjv-item",
1274
+ requiredItemClasses: ["tjv-axis-dot", "tjv-stem", "tjv-tip-dot", "tjv-label"],
1275
+ requireAlternatingClasses: ["tjv-item--left", "tjv-item--right"],
1276
+ }],
1277
+ guidance: "Use structured milestone props/content to render axis-dot -> stem -> tip-dot -> label rows. Do not hand-roll a simplified axis plus labels.",
1278
+ }
1279
+ }
1280
+
1281
+ return undefined
1282
+ }
1283
+
917
1284
  function inferLayoutSlots(name: string, content: string): string[] {
918
1285
  const slots = new Set<string>()
919
1286
  const known = ["fullbleed", "left", "right", "top", "bottom", "main", "footer", "content", "overlay", "center"]
@@ -1145,6 +1512,7 @@ const UNIVERSAL_CLASSES = new Set([
1145
1512
  "overlay",
1146
1513
  "alt",
1147
1514
  "strong",
1515
+ ...PAGE_TEMPLATE_CLASSES,
1148
1516
  ])
1149
1517
 
1150
1518
  /**
@@ -1280,6 +1648,9 @@ export function extractDesignClasses(designName?: string): DesignClassVocabulary
1280
1648
  extractFromSection(content)
1281
1649
  }
1282
1650
 
1651
+ const cssPath = join(designDir, "design.css")
1652
+ if (existsSync(cssPath)) extractFromCss(readFileSync(cssPath, "utf-8"))
1653
+
1283
1654
  return { classes, prefixExemptions: DEFAULT_PREFIX_EXEMPTIONS }
1284
1655
  }
1285
1656