@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.
- package/README.md +48 -45
- package/README.zh-CN.md +48 -45
- package/assets/img/lucent-01.jpg +0 -0
- package/assets/img/lucent-02.jpg +0 -0
- package/assets/img/lucent-03.jpg +0 -0
- package/assets/img/lucent-dark-01.jpg +0 -0
- package/assets/img/lucent-dark-02.jpg +0 -0
- package/assets/img/lucent-dark-03.jpg +0 -0
- package/assets/img/monet-01.jpg +0 -0
- package/assets/img/monet-02.jpg +0 -0
- package/assets/img/monet-03.jpg +0 -0
- package/assets/img/starter-01.jpg +0 -0
- package/assets/img/starter-02.jpg +0 -0
- package/assets/img/starter-03.jpg +0 -0
- package/assets/img/summit-01.jpg +0 -0
- package/assets/img/summit-02.jpg +0 -0
- package/assets/img/summit-03.jpg +0 -0
- package/designs/lucent/DESIGN.md +108 -1
- package/designs/lucent/design.css +283 -0
- package/designs/lucent-dark/DESIGN.md +278 -0
- package/designs/lucent-dark/assets/card-lens.jpg +0 -0
- package/designs/lucent-dark/assets/closing-background.jpg +0 -0
- package/designs/lucent-dark/assets/cover-background.jpg +0 -0
- package/designs/lucent-dark/assets/report-visual.jpg +0 -0
- package/designs/lucent-dark/assets/soft-texture.jpg +0 -0
- package/designs/lucent-dark/assets/toc-orb.png +0 -0
- package/designs/lucent-dark/design.css +417 -0
- package/designs/monet/DESIGN.md +53 -9
- package/designs/monet/assets/card-lens.jpg +0 -0
- package/designs/monet/assets/closing-background.jpg +0 -0
- package/designs/monet/assets/cover-background.jpg +0 -0
- package/designs/monet/assets/report-visual.jpg +0 -0
- package/designs/monet/assets/soft-texture.jpg +0 -0
- package/designs/monet/assets/toc-orb.png +0 -0
- package/designs/monet/design.css +340 -0
- package/designs/starter/DESIGN.md +22 -5
- package/designs/starter/assets/card-lens.jpg +0 -0
- package/designs/starter/assets/closing-background.jpg +0 -0
- package/designs/starter/assets/cover-background.jpg +0 -0
- package/designs/starter/assets/report-visual.jpg +0 -0
- package/designs/starter/assets/soft-texture.jpg +0 -0
- package/designs/starter/assets/toc-orb.png +0 -0
- package/designs/starter/design.css +322 -0
- package/designs/summit/DESIGN.md +54 -9
- package/designs/summit/assets/card-lens.jpg +0 -0
- package/designs/summit/assets/closing-background.jpg +0 -0
- package/designs/summit/assets/cover-background.jpg +0 -0
- package/designs/summit/assets/report-visual.jpg +0 -0
- package/designs/summit/assets/soft-texture.jpg +0 -0
- package/designs/summit/assets/toc-orb.png +0 -0
- package/designs/summit/design.css +334 -0
- package/lib/commands/designs-new.ts +18 -21
- package/lib/commands/designs-preview.ts +3 -8
- package/lib/deck-html/foundation.ts +8 -8
- package/lib/design/designs.ts +385 -14
- package/lib/narrative-state/deck-plan-artifact.ts +40 -3
- package/lib/page-templates/built-in-preview.html +373 -0
- package/lib/page-templates/contracts.ts +2 -0
- package/lib/page-templates/css.ts +2 -0
- package/lib/page-templates/foundation.ts +41 -0
- package/lib/page-templates/index.ts +6 -0
- package/lib/page-templates/registry.ts +3 -0
- package/lib/page-templates/render.ts +1202 -0
- package/lib/page-templates/templates/agenda.ts +4 -0
- package/lib/page-templates/templates/chart-takeaways.ts +4 -0
- package/lib/page-templates/templates/claim-supporting-visual.ts +4 -0
- package/lib/page-templates/templates/closing.ts +4 -0
- package/lib/page-templates/templates/cover.ts +4 -0
- package/lib/page-templates/templates/executive-summary.ts +4 -0
- package/lib/page-templates/templates/index.ts +19 -0
- package/lib/page-templates/templates/key-message-evidence.ts +4 -0
- package/lib/page-templates/templates/metric-highlight.ts +4 -0
- package/lib/page-templates/templates/problem-context.ts +4 -0
- package/lib/page-templates/templates/process-steps.ts +4 -0
- package/lib/page-templates/templates/recommendation-decision.ts +4 -0
- package/lib/page-templates/templates/risks-tradeoffs.ts +4 -0
- package/lib/page-templates/templates/section-divider.ts +4 -0
- package/lib/page-templates/templates/shared.ts +11 -0
- package/lib/page-templates/templates/table-comparison.ts +4 -0
- package/lib/page-templates/templates/timeline-roadmap.ts +4 -0
- package/lib/page-templates/vocabulary.ts +158 -0
- package/lib/prompt-builder.ts +9 -5
- package/lib/qa/artifact.ts +117 -7
- package/lib/qa/checks.ts +1 -1
- package/lib/qa/compliance.ts +5 -1
- package/lib/qa/component-contracts.ts +90 -0
- package/lib/runtime/index.ts +99 -3
- package/package.json +7 -15
- package/plugins/revela/.codex-plugin/plugin.json +4 -4
- package/plugins/revela/hooks/revela_guard.ts +35 -0
- package/plugins/revela/hooks/revela_post_write_notice.ts +39 -9
- package/plugins/revela/mcp/revela-server.ts +103 -7
- package/plugins/revela/skills/revela/SKILL.md +3 -3
- package/plugins/revela/skills/revela-design/SKILL.md +25 -14
- package/plugins/revela/skills/revela-helper/SKILL.md +3 -3
- package/plugins/revela/skills/revela-make-deck/SKILL.md +27 -12
- package/plugins/revela/skills/revela-research/SKILL.md +1 -0
- package/skill/SKILL.md +11 -2
- package/designs/lucent/preview.html +0 -612
- package/designs/monet/preview.html +0 -2293
- package/designs/starter/preview.html +0 -314
- package/designs/summit/preview.html +0 -2284
- package/plugins/revela/skills/revela-review/SKILL.md +0 -46
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { listPageTemplateVocabulary } from "../vocabulary"
|
|
2
|
+
|
|
3
|
+
export const BUILTIN_PAGE_TEMPLATE_IDS = listPageTemplateVocabulary().map((template) => template.templateId)
|
|
4
|
+
|
|
5
|
+
export * from "./agenda"
|
|
6
|
+
export * from "./chart-takeaways"
|
|
7
|
+
export * from "./claim-supporting-visual"
|
|
8
|
+
export * from "./closing"
|
|
9
|
+
export * from "./cover"
|
|
10
|
+
export * from "./executive-summary"
|
|
11
|
+
export * from "./key-message-evidence"
|
|
12
|
+
export * from "./metric-highlight"
|
|
13
|
+
export * from "./problem-context"
|
|
14
|
+
export * from "./process-steps"
|
|
15
|
+
export * from "./recommendation-decision"
|
|
16
|
+
export * from "./risks-tradeoffs"
|
|
17
|
+
export * from "./section-divider"
|
|
18
|
+
export * from "./table-comparison"
|
|
19
|
+
export * from "./timeline-roadmap"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getPageTemplateFoundation } from "../foundation"
|
|
2
|
+
import { getPageTemplateVocabulary } from "../vocabulary"
|
|
3
|
+
|
|
4
|
+
export function templateModule(templateId: string) {
|
|
5
|
+
return {
|
|
6
|
+
templateId,
|
|
7
|
+
foundation: () => getPageTemplateFoundation(templateId),
|
|
8
|
+
vocabulary: () => getPageTemplateVocabulary(templateId),
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export interface PageTemplateSlotVocabulary {
|
|
2
|
+
name: string
|
|
3
|
+
required: boolean
|
|
4
|
+
editable: boolean
|
|
5
|
+
replaceable: boolean
|
|
6
|
+
description: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PageTemplateVocabulary {
|
|
10
|
+
templateId: string
|
|
11
|
+
rootClasses: string[]
|
|
12
|
+
requiredClasses: string[]
|
|
13
|
+
optionalClasses: string[]
|
|
14
|
+
slots: PageTemplateSlotVocabulary[]
|
|
15
|
+
editableSlots: string[]
|
|
16
|
+
replaceableSlots: string[]
|
|
17
|
+
contractNotes: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const sharedClasses = [
|
|
21
|
+
"template-slide",
|
|
22
|
+
"template-frame",
|
|
23
|
+
"template-eyebrow",
|
|
24
|
+
"template-title",
|
|
25
|
+
"template-body",
|
|
26
|
+
"template-grid",
|
|
27
|
+
"template-chart-layout",
|
|
28
|
+
"cols-2",
|
|
29
|
+
"cols-3",
|
|
30
|
+
"cols-4",
|
|
31
|
+
"template-card",
|
|
32
|
+
"template-list",
|
|
33
|
+
"template-hero",
|
|
34
|
+
"template-hero-title",
|
|
35
|
+
"template-hero--cover",
|
|
36
|
+
"template-hero--section-divider",
|
|
37
|
+
"template-hero--closing",
|
|
38
|
+
"template-frame--catalog",
|
|
39
|
+
"template-page-number",
|
|
40
|
+
"template-image-card",
|
|
41
|
+
"template-image-frame",
|
|
42
|
+
"template-image-caption",
|
|
43
|
+
"template-visual-placeholder",
|
|
44
|
+
"template-visual-placeholder-frame",
|
|
45
|
+
"template-visual-placeholder-label",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
export const PAGE_TEMPLATE_VOCABULARY: PageTemplateVocabulary[] = [
|
|
49
|
+
vocab("cover", ["template-hero"], ["hero"], ["hero"], ["Cover, divider, and closing templates use the hero frame; keep title hierarchy visible."]),
|
|
50
|
+
vocab("section-divider", ["template-hero"], ["hero"], ["hero"], ["Section divider uses the same hero-safe structure as cover."]),
|
|
51
|
+
vocab("closing", ["template-hero"], ["hero"], ["hero"], ["Closing uses the same hero-safe structure as cover."]),
|
|
52
|
+
vocab("agenda", ["template-agenda-panel"], ["agenda", "agenda-list"], ["agenda", "agenda-list"], ["Agenda numbers must remain in DOM order."]),
|
|
53
|
+
vocab("executive-summary", ["template-card"], ["summary-cards"], ["summary-cards"], ["Cards are editable; visual placeholders are optional and may become image/chart slots."]),
|
|
54
|
+
vocab("problem-context", ["template-card"], ["context", "supporting-points"], ["context", "supporting-points"], ["Context should stay separate from supporting bullets."]),
|
|
55
|
+
vocab("key-message-evidence", ["template-key-message-panel", "template-evidence-grid"], ["key-message", "evidence"], ["key-message", "evidence"], ["Key message and evidence regions must remain distinct."]),
|
|
56
|
+
vocab("claim-supporting-visual", ["template-claim-text-panel", "template-visual-slot-panel"], ["claim", "visual"], ["claim", "visual"], ["Visual slot may be replaced by image, chart, table, or diagram container."]),
|
|
57
|
+
vocab("metric-highlight", ["template-stat-grid"], ["metrics"], ["metrics", "insight"], ["Metric values should remain visible outside prose."]),
|
|
58
|
+
vocab("chart-takeaways", ["template-chart-panel", "template-chart-takeaway-panel"], ["visual", "takeaways"], ["visual", "takeaways"], ["Chart/image slot and takeaway text panel must both remain present."]),
|
|
59
|
+
vocab("table-comparison", ["template-table-wrap", "template-table"], ["table"], ["table", "insight"], ["Table headers and body should remain structured, not prose-only."]),
|
|
60
|
+
vocab("timeline-roadmap", ["template-timeline", "template-timeline-item", "template-timeline-dot", "template-timeline-copy", "template-insight-icon"], ["timeline"], ["timeline", "insight"], ["Each timeline item must keep dot and copy as sibling anchors inside one item.", "Horizontal timeline cards reuse .template-card; highlight uses the item modifier."]),
|
|
61
|
+
vocab("process-steps", ["template-steps", "template-step-number"], ["steps"], ["steps"], ["Steps should remain ordered in DOM order."]),
|
|
62
|
+
vocab("recommendation-decision", ["template-card"], ["recommendation", "rationale", "next-steps"], ["recommendation", "rationale", "next-steps"], ["Keep recommendation, rationale, and next steps separate."]),
|
|
63
|
+
vocab("risks-tradeoffs", ["template-card"], ["risks"], ["risks"], ["Risk/tradeoff cards should name uncertainty explicitly."]),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
const additionalClasses = [
|
|
67
|
+
"template-key-message-panel",
|
|
68
|
+
"template-key-message-kicker",
|
|
69
|
+
"template-evidence-grid",
|
|
70
|
+
"template-evidence-card",
|
|
71
|
+
"template-claim-text-panel",
|
|
72
|
+
"template-claim-text-title",
|
|
73
|
+
"template-claim-text-body",
|
|
74
|
+
"template-agenda-panel",
|
|
75
|
+
"template-agenda-inner",
|
|
76
|
+
"template-agenda-header",
|
|
77
|
+
"template-agenda-footer",
|
|
78
|
+
"template-agenda-list",
|
|
79
|
+
"template-agenda-item",
|
|
80
|
+
"template-stat-grid",
|
|
81
|
+
"template-stat-value",
|
|
82
|
+
"template-metric-layout",
|
|
83
|
+
"template-metric-layout--insight-top",
|
|
84
|
+
"template-metric-layout--insight-bottom",
|
|
85
|
+
"template-chart-panel",
|
|
86
|
+
"template-chart-placeholder",
|
|
87
|
+
"template-visual-slot-panel",
|
|
88
|
+
"template-visual-slot-label",
|
|
89
|
+
"template-chart-takeaway-panel",
|
|
90
|
+
"template-chart-takeaway-list",
|
|
91
|
+
"template-chart-takeaway-item",
|
|
92
|
+
"template-bar",
|
|
93
|
+
"template-table",
|
|
94
|
+
"template-table-wrap",
|
|
95
|
+
"template-side-panel",
|
|
96
|
+
"template-side-panel-title",
|
|
97
|
+
"template-side-panel-body",
|
|
98
|
+
"template-side-panel--left",
|
|
99
|
+
"template-side-panel--right",
|
|
100
|
+
"template-text-panel",
|
|
101
|
+
"template-text-panel-title",
|
|
102
|
+
"template-text-panel-body",
|
|
103
|
+
"template-insight-panel",
|
|
104
|
+
"template-insight-title",
|
|
105
|
+
"template-insight-icon",
|
|
106
|
+
"template-insight-body",
|
|
107
|
+
"template-timeline",
|
|
108
|
+
"template-timeline-layout",
|
|
109
|
+
"template-timeline-layout--left",
|
|
110
|
+
"template-timeline-layout--right",
|
|
111
|
+
"template-timeline--horizontal",
|
|
112
|
+
"template-timeline--vertical",
|
|
113
|
+
"template-timeline-item",
|
|
114
|
+
"template-timeline-item--highlight",
|
|
115
|
+
"template-timeline-dot",
|
|
116
|
+
"template-timeline-copy",
|
|
117
|
+
"template-timeline-date",
|
|
118
|
+
"template-steps",
|
|
119
|
+
"template-step-number",
|
|
120
|
+
"template-catalog-panel",
|
|
121
|
+
"template-catalog-kicker",
|
|
122
|
+
"template-catalog-title",
|
|
123
|
+
"template-catalog-grid",
|
|
124
|
+
"template-catalog-section",
|
|
125
|
+
"template-catalog-list",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
export const PAGE_TEMPLATE_CLASSES = [...new Set([...sharedClasses, ...additionalClasses])]
|
|
129
|
+
|
|
130
|
+
export function listPageTemplateVocabulary(): PageTemplateVocabulary[] {
|
|
131
|
+
return PAGE_TEMPLATE_VOCABULARY
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getPageTemplateVocabulary(templateId: string): PageTemplateVocabulary {
|
|
135
|
+
const vocabulary = PAGE_TEMPLATE_VOCABULARY.find((item) => item.templateId === templateId)
|
|
136
|
+
if (!vocabulary) throw new Error(`Unknown page template vocabulary: ${templateId}`)
|
|
137
|
+
return vocabulary
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function vocab(templateId: string, requiredClasses: string[], slotNames: string[], replaceableSlots: string[], contractNotes: string[]): PageTemplateVocabulary {
|
|
141
|
+
const slots = slotNames.map((name) => ({
|
|
142
|
+
name,
|
|
143
|
+
required: true,
|
|
144
|
+
editable: true,
|
|
145
|
+
replaceable: replaceableSlots.includes(name),
|
|
146
|
+
description: `${templateId} ${name} slot.`,
|
|
147
|
+
}))
|
|
148
|
+
return {
|
|
149
|
+
templateId,
|
|
150
|
+
rootClasses: ["template-slide", "template-frame", ...requiredClasses.slice(0, 1)],
|
|
151
|
+
requiredClasses,
|
|
152
|
+
optionalClasses: [],
|
|
153
|
+
slots,
|
|
154
|
+
editableSlots: slotNames,
|
|
155
|
+
replaceableSlots,
|
|
156
|
+
contractNotes,
|
|
157
|
+
}
|
|
158
|
+
}
|
package/lib/prompt-builder.ts
CHANGED
|
@@ -82,12 +82,12 @@ export function buildPrompt(optionsOrDesignName?: BuildPromptOptions | string, l
|
|
|
82
82
|
// Layer 1 — core skill for the selected prompt mode.
|
|
83
83
|
const coreSkill = readFileSync(mode === "deck-render" ? SKILL_MD_PATH : NARRATIVE_SKILL_MD_PATH, "utf-8")
|
|
84
84
|
|
|
85
|
-
// Check for
|
|
85
|
+
// Check for CSS-native design styling.
|
|
86
86
|
const designDir = join(DESIGNS_DIR, design)
|
|
87
|
-
const
|
|
88
|
-
const previewLine =
|
|
89
|
-
? "<!-- -
|
|
90
|
-
: "<!-- - (no
|
|
87
|
+
const hasDesignCss = existsSync(join(designDir, "design.css"))
|
|
88
|
+
const previewLine = hasDesignCss
|
|
89
|
+
? "<!-- - design.css — executable design styling; generated previews use the built-in page template fixture -->"
|
|
90
|
+
: "<!-- - (no design.css for this design; compatibility CSS may be generated from DESIGN.md) -->"
|
|
91
91
|
|
|
92
92
|
// Layer 2 — DOMAIN.md skill text (narrative mode only). Deck-render mode
|
|
93
93
|
// renders the approved canonical narrative and must not re-interpret domain
|
|
@@ -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,13 @@
|
|
|
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"
|
|
7
|
+
import { formatPageTemplateContractReport, validatePageTemplateContracts } from "../page-templates"
|
|
5
8
|
import type { QAReport } from "./checks"
|
|
9
|
+
import { existsSync, readFileSync } from "fs"
|
|
10
|
+
import { basename, dirname, resolve } from "path"
|
|
6
11
|
|
|
7
12
|
export interface ArtifactQAReport {
|
|
8
13
|
file: string
|
|
@@ -24,6 +29,7 @@ export async function runArtifactQA(input: {
|
|
|
24
29
|
workspaceRoot: string
|
|
25
30
|
filePath: string
|
|
26
31
|
vocabulary?: DesignClassVocabulary
|
|
32
|
+
componentContracts?: DesignComponentContract[]
|
|
27
33
|
}): Promise<ArtifactQAReport> {
|
|
28
34
|
const sections: string[] = []
|
|
29
35
|
let hardErrorCount = 0
|
|
@@ -39,12 +45,42 @@ export async function runArtifactQA(input: {
|
|
|
39
45
|
sections.push("**[deck HTML contract]**\n\n" + formatDeckHtmlContractReport(contract))
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
const designCss = validateLinkedDesignCss(input.filePath)
|
|
49
|
+
if (designCss.errors.length > 0 || designCss.warnings.length > 0) {
|
|
50
|
+
hardErrorCount += designCss.errors.length
|
|
51
|
+
warningCount += designCss.warnings.length
|
|
52
|
+
sections.push("**[design CSS snapshot]**\n\n" + [
|
|
53
|
+
...designCss.errors.map((message) => `- ERROR: ${message}`),
|
|
54
|
+
...designCss.warnings.map((message) => `- WARNING: ${message}`),
|
|
55
|
+
].join("\n"))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (shouldRunArtifactCompliance(input.filePath)) {
|
|
59
|
+
const compliance = runComplianceQA(input.filePath, input.vocabulary)
|
|
60
|
+
const complianceErrors = hardErrors(compliance)
|
|
61
|
+
if (compliance.totalIssues > 0) {
|
|
62
|
+
hardErrorCount += complianceErrors
|
|
63
|
+
warningCount += warnings(compliance)
|
|
64
|
+
sections.push("**[component compliance]**\n\n" + formatReport(compliance))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const componentContracts = input.componentContracts ?? componentContractsForArtifact(input.filePath)
|
|
69
|
+
if (componentContracts.length > 0) {
|
|
70
|
+
const componentContractReport = runComponentContractQA(input.filePath, componentContracts)
|
|
71
|
+
const contractErrors = hardErrors(componentContractReport)
|
|
72
|
+
if (componentContractReport.totalIssues > 0) {
|
|
73
|
+
hardErrorCount += contractErrors
|
|
74
|
+
warningCount += warnings(componentContractReport)
|
|
75
|
+
sections.push("**[component structure contracts]**\n\n" + formatReport(componentContractReport))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const templateContracts = validatePageTemplateContracts(input.filePath)
|
|
80
|
+
if (templateContracts.issues.length > 0) {
|
|
81
|
+
hardErrorCount += templateContracts.issues.filter((issue) => issue.severity === "error").length
|
|
82
|
+
warningCount += templateContracts.issues.filter((issue) => issue.severity === "warning").length
|
|
83
|
+
sections.push("**[page template contracts]**\n\n" + formatPageTemplateContractReport(templateContracts))
|
|
48
84
|
}
|
|
49
85
|
|
|
50
86
|
try {
|
|
@@ -69,6 +105,80 @@ export async function runArtifactQA(input: {
|
|
|
69
105
|
}
|
|
70
106
|
}
|
|
71
107
|
|
|
108
|
+
function validateLinkedDesignCss(filePath: string): { errors: string[]; warnings: string[] } {
|
|
109
|
+
if (isDesignPreviewFile(filePath)) return { errors: [], warnings: [] }
|
|
110
|
+
const errors: string[] = []
|
|
111
|
+
const warnings: string[] = []
|
|
112
|
+
let html = ""
|
|
113
|
+
try {
|
|
114
|
+
html = readFileSync(filePath, "utf-8")
|
|
115
|
+
} catch {
|
|
116
|
+
return { errors, warnings }
|
|
117
|
+
}
|
|
118
|
+
const hrefs = [...html.matchAll(/<link\b[^>]*rel=["']stylesheet["'][^>]*href=["']([^"']*design\.css)["'][^>]*>/gi)].map((match) => match[1])
|
|
119
|
+
if (hrefs.length === 0) {
|
|
120
|
+
warnings.push("Deck does not reference a design.css snapshot.")
|
|
121
|
+
return { errors, warnings }
|
|
122
|
+
}
|
|
123
|
+
for (const href of hrefs) {
|
|
124
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(href)) continue
|
|
125
|
+
const cssPath = resolve(dirname(filePath), href)
|
|
126
|
+
if (!existsSync(cssPath)) {
|
|
127
|
+
errors.push(`Linked design CSS is missing: ${href}`)
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
const css = readFileSync(cssPath, "utf-8")
|
|
131
|
+
for (const asset of cssAssetUrls(css)) {
|
|
132
|
+
const assetPath = resolve(dirname(cssPath), asset)
|
|
133
|
+
if (!existsSync(assetPath)) errors.push(`Linked design CSS references missing asset: ${href} -> ${asset}`)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { errors, warnings }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function cssAssetUrls(css: string): string[] {
|
|
140
|
+
const urls: string[] = []
|
|
141
|
+
const seen = new Set<string>()
|
|
142
|
+
const urlRe = /url\(\s*["']?([^"')]+)["']?\s*\)/gi
|
|
143
|
+
let match: RegExpExecArray | null
|
|
144
|
+
while ((match = urlRe.exec(css)) !== null) {
|
|
145
|
+
const raw = match[1].trim()
|
|
146
|
+
if (!raw || raw.startsWith("data:") || /^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.startsWith("#")) continue
|
|
147
|
+
if (seen.has(raw)) continue
|
|
148
|
+
seen.add(raw)
|
|
149
|
+
urls.push(raw)
|
|
150
|
+
}
|
|
151
|
+
return urls
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function componentContractsForArtifact(filePath: string): DesignComponentContract[] {
|
|
155
|
+
const designName = designNameFromPreviewPath(filePath)
|
|
156
|
+
try {
|
|
157
|
+
return extractDesignComponentContracts(designName || activeDesign())
|
|
158
|
+
} catch {
|
|
159
|
+
return []
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function shouldRunArtifactCompliance(filePath: string): boolean {
|
|
164
|
+
return !isDesignPreviewFile(filePath)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isDesignPreviewFile(filePath: string): boolean {
|
|
168
|
+
const normalizedPath = filePath.replace(/\\/g, "/")
|
|
169
|
+
if (basename(normalizedPath) !== "preview.html") return false
|
|
170
|
+
const parts = dirname(normalizedPath).split("/")
|
|
171
|
+
return parts.length >= 2 && parts[parts.length - 2] === "designs"
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function designNameFromPreviewPath(filePath: string): string | undefined {
|
|
175
|
+
const normalizedPath = filePath.replace(/\\/g, "/")
|
|
176
|
+
if (basename(normalizedPath) !== "preview.html") return undefined
|
|
177
|
+
const parts = dirname(normalizedPath).split("/")
|
|
178
|
+
if (parts.length >= 2 && parts[parts.length - 2] === "designs") return parts[parts.length - 1]
|
|
179
|
+
return undefined
|
|
180
|
+
}
|
|
181
|
+
|
|
72
182
|
export function formatArtifactQAReport(report: ArtifactQAReport): string {
|
|
73
183
|
const heading = report.passed ? "Artifact QA: PASSED" : "Artifact QA: FAILED"
|
|
74
184
|
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 */
|
package/lib/qa/compliance.ts
CHANGED
|
@@ -103,9 +103,13 @@ function extractCssDefinedClasses(html: string): ClassUse[] {
|
|
|
103
103
|
while ((styleMatch = styleRe.exec(html)) !== null) {
|
|
104
104
|
const styleBody = styleMatch[1]
|
|
105
105
|
const bodyOffset = styleMatch.index + styleMatch[0].indexOf(styleBody)
|
|
106
|
+
const scanBody = styleBody
|
|
107
|
+
.replace(/url\([^)]*\)/gi, "url()")
|
|
108
|
+
.replace(/"[^"]*"/g, '""')
|
|
109
|
+
.replace(/'[^']*'/g, "''")
|
|
106
110
|
classRe.lastIndex = 0
|
|
107
111
|
let classMatch: RegExpExecArray | null
|
|
108
|
-
while ((classMatch = classRe.exec(
|
|
112
|
+
while ((classMatch = classRe.exec(scanBody)) !== null) {
|
|
109
113
|
const cls = classMatch[1]
|
|
110
114
|
if (classes.has(cls)) continue
|
|
111
115
|
|
|
@@ -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
|
+
}
|