@cyber-dash-tech/revela 0.18.15 → 0.18.16

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.
@@ -74,6 +74,9 @@ const VISUAL_QUALITY_RULES = `Visual extraction and CSS quality rules:
74
74
  - Preserve composition, not just colors and shapes. If the reference is a bottom strip, compact badge, side rail, or sparse header mark, keep that scale and anchoring; do not enlarge it into a full-slide mascot or background unless requested.
75
75
  - Do not rewrite the entire base layout system from scratch. Preserve base layout/container CSS where possible; mainly change tokens, typography, component skins, and small decorative components.
76
76
  - Keep CSS scoped and boring. Prefer CSS variables and reusable classes over many one-off absolute-positioned selectors.
77
+ - Treat the design as an executable visual system, not a mood board. Document grid, safe-area, spacing, type scale, surface, and chart tokens in \`@design:foundation\`.
78
+ - Layouts must use declared slots and stable grid/flex containers. Do not fake alignment with scattered one-off absolute positioning when a layout grid should own placement.
79
+ - Components must declare normal, dense, and long-copy behavior when relevant. Chart, table, media, and source-note components need stable container dimensions.
77
80
  - If a reference is flat vector, doodle, mascot, blob, line-art, or geometric illustration, prefer a self-contained SVG component with a fixed viewBox. CSS should place the SVG; the SVG should draw the motif.
78
81
  - If a reference is photography, UI screenshot, webpage, or product surface, do not convert it to SVG. Extract palette, type scale, spacing, layout rhythm, borders, and image treatment instead.
79
82
  - For SVG motifs: set a viewBox, keep all eyes/mouths/decorations inside that coordinate system, and document intended placement/scale in the component notes.
@@ -83,6 +86,8 @@ const PREVIEW_REQUIREMENTS = `Preview requirements:
83
86
  - \`preview.html\` must include a cover slide and a closing slide. Mark their \`<section class="slide">\` elements with \`data-slide-role="cover"\` and \`data-slide-role="closing"\`.
84
87
  - \`preview.html\` must define an explicit CSS rule for \`.slide-canvas\` with \`width: 1920px\` and \`height: 1080px\`; every direct \`.slide-canvas\` is the fixed 1920px x 1080px export surface.
85
88
  - \`preview.html\` must showcase every \`@component:*\` defined in \`DESIGN.md\`. Mark each showcased component with \`data-preview-component="<component-name>"\`.
89
+ - \`preview.html\` should showcase every \`@layout:*\` defined in \`DESIGN.md\`. Mark each layout fixture with \`data-preview-layout="<layout-name>"\`.
90
+ - Preview slides should act as test fixtures: include at least one normal content slide, one dense content slide, one chart/table or data slide when supported, mixed Chinese/English copy when relevant, and visible source-note behavior.
86
91
  - Do not save with \`revela-designs-author\` until every component has a corresponding preview marker. If a component is decorative or abstract, include a visible labeled sample state.
87
92
  - When the design supports chart styling, \`preview.html\` should include a 3x3 ECharts gallery with at least 9 chart examples. This is a preview quality requirement, not a validation blocker.`
88
93
 
@@ -133,11 +138,13 @@ Hard requirements:
133
138
  - \`DESIGN.md\` must include frontmatter with name, description, author, and version.
134
139
  - \`DESIGN.md\` must include valid \`@design\`, \`@layout\`, and \`@component\` markers.
135
140
  - \`DESIGN.md\` must include at least \`@design:foundation\`, \`@design:rules\`, one layout, and one component.
141
+ - \`DESIGN.md\` should document the design contract: grid/safe-area, spacing scale, typography scale, surfaces, and chart tokens when charts are supported.
136
142
  - \`preview.html\` must be self-contained and directly openable in a browser.
137
143
  - \`preview.html\` must include an explicit CSS rule: \`.slide-canvas { width: 1920px; height: 1080px; }\`.
138
144
  - Every preview slide must include \`slide-qa="true"\` or \`slide-qa="false"\`.
139
145
  - \`preview.html\` must include \`data-slide-role="cover"\` and \`data-slide-role="closing"\` on slide sections.
140
146
  - \`preview.html\` must showcase every \`@component:*\` with \`data-preview-component="<component-name>"\` before saving.
147
+ - \`preview.html\` should showcase every \`@layout:*\` with \`data-preview-layout="<layout-name>"\`.
141
148
  - Do not save anything until the user confirms the brief.
142
149
 
143
150
  Start now by interviewing the user. Keep the first question concise.`
@@ -173,11 +180,13 @@ Workflow:
173
180
  Hard requirements:
174
181
  - Preserve valid frontmatter and marker structure.
175
182
  - Preserve at least \`@design:foundation\`, \`@design:rules\`, one layout, and one component.
183
+ - Preserve or add the design contract: grid/safe-area, spacing scale, typography scale, surfaces, and chart tokens when charts are supported.
176
184
  - \`preview.html\` must be self-contained and directly openable in a browser.
177
185
  - \`preview.html\` must include an explicit CSS rule: \`.slide-canvas { width: 1920px; height: 1080px; }\`.
178
186
  - Every preview slide must include \`slide-qa="true"\` or \`slide-qa="false"\`.
179
187
  - \`preview.html\` must include \`data-slide-role="cover"\` and \`data-slide-role="closing"\` on slide sections.
180
188
  - \`preview.html\` must showcase every \`@component:*\` with \`data-preview-component="<component-name>"\` before saving.
189
+ - \`preview.html\` should showcase every \`@layout:*\` with \`data-preview-layout="<layout-name>"\`.
181
190
  - Do not save anything until the user confirms the edit brief.
182
191
 
183
192
  Start now by asking what the user wants to change in \`${name}\`.`
@@ -92,6 +92,7 @@ export interface ValidateDesignPackageResult {
92
92
  components: string[]
93
93
  assets: DesignPackageAssetInfo[]
94
94
  errors: string[]
95
+ warnings: string[]
95
96
  }
96
97
 
97
98
  export interface DesignPackageAssetInput {
@@ -551,6 +552,7 @@ export function validateDesignPackage(nameInput: string): ValidateDesignPackageR
551
552
  function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesignPackageResult {
552
553
  let name = nameInput
553
554
  const errors: string[] = []
555
+ const warnings: string[] = []
554
556
  try {
555
557
  name = normalizeDesignName(nameInput)
556
558
  } catch (e) {
@@ -605,6 +607,13 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
605
607
  if (missingComponents.length > 0) {
606
608
  errors.push(`preview.html must showcase every @component; missing: ${missingComponents.join(", ")}`)
607
609
  }
610
+ const missingLayoutPreviews = layouts.filter((layout) => !hasDataAttribute(preview, "data-preview-layout", layout))
611
+ if (missingLayoutPreviews.length > 0) {
612
+ warnings.push(`preview.html should mark layout fixtures with data-preview-layout; missing: ${missingLayoutPreviews.join(", ")}`)
613
+ }
614
+ const designText = hasDesignMd ? readFileSync(mdPath, "utf-8") : ""
615
+ const tokenWarnings = designContractTokenWarnings(`${designText}\n${preview}`)
616
+ warnings.push(...tokenWarnings)
608
617
  }
609
618
 
610
619
  return {
@@ -619,7 +628,22 @@ function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesign
619
628
  components,
620
629
  assets,
621
630
  errors,
631
+ warnings,
632
+ }
633
+ }
634
+
635
+ function designContractTokenWarnings(text: string): string[] {
636
+ const warnings: string[] = []
637
+ const checks = [
638
+ { label: "grid", pattern: /--grid-|grid columns|grid-column|column line|safe area|safe-area/i },
639
+ { label: "spacing", pattern: /--space-|spacing scale|baseline|rhythm unit|gap scale/i },
640
+ { label: "type scale", pattern: /--font-size-|type scale|typographic scale|line-height/i },
641
+ { label: "surface", pattern: /--surface|surface token|border token|shadow token/i },
642
+ ]
643
+ for (const check of checks) {
644
+ if (!check.pattern.test(text)) warnings.push(`DESIGN.md/preview.html should document ${check.label} design tokens or an equivalent contract`)
622
645
  }
646
+ return warnings
623
647
  }
624
648
 
625
649
  function designDraftDir(workspaceRoot: string, name: string): string {
@@ -764,6 +788,23 @@ export interface DesignInventoryComponent {
764
788
  acceptsChildren: boolean
765
789
  allowedChildren?: string[]
766
790
  }
791
+ contract?: DesignComponentContract
792
+ }
793
+
794
+ export interface DesignComponentContractVariant {
795
+ name: string
796
+ requiredDescendantClasses: string[]
797
+ repeatedItemClass?: string
798
+ requiredItemClasses?: string[]
799
+ requireAlternatingClasses?: string[]
800
+ }
801
+
802
+ export interface DesignComponentContract {
803
+ component: string
804
+ kind: "structure"
805
+ requiredRootClasses: string[]
806
+ variants: DesignComponentContractVariant[]
807
+ guidance: string
767
808
  }
768
809
 
769
810
  export interface DesignInventory {
@@ -838,17 +879,19 @@ export function generateComponentIndex(components: Record<string, string>): stri
838
879
  const desc = firstLine
839
880
  ? firstLine.replace(/^#+\s*/, "").replace(/\(.*?\)/, "").trim()
840
881
  : ""
841
- return `| \`${name}\` | ${desc} |`
882
+ const contract = inferComponentContract(name, body)
883
+ const contractLabel = contract ? "✓" : "—"
884
+ return `| \`${name}\` | ${contractLabel} | ${desc} |`
842
885
  })
843
886
 
844
887
  return [
845
888
  "### Component Index",
846
889
  "",
847
- "| Component | Description |",
848
- "|---|---|",
890
+ "| Component | Contract | Description |",
891
+ "|---|---|---|",
849
892
  ...rows,
850
893
  "",
851
- "_Use `revela_design_read_component` with `component: \"<name>\"` to get full CSS/HTML for any component._",
894
+ "_Use `revela_design_read_component` with `component: \"<name>\"` to get full CSS/HTML and any structure contract for a component._",
852
895
  ].join("\n")
853
896
  }
854
897
 
@@ -908,12 +951,49 @@ export function getDesignInventory(designName?: string): DesignInventory {
908
951
  name: componentName,
909
952
  description: designBlockDescription(content),
910
953
  nesting: inferComponentNesting(componentName),
954
+ contract: inferComponentContract(componentName, content),
911
955
  })),
912
956
  assets: listDesignAssetsInDir(designDir),
913
957
  hasMarkers,
914
958
  }
915
959
  }
916
960
 
961
+ export function extractDesignComponentContracts(designName?: string): DesignComponentContract[] {
962
+ const name = normalizeDesignName(designName || activeDesign())
963
+ const designDir = resolveDesignDir(name)
964
+ if (!designDir) return []
965
+ const mdPath = join(designDir, "DESIGN.md")
966
+ const text = readFileSync(mdPath, "utf-8")
967
+ const { body } = parseFrontmatter(text)
968
+ const { components, hasMarkers } = parseDesignSections(body)
969
+ if (!hasMarkers) return []
970
+ return Object.entries(components)
971
+ .map(([componentName, content]) => inferComponentContract(componentName, content))
972
+ .filter((contract): contract is DesignComponentContract => Boolean(contract))
973
+ }
974
+
975
+ function inferComponentContract(name: string, content: string): DesignComponentContract | undefined {
976
+ const mentions = (value: string) => content.includes(value)
977
+
978
+ if (name === "roadmap-vertical" && ["tjv-axis", "tjv-item", "tjv-axis-dot", "tjv-stem", "tjv-tip-dot", "tjv-label"].every(mentions)) {
979
+ return {
980
+ component: name,
981
+ kind: "structure",
982
+ requiredRootClasses: ["roadmap-vertical"],
983
+ variants: [{
984
+ name: "timeline-journey-vertical",
985
+ requiredDescendantClasses: ["tjv-axis"],
986
+ repeatedItemClass: "tjv-item",
987
+ requiredItemClasses: ["tjv-axis-dot", "tjv-stem", "tjv-tip-dot", "tjv-label"],
988
+ requireAlternatingClasses: ["tjv-item--left", "tjv-item--right"],
989
+ }],
990
+ guidance: "Use structured milestone props/content to render axis-dot -> stem -> tip-dot -> label rows. Do not hand-roll a simplified axis plus labels.",
991
+ }
992
+ }
993
+
994
+ return undefined
995
+ }
996
+
917
997
  function inferLayoutSlots(name: string, content: string): string[] {
918
998
  const slots = new Set<string>()
919
999
  const known = ["fullbleed", "left", "right", "top", "bottom", "main", "footer", "content", "overlay", "center"]
@@ -190,6 +190,10 @@ function buildDesignLayer(designName: string): string {
190
190
  const componentIndex = generateComponentIndex(components)
191
191
  if (componentIndex) {
192
192
  layerParts.push(componentIndex)
193
+ layerParts.push([
194
+ "Components marked `✓` in the Contract column have required internal structure.",
195
+ "Fetch the component details before using them and preserve the required DOM/classes instead of hand-rolling a simpler lookalike.",
196
+ ].join(" "))
193
197
  }
194
198
 
195
199
  // 5. On-demand note
@@ -1,8 +1,11 @@
1
1
  import { formatDeckHtmlContractReport, validateDeckHtmlContract } from "../deck-html/contract"
2
- import type { DesignClassVocabulary } from "../design/designs"
2
+ import { activeDesign, extractDesignComponentContracts } from "../design/designs"
3
+ import type { DesignClassVocabulary, DesignComponentContract } from "../design/designs"
3
4
  import { formatReport, runQA } from "./index"
4
5
  import { runComplianceQA } from "./compliance"
6
+ import { runComponentContractQA } from "./component-contracts"
5
7
  import type { QAReport } from "./checks"
8
+ import { basename, dirname } from "path"
6
9
 
7
10
  export interface ArtifactQAReport {
8
11
  file: string
@@ -24,6 +27,7 @@ export async function runArtifactQA(input: {
24
27
  workspaceRoot: string
25
28
  filePath: string
26
29
  vocabulary?: DesignClassVocabulary
30
+ componentContracts?: DesignComponentContract[]
27
31
  }): Promise<ArtifactQAReport> {
28
32
  const sections: string[] = []
29
33
  let hardErrorCount = 0
@@ -39,12 +43,25 @@ export async function runArtifactQA(input: {
39
43
  sections.push("**[deck HTML contract]**\n\n" + formatDeckHtmlContractReport(contract))
40
44
  }
41
45
 
42
- const compliance = runComplianceQA(input.filePath, input.vocabulary)
43
- const complianceErrors = hardErrors(compliance)
44
- if (compliance.totalIssues > 0) {
45
- hardErrorCount += complianceErrors
46
- warningCount += warnings(compliance)
47
- sections.push("**[component compliance]**\n\n" + formatReport(compliance))
46
+ if (shouldRunArtifactCompliance(input.filePath)) {
47
+ const compliance = runComplianceQA(input.filePath, input.vocabulary)
48
+ const complianceErrors = hardErrors(compliance)
49
+ if (compliance.totalIssues > 0) {
50
+ hardErrorCount += complianceErrors
51
+ warningCount += warnings(compliance)
52
+ sections.push("**[component compliance]**\n\n" + formatReport(compliance))
53
+ }
54
+ }
55
+
56
+ const componentContracts = input.componentContracts ?? componentContractsForArtifact(input.filePath)
57
+ if (componentContracts.length > 0) {
58
+ const componentContractReport = runComponentContractQA(input.filePath, componentContracts)
59
+ const contractErrors = hardErrors(componentContractReport)
60
+ if (componentContractReport.totalIssues > 0) {
61
+ hardErrorCount += contractErrors
62
+ warningCount += warnings(componentContractReport)
63
+ sections.push("**[component structure contracts]**\n\n" + formatReport(componentContractReport))
64
+ }
48
65
  }
49
66
 
50
67
  try {
@@ -69,6 +86,34 @@ export async function runArtifactQA(input: {
69
86
  }
70
87
  }
71
88
 
89
+ function componentContractsForArtifact(filePath: string): DesignComponentContract[] {
90
+ const designName = designNameFromPreviewPath(filePath)
91
+ try {
92
+ return extractDesignComponentContracts(designName || activeDesign())
93
+ } catch {
94
+ return []
95
+ }
96
+ }
97
+
98
+ export function shouldRunArtifactCompliance(filePath: string): boolean {
99
+ return !isDesignPreviewFile(filePath)
100
+ }
101
+
102
+ function isDesignPreviewFile(filePath: string): boolean {
103
+ const normalizedPath = filePath.replace(/\\/g, "/")
104
+ if (basename(normalizedPath) !== "preview.html") return false
105
+ const parts = dirname(normalizedPath).split("/")
106
+ return parts.length >= 2 && parts[parts.length - 2] === "designs"
107
+ }
108
+
109
+ function designNameFromPreviewPath(filePath: string): string | undefined {
110
+ const normalizedPath = filePath.replace(/\\/g, "/")
111
+ if (basename(normalizedPath) !== "preview.html") return undefined
112
+ const parts = dirname(normalizedPath).split("/")
113
+ if (parts.length >= 2 && parts[parts.length - 2] === "designs") return parts[parts.length - 1]
114
+ return undefined
115
+ }
116
+
72
117
  export function formatArtifactQAReport(report: ArtifactQAReport): string {
73
118
  const heading = report.passed ? "Artifact QA: PASSED" : "Artifact QA: FAILED"
74
119
  const summary = `**File:** \`${report.file}\`\n\n**Hard errors:** ${report.hardErrorCount}\n**Warnings:** ${report.warningCount}`
package/lib/qa/checks.ts CHANGED
@@ -30,7 +30,7 @@ export interface LayoutIssue {
30
30
  | "centroid_offset" | "bottom_gap" | "sparse"
31
31
  | "height_mismatch" | "density_mismatch"
32
32
  | "gap_variance"
33
- | "unknown_class" | "novel_css_rule"
33
+ | "unknown_class" | "novel_css_rule" | "component_contract"
34
34
  | "remote_url" | "refine_proxy" | "missing_file"
35
35
  severity: IssueSeverity
36
36
  /** Human-readable description for the LLM to act on */
@@ -0,0 +1,90 @@
1
+ import { readFileSync } from "fs"
2
+ import type { DesignComponentContract } from "../design/designs"
3
+ import type { LayoutIssue, QAReport, SlideReport } from "./checks"
4
+
5
+ function stripTags(value: string): string {
6
+ return value.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()
7
+ }
8
+
9
+ function extractTitle(html: string, index: number): string {
10
+ const match = /<(?:h1|h2|h3|title)\b[^>]*>([\s\S]*?)<\/(?:h1|h2|h3|title)>/i.exec(html)
11
+ const title = match ? stripTags(match[1]).slice(0, 80) : ""
12
+ return title || `Slide ${index + 1}`
13
+ }
14
+
15
+ function htmlHasClass(html: string, className: string): boolean {
16
+ const classAttrRe = /class\s*=\s*(["'])([\s\S]*?)\1/gi
17
+ let match: RegExpExecArray | null
18
+ while ((match = classAttrRe.exec(html)) !== null) {
19
+ if (match[2].split(/\s+/).includes(className)) return true
20
+ }
21
+ return false
22
+ }
23
+
24
+ function htmlUsesComponent(html: string, contract: DesignComponentContract): boolean {
25
+ if (new RegExp(`data-preview-component\\s*=\\s*["']${escapeRegExp(contract.component)}["']`, "i").test(html)) return true
26
+ return contract.requiredRootClasses.some((className) => htmlHasClass(html, className))
27
+ }
28
+
29
+ function escapeRegExp(value: string): string {
30
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
31
+ }
32
+
33
+ function validateContract(html: string, contract: DesignComponentContract): LayoutIssue[] {
34
+ if (!htmlUsesComponent(html, contract)) return []
35
+
36
+ const variantFailures: string[] = []
37
+ for (const variant of contract.variants) {
38
+ const missing = [
39
+ ...variant.requiredDescendantClasses.filter((className) => !htmlHasClass(html, className)),
40
+ ...(variant.repeatedItemClass && !htmlHasClass(html, variant.repeatedItemClass) ? [variant.repeatedItemClass] : []),
41
+ ...(variant.requiredItemClasses ?? []).filter((className) => !htmlHasClass(html, className)),
42
+ ...(variant.requireAlternatingClasses ?? []).filter((className) => !htmlHasClass(html, className)),
43
+ ]
44
+ if (missing.length === 0) return []
45
+ variantFailures.push(`${variant.name}: missing ${missing.join(", ")}`)
46
+ }
47
+
48
+ return [{
49
+ type: "compliance",
50
+ sub: "component_contract",
51
+ severity: "error",
52
+ detail: `Component \`${contract.component}\` does not satisfy its design structure contract. ${contract.guidance}`,
53
+ data: {
54
+ component: contract.component,
55
+ variants: variantFailures.join(" | "),
56
+ },
57
+ }]
58
+ }
59
+
60
+ function summarize(filePath: string, slides: SlideReport[]): QAReport {
61
+ const totalIssues = slides.reduce((sum, slide) => sum + slide.issues.length, 0)
62
+ const errorCount = slides.reduce((sum, slide) => sum + slide.issues.filter((issue) => issue.severity === "error").length, 0)
63
+ const warningCount = slides.reduce((sum, slide) => sum + slide.issues.filter((issue) => issue.severity === "warning").length, 0)
64
+ const summary = totalIssues === 0
65
+ ? "All component structure contracts passed."
66
+ : `Found ${totalIssues} component contract issue(s): ${errorCount} error(s), ${warningCount} warning(s).`
67
+ return { file: filePath, slides, totalIssues, errorCount, warningCount, summary }
68
+ }
69
+
70
+ export function runComponentContractQA(htmlFilePath: string, contracts: DesignComponentContract[]): QAReport {
71
+ const html = readFileSync(htmlFilePath, "utf-8")
72
+ const sectionRe = /<section\b[\s\S]*?<\/section>/gi
73
+ const slides: SlideReport[] = []
74
+ let match: RegExpExecArray | null
75
+ let index = 0
76
+
77
+ while ((match = sectionRe.exec(html)) !== null) {
78
+ const chunk = match[0]
79
+ const issues = contracts.flatMap((contract) => validateContract(chunk, contract))
80
+ slides.push({ index, title: extractTitle(chunk, index), issues })
81
+ index++
82
+ }
83
+
84
+ if (slides.length === 0) {
85
+ const issues = contracts.flatMap((contract) => validateContract(html, contract))
86
+ slides.push({ index: 0, title: extractTitle(html, 0), issues })
87
+ }
88
+
89
+ return summarize(htmlFilePath, slides)
90
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.18.15",
3
+ "version": "0.18.16",
4
4
  "description": "Codex-first CLI/MCP workspace for trusted narrative artifacts from local sources, research, and evidence",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "revela",
3
- "version": "0.18.15",
4
- "description": "Use Revela in Codex to specify, research, plan, make, review, and export trusted narrative decision artifacts.",
3
+ "version": "0.18.16",
4
+ "description": "Use Revela in Codex to specify, research, plan, make, and export trusted narrative decision artifacts.",
5
5
  "author": {
6
6
  "name": "cyber-dash-tech",
7
7
  "url": "https://github.com/cyber-dash-tech"
@@ -20,7 +20,7 @@
20
20
  "interface": {
21
21
  "displayName": "Revela",
22
22
  "shortDescription": "Trusted narrative artifacts from local sources and research.",
23
- "longDescription": "Revela helps Codex route workspace workflows, capture requirements in spec.md, ingest local materials, save research findings, plan decks, generate HTML deck artifacts, review them, and export PDF/PPTX/PNG outputs while preserving source traceability.",
23
+ "longDescription": "Revela helps Codex route workspace workflows, capture requirements in spec.md, ingest local materials, save research findings, plan decks, generate HTML deck artifacts, open them in Codex Browser for annotation, and export PDF/PPTX/PNG outputs while preserving source traceability.",
24
24
  "developerName": "cyber-dash-tech",
25
25
  "category": "Productivity",
26
26
  "capabilities": [
@@ -31,7 +31,7 @@
31
31
  "Use Revela to route the next workflow step for this workspace.",
32
32
  "Use Revela to write a spec.md for this deck objective.",
33
33
  "Use Revela to make a deck from the deck plan.",
34
- "Use Revela to review this deck artifact."
34
+ "Use Revela to export this deck artifact."
35
35
  ],
36
36
  "brandColor": "#2563EB"
37
37
  }
@@ -1,4 +1,5 @@
1
- import { dirname, resolve } from "path"
1
+ import { basename, dirname, resolve } from "path"
2
+ import { existsSync, readFileSync } from "fs"
2
3
  import { fileURLToPath, pathToFileURL } from "url"
3
4
  import { resolveRevelaRuntime } from "../mcp/runtime-resolver"
4
5
 
@@ -9,15 +10,13 @@ interface HookResult {
9
10
 
10
11
  export function extractDeckHtmlTargets(input: string): string[] {
11
12
  const targets = new Set<string>()
12
- const patterns = [
13
- /\bdecks\/[^\s"'`<>]+\.html\b/g,
14
- /(?:^\*\*\* Update File: |^\*\*\* Add File: )([^\r\n]+decks\/[^\r\n]+\.html)\s*$/gm,
15
- ]
13
+ const pattern = /(?:^\*\*\* Update File: |^\*\*\* Add File: )((?:[^\r\n]*\/)?decks\/[^\s"'`<>*?\r\n]+\.html)\s*$/gm
16
14
 
17
- for (const pattern of patterns) {
15
+ for (const patch of patchPayloadsFromInput(input)) {
18
16
  let match: RegExpExecArray | null
19
- while ((match = pattern.exec(input))) {
20
- targets.add((match[1] ?? match[0]).trim())
17
+ while ((match = pattern.exec(patch))) {
18
+ const target = match[1].trim()
19
+ if (!target.includes("*") && !target.includes("?")) targets.add(target)
21
20
  }
22
21
  }
23
22
 
@@ -59,6 +58,32 @@ export function workspaceRootFromInput(input: string): string {
59
58
  return resolve(process.env.CODEX_WORKSPACE_ROOT || process.env.PWD || process.cwd())
60
59
  }
61
60
 
61
+ function titleFromDeckHtml(absolutePath: string): string {
62
+ try {
63
+ if (!existsSync(absolutePath)) return basename(absolutePath)
64
+ const html = readFileSync(absolutePath, "utf-8")
65
+ const title = html.match(/<title[^>]*>([^<]+)<\/title>/i)?.[1]?.trim()
66
+ return title || basename(absolutePath)
67
+ } catch {
68
+ return basename(absolutePath)
69
+ }
70
+ }
71
+
72
+ export function formatDeckWebsiteCardHandoffNotice(workspaceRoot: string, target: string): string {
73
+ const absolutePath = resolve(workspaceRoot, target)
74
+ const deckUrl = pathToFileURL(absolutePath).href
75
+ const title = titleFromDeckHtml(absolutePath)
76
+ return [
77
+ "**Deck website card ready**",
78
+ "",
79
+ `Artifact QA passed for \`${target}\`. Reply with this standalone deck link so Codex renders an Open in Browser website card:`,
80
+ "",
81
+ `[${title}](${deckUrl})`,
82
+ "",
83
+ `If the card or direct \`file://\` navigation is unavailable, start a read-only local static server from the workspace root and link to \`http://127.0.0.1:<port>/${target}\` instead.`,
84
+ ].join("\n")
85
+ }
86
+
62
87
  export async function runPostWriteChecks(input: string): Promise<HookResult> {
63
88
  const messages: string[] = []
64
89
  const deckTargets = extractDeckHtmlTargets(input)
@@ -97,11 +122,16 @@ export async function runPostWriteChecks(input: string): Promise<HookResult> {
97
122
  }
98
123
 
99
124
  for (const target of deckTargets) {
125
+ if (!existsSync(resolve(workspaceRoot, target))) continue
100
126
  const result = await runtimeModule.runDeckQa({ workspaceRoot, file: target })
101
127
  messages.push(result.markdown ?? JSON.stringify(result, null, 2))
102
128
  const notice = runtimeModule.formatArtifactQaUserNotice?.(result.report)
103
129
  if (notice) messages.push(notice)
104
- if (!result.ok) ok = false
130
+ if (result.ok) {
131
+ messages.push(formatDeckWebsiteCardHandoffNotice(workspaceRoot, target))
132
+ } else {
133
+ ok = false
134
+ }
105
135
  }
106
136
 
107
137
  return { ok, messages }
@@ -296,7 +296,7 @@ const tools = [
296
296
  },
297
297
  {
298
298
  name: "revela_review_deck_read",
299
- description: "Read-only aggregate Review diagnostics for a Revela HTML deck: artifact QA, deck-plan diagnostics, and export/readiness signals.",
299
+ description: "Compatibility-only read of aggregate Review diagnostics for a Revela HTML deck: artifact QA, deck-plan diagnostics, and export/readiness signals.",
300
300
  inputSchema: objectSchema({
301
301
  workspaceRoot: stringProp("Optional workspace root."),
302
302
  file: requiredStringProp("Workspace-relative or absolute HTML deck path."),
@@ -305,7 +305,7 @@ const tools = [
305
305
  },
306
306
  {
307
307
  name: "revela_review_deck_open",
308
- description: "Open a local Codex-backed Review UI for a Revela HTML deck from the current MCP server process.",
308
+ description: "Compatibility-only opener for the legacy local Codex-backed Review UI for a Revela HTML deck.",
309
309
  inputSchema: objectSchema({
310
310
  workspaceRoot: stringProp("Optional workspace root."),
311
311
  file: requiredStringProp("Workspace-relative or absolute HTML deck path."),
@@ -11,7 +11,7 @@ Use this skill as the main Revela entrypoint in Codex. It should inspect intent
11
11
 
12
12
  - This is a non-mutating router.
13
13
  - It may inspect runtime, active design/domain, and workspace artifact status.
14
- - It must not write `spec.md`, save research findings, write `deck-plan.md`, generate deck HTML, open Review UI, or export artifacts.
14
+ - It must not write `spec.md`, save research findings, write `deck-plan.md`, generate deck HTML, open deck browser views, or export artifacts.
15
15
  - Route quickly once the next workflow is clear.
16
16
 
17
17
  ## Required Tools
@@ -29,7 +29,7 @@ Use this skill as the main Revela entrypoint in Codex. It should inspect intent
29
29
  - `spec.md` exists but source support, material review, or findings are missing: use `revela-research`.
30
30
  - `spec.md` and sufficient findings exist but `deck-plan.md` is missing or needs normal authoring: use `revela-research` Planning Handoff.
31
31
  - Valid `deck-plan.md` exists and the user asks to make, generate, render, or update a deck: use `revela-make-deck`.
32
- - Existing deck artifact and the user asks to review, diagnose, QA, or refine: use `revela-review`.
32
+ - Existing deck artifact and the user asks to review, annotate, diagnose, QA, or refine: use Codex Browser's native browsing/annotation flow. If the deck was not just generated, reply with the existing deck as a website card/link and use native annotations after it opens; route export requests to `revela-export`.
33
33
  - Existing deck artifact and the user asks for PDF, PPTX, or PNG output: use `revela-export`.
34
34
  - If the next step is still ambiguous after inspection, ask the smallest missing question and recommend the safest next specialist skill.
35
35
 
@@ -47,5 +47,5 @@ Report:
47
47
  - Do not write or patch files.
48
48
  - Do not do external web research.
49
49
  - Do not create or repair `spec.md` or `deck-plan.md`.
50
- - Do not generate, review, patch, or export deck artifacts.
50
+ - Do not generate, annotate, patch, or export deck artifacts.
51
51
  - Do not install or activate designs or domains; route those requests to `revela-design` or `revela-domain`.
@@ -10,6 +10,7 @@ Use this skill when the user asks to create, customize, edit, validate, package,
10
10
  ## Contract
11
11
 
12
12
  - Designs define deck visual systems: rules, foundation, layouts, components, chart rules, and preview coverage.
13
+ - Designs should define executable visual contracts, not only mood, fonts, and palettes. Capture grid/safe-area, spacing scale, type scale, surface behavior, chart tokens, component states, and preview fixtures in the design package.
13
14
  - Designs may include package-owned `assets/**` such as cover or closing backgrounds; design tools surface these as design elements, not source evidence.
14
15
  - When the user uploads or provides logo, cover, closing, background, texture, brand image, or similar design material, store it inside the design package with `revela_design_draft_create.assets`; use paths under `assets/**` only.
15
16
  - Generated `preview.html` must actually reference uploaded design assets with package-relative `assets/...` paths rather than describing them only in text.
@@ -53,11 +54,16 @@ Use `revela_design_create` only when the user explicitly requests direct local c
53
54
  - Use a kebab-case design name.
54
55
  - `DESIGN.md` must include valid frontmatter and complete design marker sections.
55
56
  - Include design rules, foundation guidance, at least one layout, and at least one component.
57
+ - In `@design:foundation`, document the design contract: grid columns or layout rails, safe area, spacing/baseline scale, typography scale, surfaces/borders/shadows, and chart tokens when charts are supported.
58
+ - Layouts must declare stable slots and use grid/flex structure as the source of alignment. Avoid one-off absolute positioning that bypasses the declared layout contract.
59
+ - Components should describe normal, dense, and long-copy behavior where relevant. Chart, table, media, and source-note components need stable container dimensions.
56
60
  - Optional assets must live under `assets/**`; reference them as package-relative paths like `assets/cover-background.png`.
57
61
  - `DESIGN.md` may reference package assets in rules, layouts, or components with `assets/...`; do not reference workspace `assets/` media manifest entries for design-owned visuals.
58
62
  - `preview.html` must use the fixed Revela preview canvas contract and visibly preview the design.
59
63
  - If design assets are present, `preview.html` must visibly use the saved `assets/...` files, for example a cover hero background or logo image.
60
64
  - Preview must include cover and closing examples and showcase every component.
65
+ - Preview should showcase every layout with `data-preview-layout="<layout-name>"` and every component with `data-preview-component="<component-name>"`.
66
+ - Preview should behave like a design test fixture: include normal content, dense content, mixed-language text where relevant, chart/table examples when supported, readable media, and source-note behavior.
61
67
  - Preserve source inspiration and limitations explicitly; do not copy copyrighted design text or assets into the package.
62
68
 
63
69
  ## Outputs
@@ -12,7 +12,7 @@ Use this skill when the user asks what Revela is, what the current workspace sta
12
12
  - This is a read-only helper and orientation surface.
13
13
  - `revela` is the main workflow router; this skill explains status and capabilities.
14
14
  - It may inspect runtime, design, domain, and workspace artifact status.
15
- - It must not perform research, write files, create `spec.md`, create `deck-plan.md`, generate decks, open Review UI, or export artifacts.
15
+ - It must not perform research, write files, create `spec.md`, create `deck-plan.md`, generate decks, open deck browser views, or export artifacts.
16
16
  - Keep the answer short and operational.
17
17
 
18
18
  ## Preconditions
@@ -46,7 +46,7 @@ Report:
46
46
  - `spec.md` exists but no `researches/`: run `revela-research`.
47
47
  - Research exists but no `deck-plan.md`: continue `revela-research` to the Planning Handoff.
48
48
  - Valid `deck-plan.md` but no deck artifact: run `revela-make-deck`.
49
- - Existing deck artifact: run `revela-review` or `revela-export` depending on the user goal.
49
+ - Existing deck artifact: surface the HTML deck as a website card/link for Codex Browser native annotation, or run `revela-export` for PDF/PPTX/PNG.
50
50
 
51
51
  ## Must Not
52
52
 
@@ -54,5 +54,5 @@ Report:
54
54
  - Do not do external web research.
55
55
  - Do not generate or repair `spec.md`.
56
56
  - Do not generate or repair `deck-plan.md`.
57
- - Do not generate, review, patch, or export deck artifacts.
57
+ - Do not generate, annotate, patch, or export deck artifacts.
58
58
  - Do not create, install, or activate designs or domains; route those requests to `revela-design` or `revela-domain`.