@cyber-dash-tech/revela 0.18.14 → 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.
- package/README.md +35 -46
- package/README.zh-CN.md +35 -46
- package/bin/revela.ts +0 -0
- package/designs/lucent/DESIGN.md +32 -1
- package/designs/lucent/preview.html +411 -494
- package/designs/monet/DESIGN.md +39 -9
- package/designs/monet/preview.html +157 -2260
- package/designs/starter/DESIGN.md +8 -5
- package/designs/starter/preview.html +70 -49
- package/designs/summit/DESIGN.md +36 -9
- package/designs/summit/preview.html +139 -2237
- package/lib/commands/designs-new.ts +9 -0
- package/lib/design/designs.ts +84 -4
- package/lib/prompt-builder.ts +4 -0
- package/lib/qa/artifact.ts +52 -7
- package/lib/qa/checks.ts +1 -1
- package/lib/qa/component-contracts.ts +90 -0
- package/package.json +1 -1
- package/plugins/revela/.codex-plugin/plugin.json +4 -4
- package/plugins/revela/.mcp.json +3 -2
- package/plugins/revela/hooks/revela_post_write_notice.ts +39 -9
- package/plugins/revela/mcp/revela-server.ts +3 -3
- package/plugins/revela/skills/revela/SKILL.md +3 -3
- package/plugins/revela/skills/revela-design/SKILL.md +6 -0
- package/plugins/revela/skills/revela-helper/SKILL.md +3 -3
- package/plugins/revela/skills/revela-make-deck/SKILL.md +9 -3
- package/plugins/revela/skills/revela-research/SKILL.md +1 -0
- package/skill/SKILL.md +11 -2
- package/plugins/revela/skills/revela-review/SKILL.md +0 -46
|
@@ -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}\`.`
|
package/lib/design/designs.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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"]
|
package/lib/prompt-builder.ts
CHANGED
|
@@ -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
|
package/lib/qa/artifact.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { formatDeckHtmlContractReport, validateDeckHtmlContract } from "../deck-html/contract"
|
|
2
|
-
import
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "revela",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Use Revela in Codex to specify, research, plan, make,
|
|
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,
|
|
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
|
|
34
|
+
"Use Revela to export this deck artifact."
|
|
35
35
|
],
|
|
36
36
|
"brandColor": "#2563EB"
|
|
37
37
|
}
|
package/plugins/revela/.mcp.json
CHANGED
|
@@ -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
|
|
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
|
|
15
|
+
for (const patch of patchPayloadsFromInput(input)) {
|
|
18
16
|
let match: RegExpExecArray | null
|
|
19
|
-
while ((match = pattern.exec(
|
|
20
|
-
|
|
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 (
|
|
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: "
|
|
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: "
|
|
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."),
|
|
@@ -395,7 +395,7 @@ async function handle(req: JsonRpcRequest): Promise<any | undefined> {
|
|
|
395
395
|
return result(req.id, {
|
|
396
396
|
protocolVersion: req.params?.protocolVersion || "2024-11-05",
|
|
397
397
|
capabilities: { tools: {} },
|
|
398
|
-
serverInfo: { name: "revela", version: "0.
|
|
398
|
+
serverInfo: { name: "revela", version: "0.18.15" },
|
|
399
399
|
})
|
|
400
400
|
}
|
|
401
401
|
if (req.method === "tools/list") {
|
|
@@ -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
|
|
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-
|
|
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,
|
|
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
|
|
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:
|
|
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,
|
|
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`.
|