@cyber-dash-tech/revela 0.17.0 → 0.17.2
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 +45 -567
- package/README.zh-CN.md +47 -535
- package/designs/monet/DESIGN.md +1 -1
- package/designs/starter/DESIGN.md +1 -1
- package/designs/summit/DESIGN.md +1 -1
- package/lib/commands/help.ts +4 -4
- package/lib/commands/init.ts +9 -7
- package/lib/commands/narrative.ts +13 -4
- package/lib/commands/pdf.ts +24 -15
- package/lib/commands/pptx.ts +2 -16
- package/lib/commands/research.ts +2 -0
- package/lib/commands/review.ts +81 -93
- package/lib/deck-html/contract.ts +26 -10
- package/lib/decks-state.ts +75 -86
- package/lib/edit/deck-state.ts +3 -111
- package/lib/edit/open.ts +2 -2
- package/lib/edit/resolve-deck.ts +14 -24
- package/lib/inspect/open.ts +2 -2
- package/lib/media/download.ts +23 -3
- package/lib/media/save.ts +1 -0
- package/lib/media/types.ts +1 -0
- package/lib/narrative-state/deck-plan-artifact.ts +584 -0
- package/lib/narrative-state/display.ts +74 -4
- package/lib/narrative-state/map-html.ts +242 -107
- package/lib/narrative-state/render-plan.ts +649 -44
- package/lib/narrative-state/research-gaps.ts +5 -2
- package/lib/narrative-vault/compile.ts +16 -1
- package/lib/narrative-vault/types.ts +4 -2
- package/lib/qa/checks.ts +206 -5
- package/lib/qa/measure.ts +63 -1
- package/lib/refine/open.ts +2 -2
- package/lib/refine/server.ts +157 -20
- package/package.json +1 -1
- package/plugin.ts +2 -2
- package/skill/NARRATIVE_SKILL.md +19 -19
- package/skill/SKILL.md +99 -35
- package/tools/decks.ts +83 -51
- package/tools/narrative-view.ts +16 -0
|
@@ -328,8 +328,11 @@ function questionForClaimTarget(kind: ResearchTargetKind, claim: NarrativeClaim)
|
|
|
328
328
|
function requiredEvidenceForClaim(claim: NarrativeClaim | undefined): string[] {
|
|
329
329
|
const base = ["source", "quote/snippet", "support scope", "unsupported scope", "caveat", "strength"]
|
|
330
330
|
if (!claim) return base
|
|
331
|
-
|
|
332
|
-
|
|
331
|
+
const chapterReady = claim.importance === "central"
|
|
332
|
+
? ["framing/background support", "1-2 evidence bindings, cases, or quantitative signals", "implication, risk, or boundary material"]
|
|
333
|
+
: []
|
|
334
|
+
if (claim.unsupportedScope) return [...base, ...chapterReady, `address unsupported scope: ${claim.unsupportedScope}`]
|
|
335
|
+
return [...base, ...chapterReady]
|
|
333
336
|
}
|
|
334
337
|
|
|
335
338
|
function bindingFailuresForClaim(claim: NarrativeClaim): EvidenceBindingFailureReason[] {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { normalizeCanonicalNarrativeState } from "../narrative-state/normalize"
|
|
2
2
|
import { computeNarrativeHash } from "../narrative-state/hash"
|
|
3
|
+
import { readDeckPlanProjection } from "../narrative-state/deck-plan-artifact"
|
|
3
4
|
import type {
|
|
4
5
|
AudienceIntent,
|
|
5
6
|
DecisionIntent,
|
|
6
7
|
NarrativeApproval,
|
|
7
8
|
NarrativeClaim,
|
|
8
9
|
NarrativeClaimKind,
|
|
10
|
+
NarrativeClaimRelationType,
|
|
9
11
|
NarrativeEvidenceBinding,
|
|
10
12
|
NarrativeObjection,
|
|
11
13
|
NarrativeResearchGap,
|
|
@@ -89,7 +91,7 @@ export function compileNarrativeVault(workspaceRoot: string, options: CompileNar
|
|
|
89
91
|
risks: docs.filter((doc) => typeField(doc) === "risk").map((doc) => compileRisk(doc, relationTargets)),
|
|
90
92
|
researchGaps: docs.filter((doc) => typeField(doc) === "research-gap").map((doc) => compileResearchGap(doc, options.now, relationTargets)),
|
|
91
93
|
claimRelations: relations
|
|
92
|
-
.filter((relation) => byId.get(relation.fromId) && typeField(byId.get(relation.fromId)) === "claim" && byId.get(relation.toId) && typeField(byId.get(relation.toId)) === "claim")
|
|
94
|
+
.filter((relation): relation is VaultRelation & { relation: NarrativeClaimRelationType } => isNarrativeClaimRelationType(relation.relation) && Boolean(byId.get(relation.fromId)) && typeField(byId.get(relation.fromId)) === "claim" && Boolean(byId.get(relation.toId)) && typeField(byId.get(relation.toId)) === "claim")
|
|
93
95
|
.map((relation) => ({ id: relation.id ?? `${relation.fromId}:${relation.relation}:${relation.toId}`, fromClaimId: relation.fromId, toClaimId: relation.toId, relation: relation.relation, rationale: relation.rationale })),
|
|
94
96
|
approvals: options.fallbackApprovals ?? [],
|
|
95
97
|
updatedAt: options.now ?? new Date().toISOString(),
|
|
@@ -108,6 +110,15 @@ export function compileNarrativeVault(workspaceRoot: string, options: CompileNar
|
|
|
108
110
|
nodes: nodeDocs.map((doc) => ({ id: stringField(doc, "id"), type: typeField(doc), file: doc.relativePath })),
|
|
109
111
|
relations,
|
|
110
112
|
}
|
|
113
|
+
if (normalized) {
|
|
114
|
+
const knownNodeIds = new Set(graph.nodes.map((node) => node.id))
|
|
115
|
+
const deckPlan = readDeckPlanProjection(workspaceRoot, { narrativeHash: computeNarrativeHash(normalized), knownNodeIds })
|
|
116
|
+
if (deckPlan) {
|
|
117
|
+
graph.nodes.push(...deckPlan.graphNodes)
|
|
118
|
+
graph.relations.push(...deckPlan.graphRelations)
|
|
119
|
+
for (const diagnostic of deckPlan.diagnostics) diagnostics.push({ severity: diagnostic.severity, code: diagnostic.code, message: diagnostic.message, file: diagnostic.file, nodeId: diagnostic.nodeId })
|
|
120
|
+
}
|
|
121
|
+
}
|
|
111
122
|
return { ok: !diagnostics.some((diagnostic) => diagnostic.severity === "error") && Boolean(normalized), narrative: normalized, diagnostics, graph }
|
|
112
123
|
}
|
|
113
124
|
|
|
@@ -227,6 +238,10 @@ function isVaultNodeType(value: string): value is VaultNodeType {
|
|
|
227
238
|
return value === "research-gap" || ["index", "audience", "decision", "thesis", "claim", "evidence", "objection", "risk"].includes(value)
|
|
228
239
|
}
|
|
229
240
|
|
|
241
|
+
function isNarrativeClaimRelationType(value: string): value is NarrativeClaimRelationType {
|
|
242
|
+
return ["leads_to", "supports", "depends_on", "contrasts_with", "constrains", "answers"].includes(value)
|
|
243
|
+
}
|
|
244
|
+
|
|
230
245
|
function illegalRelationReason(fromType: VaultNodeType, toType: VaultNodeType, relation: string): string | undefined {
|
|
231
246
|
if (fromType !== "claim") {
|
|
232
247
|
const allowedNonClaim: Record<string, VaultNodeType[]> = {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { NarrativeClaimRelationType, NarrativeStateV1 } from "../narrative-state/types"
|
|
2
2
|
|
|
3
3
|
export type VaultNodeType = "index" | "audience" | "decision" | "thesis" | "claim" | "evidence" | "objection" | "risk" | "research-gap"
|
|
4
|
+
export type WorkspaceGraphNodeType = VaultNodeType | "deck-plan" | "deck-plan-slide"
|
|
5
|
+
export type WorkspaceGraphRelationType = NarrativeClaimRelationType | "uses_claim" | "uses_evidence" | "addresses_risk" | "answers_objection" | "mentions_gap"
|
|
4
6
|
|
|
5
7
|
export type VaultDiagnosticSeverity = "error" | "warning"
|
|
6
8
|
|
|
@@ -15,7 +17,7 @@ export interface VaultDiagnostic {
|
|
|
15
17
|
export interface VaultRelation {
|
|
16
18
|
id?: string
|
|
17
19
|
fromId: string
|
|
18
|
-
relation:
|
|
20
|
+
relation: WorkspaceGraphRelationType
|
|
19
21
|
toId: string
|
|
20
22
|
rationale?: string
|
|
21
23
|
file: string
|
|
@@ -39,6 +41,6 @@ export interface NarrativeVaultCompileResult {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
export interface NarrativeVaultGraph {
|
|
42
|
-
nodes: Array<{ id: string; type:
|
|
44
|
+
nodes: Array<{ id: string; type: WorkspaceGraphNodeType; file: string }>
|
|
43
45
|
relations: VaultRelation[]
|
|
44
46
|
}
|
package/lib/qa/checks.ts
CHANGED
|
@@ -23,9 +23,10 @@ import { CANVAS_W, CANVAS_H } from "./measure"
|
|
|
23
23
|
export type IssueSeverity = "error" | "warning" | "info"
|
|
24
24
|
|
|
25
25
|
export interface LayoutIssue {
|
|
26
|
-
type: "canvas" | "scrollbar" | "overflow" | "text_overflow" | "density" | "balance" | "symmetry" | "rhythm" | "compliance" | "asset"
|
|
26
|
+
type: "canvas" | "scrollbar" | "navigation" | "overflow" | "text_overflow" | "overlap" | "density" | "balance" | "symmetry" | "rhythm" | "compliance" | "asset"
|
|
27
27
|
/** Sub-category within the dimension */
|
|
28
|
-
sub?: "size_mismatch" | "page_scroll" | "text_clipped" | "thin_content"
|
|
28
|
+
sub?: "size_mismatch" | "page_scroll" | "fixed_overlay_slides" | "hidden_paging" | "unreachable_slides" | "text_clipped" | "thin_content"
|
|
29
|
+
| "element_collision" | "major_overlap" | "possible_overlay"
|
|
29
30
|
| "centroid_offset" | "bottom_gap" | "sparse"
|
|
30
31
|
| "height_mismatch" | "density_mismatch"
|
|
31
32
|
| "gap_variance"
|
|
@@ -81,6 +82,12 @@ const T = {
|
|
|
81
82
|
CANVAS_TOLERANCE: 1,
|
|
82
83
|
DENSITY_MIN_TEXT_POINTS: 70,
|
|
83
84
|
DENSITY_MIN_UNITS: 2,
|
|
85
|
+
OVERLAP_MIN_AREA: 1600,
|
|
86
|
+
OVERLAP_MIN_ELEMENT_AREA: 5000,
|
|
87
|
+
OVERLAP_WARN_RATIO: 0.08,
|
|
88
|
+
OVERLAP_ERROR_RATIO: 0.18,
|
|
89
|
+
OVERLAP_TEXT_WARN_RATIO: 0.05,
|
|
90
|
+
OVERLAP_TEXT_ERROR_RATIO: 0.12,
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
// ── Geometry helpers ──────────────────────────────────────────────────────────
|
|
@@ -205,15 +212,78 @@ function checkCanvas(metrics: SlideMetrics): LayoutIssue[] {
|
|
|
205
212
|
}
|
|
206
213
|
|
|
207
214
|
function checkScrollbars(metrics: SlideMetrics): LayoutIssue[] {
|
|
208
|
-
|
|
215
|
+
const scrollbars = metrics.scrollbars
|
|
216
|
+
const totalSlides = metrics.navigation?.totalSlides ?? 1
|
|
217
|
+
const hasAllowedMultiSlideVerticalScroll = totalSlides > 1
|
|
218
|
+
|
|
219
|
+
if (scrollbars) {
|
|
220
|
+
const hasBlockingScrollbars =
|
|
221
|
+
scrollbars.documentHorizontal ||
|
|
222
|
+
scrollbars.bodyHorizontal ||
|
|
223
|
+
scrollbars.slideHorizontal ||
|
|
224
|
+
scrollbars.slideVertical ||
|
|
225
|
+
(!hasAllowedMultiSlideVerticalScroll && (scrollbars.documentVertical || scrollbars.bodyVertical))
|
|
226
|
+
|
|
227
|
+
if (!hasBlockingScrollbars) return []
|
|
228
|
+
} else if (!metrics.hasScrollbars) {
|
|
229
|
+
return []
|
|
230
|
+
}
|
|
231
|
+
|
|
209
232
|
return [{
|
|
210
233
|
type: "scrollbar",
|
|
211
234
|
sub: "page_scroll",
|
|
212
235
|
severity: "error",
|
|
213
|
-
detail: "Rendered slide/page has scrollbars at 1920x1080.
|
|
236
|
+
detail: "Rendered slide/page has blocking scrollbars at 1920x1080. Normal vertical document scroll is allowed for multi-slide navigation, but horizontal document/body scroll and slide-internal scroll must be removed.",
|
|
214
237
|
}]
|
|
215
238
|
}
|
|
216
239
|
|
|
240
|
+
function checkNavigationModel(allMetrics: SlideMetrics[]): LayoutIssue[] {
|
|
241
|
+
if (allMetrics.length <= 1) return []
|
|
242
|
+
|
|
243
|
+
const nav = allMetrics.map((metrics) => metrics.navigation).filter((item): item is NonNullable<SlideMetrics["navigation"]> => Boolean(item))
|
|
244
|
+
if (nav.length <= 1) return []
|
|
245
|
+
|
|
246
|
+
const positioned = nav.filter((item) => item.position === "fixed" || item.position === "absolute")
|
|
247
|
+
const hiddenByAria = nav.filter((item) => item.ariaHidden === "true" || item.visibility === "hidden" || item.display === "none")
|
|
248
|
+
const uniqueTops = new Set(nav.map((item) => Math.round(item.initialTop)))
|
|
249
|
+
const stacked = uniqueTops.size <= 1
|
|
250
|
+
const documentCanScroll = nav[0].documentScrollHeight > nav[0].viewportHeight + 2
|
|
251
|
+
const overflowHidden = nav[0].bodyOverflowY === "hidden" || nav[0].documentOverflowY === "hidden"
|
|
252
|
+
|
|
253
|
+
const issues: LayoutIssue[] = []
|
|
254
|
+
if (positioned.length === nav.length && stacked) {
|
|
255
|
+
issues.push({
|
|
256
|
+
type: "navigation",
|
|
257
|
+
sub: "fixed_overlay_slides",
|
|
258
|
+
severity: "error",
|
|
259
|
+
detail: "Slides are stacked with fixed/absolute positioning. Revela decks must keep each .slide in normal document flow so scrollIntoView and keyboard navigation can reach every slide.",
|
|
260
|
+
data: { slideCount: nav.length, positionedSlides: positioned.length },
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (positioned.length === nav.length && hiddenByAria.length > 0) {
|
|
265
|
+
issues.push({
|
|
266
|
+
type: "navigation",
|
|
267
|
+
sub: "hidden_paging",
|
|
268
|
+
severity: "error",
|
|
269
|
+
detail: "Slides use aria-hidden/visibility toggles with fixed overlay pagination. Do not make slide visibility depend on aria-hidden; keep slides visible in normal flow and navigate with scrollIntoView.",
|
|
270
|
+
data: { hiddenSlides: hiddenByAria.length, slideCount: nav.length },
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!documentCanScroll && overflowHidden && allMetrics.length > 1) {
|
|
275
|
+
issues.push({
|
|
276
|
+
type: "navigation",
|
|
277
|
+
sub: "unreachable_slides",
|
|
278
|
+
severity: "error",
|
|
279
|
+
detail: "Multi-slide deck disables document vertical scrolling. Normal slide flow needs enough document height for all slides; fix slide overflow locally instead of hiding body/html overflow.",
|
|
280
|
+
data: { slideCount: nav.length, documentScrollHeight: nav[0].documentScrollHeight, viewportHeight: nav[0].viewportHeight },
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return issues
|
|
285
|
+
}
|
|
286
|
+
|
|
217
287
|
/**
|
|
218
288
|
* Check 1: Overflow — elements extending beyond canvas boundaries.
|
|
219
289
|
* Hard correctness check; applies to all slide types.
|
|
@@ -270,6 +340,132 @@ function checkTextOverflow(metrics: SlideMetrics): LayoutIssue[] {
|
|
|
270
340
|
return issues
|
|
271
341
|
}
|
|
272
342
|
|
|
343
|
+
const SEMANTIC_COMPONENT_CLASSES = [
|
|
344
|
+
"box",
|
|
345
|
+
"text-panel",
|
|
346
|
+
"media",
|
|
347
|
+
"echart-panel",
|
|
348
|
+
"data-table",
|
|
349
|
+
"stat-card",
|
|
350
|
+
"quote",
|
|
351
|
+
"hero",
|
|
352
|
+
"toc",
|
|
353
|
+
"steps",
|
|
354
|
+
"roadmap-horizontal",
|
|
355
|
+
"roadmap-vertical",
|
|
356
|
+
]
|
|
357
|
+
|
|
358
|
+
const DECORATIVE_CLASSES = [
|
|
359
|
+
"page-number",
|
|
360
|
+
"brand-watermark",
|
|
361
|
+
"watermark",
|
|
362
|
+
"background",
|
|
363
|
+
"decorative",
|
|
364
|
+
"motif",
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
function checkElementOverlap(metrics: SlideMetrics): LayoutIssue[] {
|
|
368
|
+
const candidates = overlapCandidates(metrics.elements)
|
|
369
|
+
const issues: LayoutIssue[] = []
|
|
370
|
+
|
|
371
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
372
|
+
for (let j = i + 1; j < candidates.length; j++) {
|
|
373
|
+
const a = candidates[i]
|
|
374
|
+
const b = candidates[j]
|
|
375
|
+
if (isIntentionalOverlayPair(a, b)) continue
|
|
376
|
+
|
|
377
|
+
const overlap = intersection(a.rect, b.rect)
|
|
378
|
+
if (!overlap) continue
|
|
379
|
+
|
|
380
|
+
const intersectionArea = overlap.width * overlap.height
|
|
381
|
+
if (intersectionArea < T.OVERLAP_MIN_AREA) continue
|
|
382
|
+
|
|
383
|
+
const minArea = Math.min(area(a.rect), area(b.rect))
|
|
384
|
+
if (minArea <= 0) continue
|
|
385
|
+
|
|
386
|
+
const ratio = intersectionArea / minArea
|
|
387
|
+
const textSensitive = hasText(a) || hasText(b)
|
|
388
|
+
const errorRatio = textSensitive ? T.OVERLAP_TEXT_ERROR_RATIO : T.OVERLAP_ERROR_RATIO
|
|
389
|
+
const warnRatio = textSensitive ? T.OVERLAP_TEXT_WARN_RATIO : T.OVERLAP_WARN_RATIO
|
|
390
|
+
if (ratio < warnRatio) continue
|
|
391
|
+
|
|
392
|
+
const severity: IssueSeverity = ratio >= errorRatio ? "error" : "warning"
|
|
393
|
+
issues.push({
|
|
394
|
+
type: "overlap",
|
|
395
|
+
sub: severity === "error" ? "element_collision" : "possible_overlay",
|
|
396
|
+
severity,
|
|
397
|
+
detail: `Elements \`${a.selector}\` and \`${b.selector}\` overlap by ${Math.round(ratio * 100)}% of the smaller element (${Math.round(intersectionArea)}px²). Separate the components, reduce content, or choose a layout with more space.`,
|
|
398
|
+
data: {
|
|
399
|
+
elementA: a.selector,
|
|
400
|
+
elementB: b.selector,
|
|
401
|
+
overlapRatioPct: Math.round(ratio * 100),
|
|
402
|
+
intersectionArea: Math.round(intersectionArea),
|
|
403
|
+
rectA: rectData(a.rect),
|
|
404
|
+
rectB: rectData(b.rect),
|
|
405
|
+
},
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return issues
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function overlapCandidates(elements: ElementInfo[]): ElementInfo[] {
|
|
414
|
+
const semantic = elements.flatMap((element) => semanticDescendants(element))
|
|
415
|
+
const base = semantic.length > 1 ? semantic : elements.filter((element) => element.visible)
|
|
416
|
+
return base.filter((element) => {
|
|
417
|
+
if (!element.visible) return false
|
|
418
|
+
if (area(element.rect) < T.OVERLAP_MIN_ELEMENT_AREA) return false
|
|
419
|
+
if (isDecorative(element)) return false
|
|
420
|
+
return true
|
|
421
|
+
})
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function semanticDescendants(element: ElementInfo): ElementInfo[] {
|
|
425
|
+
if (!element.visible || isDecorative(element)) return []
|
|
426
|
+
if (isSemanticComponent(element)) return [element]
|
|
427
|
+
return element.children.flatMap((child) => semanticDescendants(child))
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function isSemanticComponent(element: ElementInfo): boolean {
|
|
431
|
+
return element.classList.some((className) =>
|
|
432
|
+
SEMANTIC_COMPONENT_CLASSES.includes(className) || className.startsWith("roadmap-")
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function isDecorative(element: ElementInfo): boolean {
|
|
437
|
+
return element.classList.some((className) =>
|
|
438
|
+
DECORATIVE_CLASSES.includes(className) || className.startsWith("decorative-") || className.startsWith("background-")
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function isIntentionalOverlayPair(a: ElementInfo, b: ElementInfo): boolean {
|
|
443
|
+
const classes = new Set([...a.classList, ...b.classList])
|
|
444
|
+
return classes.has("hero") && (classes.has("media") || classes.has("text-panel"))
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function intersection(a: Rect, b: Rect): Rect | undefined {
|
|
448
|
+
const left = Math.max(a.left, b.left)
|
|
449
|
+
const top = Math.max(a.top, b.top)
|
|
450
|
+
const right = Math.min(a.right, b.right)
|
|
451
|
+
const bottom = Math.min(a.bottom, b.bottom)
|
|
452
|
+
if (right <= left || bottom <= top) return undefined
|
|
453
|
+
return { left, top, right, bottom, width: right - left, height: bottom - top }
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function area(rect: Rect): number {
|
|
457
|
+
return Math.max(0, rect.width) * Math.max(0, rect.height)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function hasText(element: ElementInfo): boolean {
|
|
461
|
+
if (element.text?.trim()) return true
|
|
462
|
+
return element.children.some(hasText)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function rectData(rect: Rect): string {
|
|
466
|
+
return `${Math.round(rect.left)},${Math.round(rect.top)},${Math.round(rect.right)},${Math.round(rect.bottom)}`
|
|
467
|
+
}
|
|
468
|
+
|
|
273
469
|
function checkContentDensity(metrics: SlideMetrics): LayoutIssue[] {
|
|
274
470
|
if (!metrics.slideQa) return []
|
|
275
471
|
const { bodyTextPoints, contentUnits, supportReferences } = metrics.contentStats
|
|
@@ -621,13 +817,16 @@ export function runChecks(
|
|
|
621
817
|
_options?: RunChecksOptions,
|
|
622
818
|
): QAReport {
|
|
623
819
|
const slides: SlideReport[] = []
|
|
820
|
+
const navigationIssues = checkNavigationModel(allMetrics)
|
|
624
821
|
|
|
625
822
|
for (const metrics of allMetrics) {
|
|
626
823
|
const issues: LayoutIssue[] = [
|
|
824
|
+
...(metrics.index === 0 ? navigationIssues : []),
|
|
627
825
|
...checkCanvas(metrics),
|
|
628
826
|
...checkScrollbars(metrics),
|
|
629
827
|
...checkOverflow(metrics),
|
|
630
828
|
...checkTextOverflow(metrics),
|
|
829
|
+
...checkElementOverlap(metrics),
|
|
631
830
|
...checkContentDensity(metrics),
|
|
632
831
|
]
|
|
633
832
|
|
|
@@ -693,9 +892,11 @@ export function formatReport(report: QAReport): string {
|
|
|
693
892
|
``,
|
|
694
893
|
`Please fix the above hard-error issues in the HTML file. For each issue type:`,
|
|
695
894
|
`- **canvas**: ensure each slide and .slide-canvas render exactly 1920x1080px, not merely any 16:9 size.`,
|
|
696
|
-
`- **scrollbar**: remove document/body
|
|
895
|
+
`- **scrollbar**: remove horizontal document/body scrolling and slide-internal scrolling. Multi-slide decks may use normal vertical document scroll for navigation.`,
|
|
896
|
+
`- **navigation**: keep every .slide in normal document flow; do not stack slides with fixed/absolute positioning or rely on aria-hidden/visibility toggles for pagination. Use scrollIntoView-based navigation.`,
|
|
697
897
|
`- **overflow**: reduce font size, padding, or content amount for the affected element.`,
|
|
698
898
|
`- **text_overflow**: increase the text container size, reduce copy, or adjust font/line-height so text is not clipped.`,
|
|
899
|
+
`- **overlap**: separate overlapping components, reduce copy, resize media/table/chart blocks, or use a layout with more space.`,
|
|
699
900
|
`- **density/thin_content**: add concrete claim/evidence points, metrics, chart/table support, or source/caveat text. This is a warning for content substance, not a blank-space failure.`,
|
|
700
901
|
`- **compliance/unknown_class**: an HTML element uses a CSS class not defined in the active design. Replace it with a class from the Component Index or Layout Index. Fetch the component/layout details with the \`revela-designs\` tool if needed.`,
|
|
701
902
|
`- **compliance/novel_css_rule**: \`<style>\` defines a CSS class that is not part of the active design. Remove the custom rule and use the design's existing component styles. For minor spacing/sizing adjustments, use inline \`style=""\` instead.`,
|
package/lib/qa/measure.ts
CHANGED
|
@@ -64,6 +64,29 @@ export interface SlideContentStats {
|
|
|
64
64
|
supportReferences: number
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
export interface ScrollbarMetrics {
|
|
68
|
+
documentHorizontal: boolean
|
|
69
|
+
documentVertical: boolean
|
|
70
|
+
bodyHorizontal: boolean
|
|
71
|
+
bodyVertical: boolean
|
|
72
|
+
slideHorizontal: boolean
|
|
73
|
+
slideVertical: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SlideNavigationMetrics {
|
|
77
|
+
totalSlides: number
|
|
78
|
+
initialTop: number
|
|
79
|
+
initialLeft: number
|
|
80
|
+
position: string
|
|
81
|
+
visibility: string
|
|
82
|
+
display: string
|
|
83
|
+
ariaHidden: string | null
|
|
84
|
+
bodyOverflowY: string
|
|
85
|
+
documentOverflowY: string
|
|
86
|
+
documentScrollHeight: number
|
|
87
|
+
viewportHeight: number
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
export interface SlideMetrics {
|
|
68
91
|
/** 0-based slide index */
|
|
69
92
|
index: number
|
|
@@ -82,6 +105,10 @@ export interface SlideMetrics {
|
|
|
82
105
|
slideRect: Rect
|
|
83
106
|
/** whether document/body/slide has scrollbars at 1920x1080 */
|
|
84
107
|
hasScrollbars: boolean
|
|
108
|
+
/** detailed scrollbar source signals for document/body/slide */
|
|
109
|
+
scrollbars?: ScrollbarMetrics
|
|
110
|
+
/** deck navigation model signals captured before per-slide scrolling */
|
|
111
|
+
navigation?: SlideNavigationMetrics
|
|
85
112
|
/** top-level visible children of .slide-canvas */
|
|
86
113
|
elements: ElementInfo[]
|
|
87
114
|
/** union bounding box of all visible leaf elements */
|
|
@@ -163,6 +190,31 @@ export async function measureSlides(htmlFilePath: string): Promise<MeasurementRe
|
|
|
163
190
|
() => document.querySelectorAll(".slide").length
|
|
164
191
|
)
|
|
165
192
|
|
|
193
|
+
const navigationData = await page.evaluate(() => {
|
|
194
|
+
const doc = document.documentElement
|
|
195
|
+
const body = document.body
|
|
196
|
+
const docStyle = window.getComputedStyle(doc)
|
|
197
|
+
const bodyStyle = window.getComputedStyle(body)
|
|
198
|
+
const slides = Array.from(document.querySelectorAll(".slide")) as HTMLElement[]
|
|
199
|
+
return slides.map((slide) => {
|
|
200
|
+
const rect = slide.getBoundingClientRect()
|
|
201
|
+
const style = window.getComputedStyle(slide)
|
|
202
|
+
return {
|
|
203
|
+
totalSlides: slides.length,
|
|
204
|
+
initialTop: rect.top,
|
|
205
|
+
initialLeft: rect.left,
|
|
206
|
+
position: style.position,
|
|
207
|
+
visibility: style.visibility,
|
|
208
|
+
display: style.display,
|
|
209
|
+
ariaHidden: slide.getAttribute("aria-hidden"),
|
|
210
|
+
bodyOverflowY: bodyStyle.overflowY,
|
|
211
|
+
documentOverflowY: docStyle.overflowY,
|
|
212
|
+
documentScrollHeight: doc.scrollHeight,
|
|
213
|
+
viewportHeight: window.innerHeight,
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
166
218
|
const metrics: SlideMetrics[] = []
|
|
167
219
|
|
|
168
220
|
for (let idx = 0; idx < slideCount; idx++) {
|
|
@@ -370,6 +422,15 @@ export async function measureSlides(htmlFilePath: string): Promise<MeasurementRe
|
|
|
370
422
|
slideEl.scrollWidth > slideEl.clientWidth + 2 ||
|
|
371
423
|
slideEl.scrollHeight > slideEl.clientHeight + 2
|
|
372
424
|
|
|
425
|
+
const scrollbars = {
|
|
426
|
+
documentHorizontal: doc.scrollWidth > window.innerWidth + 2,
|
|
427
|
+
documentVertical: doc.scrollHeight > window.innerHeight + 2,
|
|
428
|
+
bodyHorizontal: body.scrollWidth > window.innerWidth + 2,
|
|
429
|
+
bodyVertical: body.scrollHeight > window.innerHeight + 2,
|
|
430
|
+
slideHorizontal: slideEl.scrollWidth > slideEl.clientWidth + 2,
|
|
431
|
+
slideVertical: slideEl.scrollHeight > slideEl.clientHeight + 2,
|
|
432
|
+
}
|
|
433
|
+
|
|
373
434
|
const titleEl = canvas.querySelector("h1, h2")
|
|
374
435
|
const title = titleEl
|
|
375
436
|
? (titleEl.textContent || "").replace(/\s+/g, " ").trim().slice(0, 80)
|
|
@@ -382,6 +443,7 @@ export async function measureSlides(htmlFilePath: string): Promise<MeasurementRe
|
|
|
382
443
|
canvasRect,
|
|
383
444
|
slideRect,
|
|
384
445
|
hasScrollbars,
|
|
446
|
+
scrollbars,
|
|
385
447
|
elements,
|
|
386
448
|
contentRect: unionRect(elements),
|
|
387
449
|
contentStats: { bodyTextPoints, contentUnits, supportReferences },
|
|
@@ -390,7 +452,7 @@ export async function measureSlides(htmlFilePath: string): Promise<MeasurementRe
|
|
|
390
452
|
idx
|
|
391
453
|
)
|
|
392
454
|
|
|
393
|
-
if (slideData) metrics.push(slideData as SlideMetrics)
|
|
455
|
+
if (slideData) metrics.push({ ...(slideData as SlideMetrics), navigation: navigationData[idx] })
|
|
394
456
|
}
|
|
395
457
|
|
|
396
458
|
// Extract all CSS class names defined in <style> blocks.
|
package/lib/refine/open.ts
CHANGED
|
@@ -78,8 +78,8 @@ function openRefineDeckInternal(
|
|
|
78
78
|
return {
|
|
79
79
|
deck,
|
|
80
80
|
url,
|
|
81
|
-
source: deck.source === "
|
|
82
|
-
stateNote: preflight.changed ? "Deck
|
|
81
|
+
source: deck.source === "file-path" ? "file path" : "discovered deck file",
|
|
82
|
+
stateNote: preflight.changed ? "Deck file preflight updated runtime state." : "Deck review uses the selected HTML artifact directly.",
|
|
83
83
|
preflightChanged: preflight.changed,
|
|
84
84
|
reusedSession: session.reused,
|
|
85
85
|
liveSession: session.live,
|