diff --git a/README.md b/README.md index 7869f893ab..026f545222 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ specify init my-project --integration codex --ignore-agent-tools | 默认扩展 | `arch` | `extensions/arch` | 生成或反向生成面向 `/speckit.plan` 的架构规划契约。 | | 默认扩展 | `discovery` | `extensions/discovery` | 在正式计划前做可行性、技术选型、旧代码评估、接口理解、PoC 和场景化技术决策。 | | 默认扩展 | `inception` | `extensions/inception` | 在正式 SDD 前通过对话收敛产品 UC、wireflow、架构边界和真实 API POC 证据。 | -| 默认扩展 | `intake` | `extensions/intake` | 把 PRD、设计稿、Figma、视觉规格资产包、preview 覆盖证据、测试用例等来源归一化为 SDD 可消费的证据包。 | +| 默认扩展 | `intake` | `extensions/intake` | 把 PRD、设计稿、Figma、最终静态 HTML 交付、测试用例等来源归一化为 SDD 可消费的证据包。 | | 默认扩展 | `preview` | `extensions/preview` | 从规格和计划生成低、中、高保真 Markdown 或自包含 HTML 预览。 | | 默认扩展 | `repository-governance` | `extensions/repository-governance` | 生成仓库治理 SSOT,帮助 agent 明确目录责任、读取顺序和事实证据。 | | 默认预设 | `workflow-preset` | `presets/workflow-preset` | 强化 BDD、NFR、UIF、设计产物、任务验证策略和 implement handoff 编排。 | @@ -173,7 +173,7 @@ inception/arch/api-poc-runs/ - PRD、产品说明、Markdown、PDF、导出的文档。 - 图片、线框图、设计 PDF、Figma 文件、Figma 页面或节点。 -- 视觉规格结构化资产包,以及 Figma 派生的组件矩阵 preview 覆盖证据。 +- 设计稿派生的视觉 IR、操作回放、动效锚点、视口截图和最终静态 HTML 交付证据。 - 既有测试、Gherkin、手工测试用例、QA 导出、测试管理表格。 主要产物: @@ -181,8 +181,8 @@ inception/arch/api-poc-runs/ ```text specs//intake/prd/ specs//intake/visual-design/ -specs//intake/visual-design/visual-spec-package/ -specs//intake/visual-design/previews/ +specs//intake/visual-design/visual-ir/ +specs//intake/visual-design/delivery/ specs//intake/test-cases/ ``` diff --git a/docs/community/extensions.md b/docs/community/extensions.md index cc51e4e71f..0b247d15e4 100644 --- a/docs/community/extensions.md +++ b/docs/community/extensions.md @@ -57,7 +57,7 @@ The following community-contributed extensions are available in [`catalog.commun | GitHub Issues Integration 2 | Creates and syncs local specs from an existing GitHub issue | `integration` | Read+Write | [spec-kit-issue](https://github.com/aaronrsun/spec-kit-issue) | | Golden Demo | Extracts acceptance criteria from specs, builds test vectors, and produces a behavioral drift report — complementary to Architecture Guard and CDD | `docs` | Read+Write | [spec-kit-golden-demo](https://github.com/jasstt/spec-kit-golden-demo) | | Improve Extension | Audits any codebase as a senior advisor and writes prioritized, self-contained spec prompts under specs/ that the spec-kit lifecycle can process | `process` | Read+Write | [spec-kit-improve](https://github.com/d0whc3r/spec-kit-improve) | -| Intake | Normalize PRD, visual design/spec packages, preview evidence, and test-case evidence into SDD-ready intake artifacts | `docs` | Read+Write | [spec-kit-intake](https://github.com/bigsmartben/spec-kit-intake) | +| Intake | Normalize PRD, visual design, final static HTML delivery, and test-case evidence into SDD-ready intake artifacts | `docs` | Read+Write | [spec-kit-intake](https://github.com/bigsmartben/spec-kit-intake) | | Intelligent Agent Orchestrator | Cross-catalog agent discovery and intelligent prompt-to-command routing | `process` | Read+Write | [spec-kit-orchestrator](https://github.com/pragya247/spec-kit-orchestrator) | | Iterate | Iterate on spec documents with a two-phase define-and-apply workflow — refine specs mid-implementation and go straight back to building | `docs` | Read+Write | [spec-kit-iterate](https://github.com/imviancagrace/spec-kit-iterate) | | Jira Integration | Create Jira Epics, Stories, and Issues from spec-kit specifications and task breakdowns with configurable hierarchy and custom field support | `integration` | Read+Write | [spec-kit-jira](https://github.com/mbachorik/spec-kit-jira) | diff --git a/extensions/catalog.json b/extensions/catalog.json index a366100d5e..7bc6922233 100644 --- a/extensions/catalog.json +++ b/extensions/catalog.json @@ -1,6 +1,6 @@ { "schema_version": "1.0", - "updated_at": "2026-07-06T00:00:00Z", + "updated_at": "2026-07-07T00:00:00Z", "catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json", "extensions": { "agent-context": { @@ -95,8 +95,8 @@ "intake": { "name": "Intake", "id": "intake", - "version": "0.1.5", - "description": "Normalize PRD, visual design/spec packages, preview evidence, and test-case evidence into SDD-ready intake artifacts", + "version": "0.2.0", + "description": "Normalize PRD, visual design, final static HTML delivery, and test-case evidence into SDD-ready intake artifacts", "author": "bigsmartben", "repository": "https://github.com/bigsmartben/spec-kit-intake", "license": "MIT", @@ -113,7 +113,8 @@ "sdd", "requirements", "validation", - "figma" + "figma", + "static-html" ] }, "preview": { diff --git a/extensions/intake/CHANGELOG.md b/extensions/intake/CHANGELOG.md index f38315cf0b..cb2b9972d3 100644 --- a/extensions/intake/CHANGELOG.md +++ b/extensions/intake/CHANGELOG.md @@ -1,62 +1,22 @@ -# Changelog +# Changelog -## [0.1.5] - 2026-07-03 +## [0.2.0] - 2026-07-07 -### Added - -- Added `capture_figma_metadata_shards.py` to stage sharded Figma `get_metadata` captures into canonical raw metadata shards, index, and node inventory artifacts without passing large provider responses through agent context. -- Added `normalize_figma_layout.py`, `figma-normalized-tree.yaml`, and schema-backed readiness checks for Figma rename, grouping, and visual re-sort normalization after raw metadata parity passes. -- Added HTML mock readiness checks that require rendered mock page surfaces, page visual-state enumerations, page IA matrices, component visual-state enumerations, component IA matrices with interaction refs, and coverage evidence conclusions. -- Added HTML mock readiness checks for adjacent visual spec package readiness, `visual-spec-package/visual-spec.yaml` refs, visualized component node kinds, and preview-local screenshot refs. -- Added structured UI/visual asset source-of-truth checks that block evidence packets, preview HTML, screenshots, and visual diffs from being used as `source_refs` or `evidence_refs`. - -### Changed - -- Repositioned `previews/preview.html` as the static HTML/CSS mock equivalent for the visual input design generated from upstream intake artifacts, with coverage YAML preserving machine-checkable traceability. -- Kept Figma layout normalization as a derived artifact that does not rewrite raw metadata or replace `visual-requirements.yaml` source-backed facts. - -## [0.1.4] - 2026-07-01 - -### Added - -- Added visual-design visual spec package artifacts for CI-friendly DOM, ARIA, token, state, locator, relation, and assertion evidence. -- Added visual spec package schemas and `validate_visual_spec_package.py` for source readiness, cross-reference, provider-evidence, product-ambiguity, locator, downstream-ownership, and CI assertion checks. -- Added `visual-design/previews/` with `preview.html` as the visual-equivalent HTML delivery and `component-coverage.yaml` / `viewport-coverage.yaml` for machine-checkable coverage evidence under `/speckit.intake.visual-design`. - -## [0.1.3] - 2026-06-29 - -### Added - -- Added Figma-derived HTML mock coverage evidence with component-state, page, asset, viewport, screenshot, and known-gap readiness checks. -- Added bounded visual inference statuses for irregular Figma and visual-design sources, including `candidate` and `unsupported` claim handling. -- Added readiness blocking for unbounded visual inference and unsupported visual claims. - -## [0.1.2] - 2026-06-23 - -### Changed - -- Added release installation and runtime dependency documentation. -- Shortened manifest description and reduced tags for community catalog submission. - -## [0.1.1] - 2026-06-23 - -### Changed - -- Added community catalog metadata for category and effect. +### Breaking Reset -## [0.1.0] - 2026-06-23 +- Rebuilt `/speckit.intake.visual-design` as the only external visual-intake entrypoint. Workflow phases are internal orchestration only; users do not invoke visual subcommands. +- Removed legacy preview/mock and visual spec package delivery surfaces from the extension source. The final visual deliverable is now `specs//intake/visual-design/delivery/index.html`. +- Replaced preview/spec-package readiness with static HTML delivery readiness across `README.md`, `extension.yml`, `config-template.yml`, contracts, schemas, validators, and tests. ### Added -- Initial Spec Kit extension manifest. -- Intake commands for visual design, PRD, and test-case sources. -- Intake contracts and evidence packet templates for visual design, PRD, and test-case sources. -- Local readiness validators for PRD, test-case, visual design, and Figma metadata intake artifacts. +- Added `templates/intake-static-html-delivery-contract.md` as the static HTML delivery contract. +- Added `templates/schemas/static-html-delivery.schema.json` for `delivery/render-replay-report.yaml`. +- Added `scripts/python/validate_static_html_delivery.py` for static HTML delivery readiness. +- Added validator checks for visual IR readiness, exact `visual_ir_refs` parity, IR fragment resolution, source-intake ref resolution, typed HTML anchor namespaces, clarification-log structure, asset resolution, operation replay, motion anchors, viewport screenshots, visual diffs, and delivery evidence packets. ### Changed -- Repositioned the extension as the generic `intake` extension with domain commands for visual design, PRD, and test-case sources. -- Expanded the extension goal from Figma-only metadata intake to visual design requirement intake. -- Added support contracts for image, PDF, Markdown, and Figma sources across low, medium, and high-fidelity drafts. -- Added readiness concepts for source integrity, visual requirement traceability, fidelity rules, and delivery parity planning. -- Updated the validator to support generic visual sources while preserving Figma metadata parity validation. +- Visual intake now requires source-backed visual IR artifacts for assets/resources, layout/boxes, component states, page/route/IA states, user-operation replay, motion/event anchors, and clarification questions. +- Static delivery readiness now blocks when user-confirmed operation behavior, route behavior, responsive rules, resource substitutions, or motion timing is missing. +- Source refs now reject generated delivery HTML, delivery screenshots, visual diff outputs, and evidence packets as source-of-truth records. diff --git a/extensions/intake/README.md b/extensions/intake/README.md index cd3e8503d7..0491f1a6ed 100644 --- a/extensions/intake/README.md +++ b/extensions/intake/README.md @@ -1,14 +1,12 @@ -# Spec Kit Intake Extension +# Spec Kit Intake Extension -Extract, normalize, and validate SDD-ready intake artifacts from PRDs, visual designs, structured UI/visual assets, visual-equivalent HTML delivery pages, test cases, and other software sources before downstream Spec Kit workflows project them into requirements. +Extract, normalize, and validate SDD-ready intake artifacts from PRDs, visual designs, final static HTML delivery bundles, test cases, and other software sources before downstream Spec Kit workflows project them into requirements. -For UI intake, the structured UI/visual asset is the source of truth. Evidence packets support confidence, and `previews/preview.html` is the generated static HTML mock equivalent for the visual input design, derived from intake artifacts to show the page, layout, component, state, interaction, and viewport surfaces; neither may create, override, or replace structured asset records. +For visual intake, `/speckit.intake.visual-design` is the only external command. It internally captures source evidence, builds visual IR, asks user clarification questions when operation or visual behavior is under-specified, generates `delivery/index.html`, and validates the final static HTML delivery. -The first goal of intake is not to generate requirements. It is to preserve as much input information as possible and turn it into structured material that SDD `specify` can consume accurately. +The visual intake goal is high-information source restoration: preserve original evidence, keep unsupported behavior explicit, and produce an offline-runnable static HTML artifact whose visual surfaces and user operations trace back to source evidence or user confirmation. -The intake goal is high-information source restoration: extracted facts must be usable as provider-neutral engineering input, and every downstream projection should remain traceable to the original artifact. - -Intake artifacts are validated in two layers: JSON Schema checks enforce the required structure, field types, and enums; readiness validators then check source integrity, traceability, hashes, gaps, and cross-file parity. +Intake artifacts are validated in two layers: JSON Schema checks enforce required structure, field types, and enums; readiness validators then check source integrity, traceability, hashes, clarification status, operation replay, assets, screenshots, visual diff records, and cross-file parity. ## Supported Intake Sources @@ -17,26 +15,22 @@ Intake artifacts are validated in two layers: JSON Schema checks enforce the req - Static images such as PNG, JPG, WebP, and exported screens - PDF design packs and annotated review documents - Figma files, pages, frames, nodes, components, variables, and exported screenshots -- Intake-derived HTML mock equivalent pages and coverage evidence with traceable page, component, visual-state, interaction, variant, viewport, and screenshot coverage -- Visual requirements/spec structured asset packages with CI-friendly DOM, ARIA, token, state, locator, relation, and assertion facts +- Final static HTML delivery bundles with visual IR, operation replay, motion anchors, viewport screenshots, and visual diff evidence - Existing test cases, Gherkin files, QA exports, and test management spreadsheets ## Intake Scenario Coverage -Intake commands are organized by vertical source domain. Each domain uses the same evidence pattern: preserve the original source, normalize source-backed facts, keep uncertainty explicit, and report readiness before downstream Spec Kit workflows project the evidence. - | Domain | Vertical scenarios | Normalized artifact | Primary readiness focus | | --- | --- | --- | --- | | PRD | product briefs, Markdown PRDs, exported docs, PDFs, issue or epic links, mixed stakeholder notes | `prd-intake.yaml` | source identity, product intent traceability, scope boundaries, acceptance evidence, clarification gaps | -| Visual design | static images, wireframes, PDF design packs, Markdown design briefs, Figma files or selected nodes | `visual-requirements.yaml` and `visual-spec-package/` | source integrity, Figma-backed resource traceability, fidelity rules, visual requirement traceability, structured visual spec readiness, CI-low-cost assertion readiness | -| HTML mock equivalent delivery | Validated UI intake artifacts projected into a static HTML/CSS mock equivalent of the visual input design | `previews/preview.html` plus coverage YAML | Visual equivalence, page coverage, component coverage, visual-state coverage, fused interaction coverage, variant coverage, viewport screenshots, known gaps | +| Visual design | static images, wireframes, PDF design packs, Markdown design briefs, Figma files or selected nodes | `visual-requirements.yaml`, `visual-ir/`, `delivery/index.html` | source integrity, full resource traceability, layout/box restoration, component/page/state coverage, user-operation replay, motion anchors, viewport screenshots, visual diff readiness | | Test cases | automated tests, Gherkin files, manual QA cases, spreadsheets, test management exports, bug or issue repro steps | `test-case-intake.yaml` | scenario traceability, assertion extraction, fixture evidence, coverage gaps, flaky or skipped case reporting | -Vertical instructions should never convert source evidence directly into downstream-owned requirement IDs, implementation tasks, or code component names. They produce provider-neutral intake facts that downstream workflows can consume with source refs intact. +Vertical instructions must not convert source evidence directly into downstream-owned requirement IDs, implementation tasks, or code component names. ## Commands -- `/speckit.intake.visual-design` captures or validates visual design evidence, Figma-backed resources, visual requirements, the visual spec package, and the intake-derived HTML mock equivalent for the active feature. +- `/speckit.intake.visual-design ` orchestrates visual source capture, visual IR, user clarification, static HTML delivery generation, and delivery validation through one external command. - `/speckit.intake.prd` captures or validates PRD evidence and normalizes product intent, scope, business rules, acceptance criteria, and clarification items. - `/speckit.intake.test-cases` captures or validates test case evidence and normalizes scenarios, assertions, fixtures, and coverage gaps. @@ -58,16 +52,20 @@ specs//intake/ │ ├── figma-node-inventory.yaml │ ├── figma-normalized-tree.yaml │ ├── visual-evidence-packet.md -│ ├── previews/ -│ │ ├── preview.html -│ │ ├── component-coverage.yaml -│ │ ├── viewport-coverage.yaml -│ │ ├── known-gaps.md -│ │ └── screenshots/ -│ └── visual-spec-package/ -│ ├── visual-spec.yaml -│ ├── visual-spec-assertions.yaml -│ └── visual-spec-evidence-packet.md +│ ├── visual-ir/ +│ │ ├── asset-inventory.yaml +│ │ ├── layout-tree.yaml +│ │ ├── component-model.yaml +│ │ ├── page-route-model.yaml +│ │ ├── interaction-model.yaml +│ │ ├── motion-anchor-model.yaml +│ │ └── clarification-log.yaml +│ └── delivery/ +│ ├── index.html +│ ├── assets/ +│ ├── screenshots/ +│ ├── render-replay-report.yaml +│ └── evidence-packet.md └── test-cases/ ├── source-manifest.yaml ├── source-files/ @@ -75,17 +73,9 @@ specs//intake/ └── evidence-packet.md ``` -Figma metadata artifacts and `figma-normalized-tree.yaml` are required for Figma visual-design sources. Image, PDF, and Markdown visual-design sources use `design-source-manifest.yaml`, source-file checksums, extracted visual requirements, and visual parity evidence instead. PRD and test-case domains use their own source manifests and normalized intake files. - -Machine-readable JSON Schemas live under `templates/schemas/` and are used by the validators before readiness rules run. Figma layout normalization uses `figma-normalized-tree.schema.json`. The HTML mock equivalent delivery contract is defined by `templates/intake-visual-previews-contract.md` and uses `component-coverage.schema.json` and `viewport-coverage.schema.json`. Visual spec packages use `visual-spec-package.schema.json` and `visual-spec-assertions.schema.json`. +Figma metadata artifacts and `figma-normalized-tree.yaml` are required for Figma visual-design sources. Image, PDF, and Markdown visual-design sources use `design-source-manifest.yaml`, source-file checksums, visual requirements, visual IR, and delivery parity evidence. -All intake commands provide capture instructions, evidence contracts, and readiness validation. Visual design validation additionally checks visual fidelity and Figma metadata parity. -Visual validators reject evidence packets, preview HTML, screenshots, and visual diffs when they appear in source-of-truth fields such as `source_refs` or `evidence_refs`; use preview-specific helper refs for those supporting artifacts. -Figma metadata capture should use bounded shards: stage raw `get_metadata` response files with `scripts/python/capture_figma_metadata_shards.py` so large provider responses are written to disk and only shard paths, hashes, completeness, and inventory parity flow into the intake index. -Keep raw capture files outside the target `visual-design/` directory before staging, and pass one `--node-id` per selected root when a capture spans multiple roots. -After Figma metadata parity passes, derive `figma-normalized-tree.yaml` with `scripts/python/normalize_figma_layout.py`. This derived artifact records rename, grouping, and visual re-sort results while preserving raw Figma metadata unchanged. -HTML mock delivery validation is owned by `scripts/python/validate_visual_previews.py`, including cross-file checks for mock page refs, visualized component refs, interaction refs, visual spec refs, screenshots, component coverage, viewport coverage, and known gaps. -Visual spec package validation is owned by `scripts/python/validate_visual_spec_package.py`, including source readiness, schema, cross-reference, locator, downstream-ownership, provider-evidence, product-ambiguity, design-source resource traceability, and CI assertion checks. +Machine-readable JSON Schemas live under `templates/schemas/`. Static HTML delivery validation uses `templates/schemas/static-html-delivery.schema.json` and `scripts/python/validate_static_html_delivery.py`. ## Requirements @@ -106,7 +96,7 @@ specify extension add --dev C:/Users/24598/Documents/github/spec-kit-intake From a Spec Kit project: ```bash -specify extension add intake --from https://github.com/bigsmartben/spec-kit-intake/archive/refs/tags/v0.1.5.zip +specify extension add intake --from https://github.com/bigsmartben/spec-kit-intake/archive/refs/tags/v0.2.0.zip ``` Release artifacts must include source-backed provenance for the `bigsmartben/spec-kit` integration fork. The release workflow uploads `release-provenance.json` with: @@ -120,15 +110,10 @@ Release artifacts must include source-backed provenance for the `bigsmartben/spe Then run: ```text -/speckit.intake.visual-design capture -/speckit.intake.visual-design validate -/speckit.intake.visual-design build-spec-package -/speckit.intake.visual-design validate-spec-package -/speckit.intake.visual-design build-html-mock -/speckit.intake.visual-design validate-html-mock +/speckit.intake.visual-design /speckit.intake.prd capture /speckit.intake.prd validate -/speckit.intake.test-cases capture +/speckit.intake.test-cases capture /speckit.intake.test-cases validate ``` @@ -141,54 +126,26 @@ Visual design intake passes only when: - extracted requirements preserve layout hierarchy, spacing, typography, color, assets, states, responsive behavior, and accessibility evidence at the fidelity level supplied - non-observed visual claims use bounded inference fields and stay auditable - candidate completions remain reference-only and unsupported claims emit blocker codes -- parity evidence explains how implementation output will be compared with the original design artifact -- Figma sources also pass raw metadata completeness and node-inventory parity -- Figma sources also pass layout normalization coverage with one normalized node per raw node +- source-side parity evidence explains how final delivery output is compared with the original design artifact +- Figma sources pass raw metadata completeness, node-inventory parity, and layout normalization coverage - no blocker lint errors exist -Visual design claims use these evidence types: - -- `observed`: source-backed fact from preserved files, rendered evidence, Figma metadata, screenshots, or prototype metadata. -- `inferred`: high-confidence derived claim that includes an inference rule, confidence method, score breakdown, and blocking conditions. -- `candidate`: low- or medium-confidence completion with `downstream_use: reference_only`. -- `unsupported`: rejected or blocked claim with a stable blocker code. -- `missing` or `out_of_scope`: explicit absence or excluded surface. - -For irregular Figma sources, intake may generate bounded candidate completions, but it must not infer business rules, permissions, form validation, error copy, loading or disabled states, data sources, analytics, security, or compliance behavior from visual appearance alone. - -## HTML Mock Delivery Readiness Gate +## Static HTML Delivery Readiness Gate -The intake-derived HTML mock equivalent delivery passes only when: +Static HTML delivery passes only when: - upstream visual-design intake is ready -- adjacent `visual-spec-package/` readiness is PASS -- `preview.html` renders static HTML/CSS mock page surfaces for intake-backed pages, layout regions, components, component states, interaction states, content samples, and viewport surfaces -- `preview.html` includes IA matrix sections, anchors, and coverage summaries as the validation layer for the mock, not as a replacement for visualized page/component surfaces -- `preview.html` implements only facts from `visual-requirements.yaml`, `visual-spec-package/`, coverage YAML, source-backed refs, screenshot refs, and explicit missing or blocked records -- every required page, component set, component instance, variant prop, state, size, density, theme, content sample, interaction, resource, and viewport is covered or recorded as a blocking missing record -- the minimum acceptance grain is covered: page state, page IA row, component instance, component state, component IA row, interaction event, content sample, container constraint, and viewport -- required preview cells and interaction rows link back to Figma refs, `visual-spec-package/visual-spec.yaml` items, `component-coverage.yaml` records, and screenshot or diff evidence when available -- component coverage is expressed in `previews/component-coverage.yaml` -- viewport coverage is expressed in `previews/viewport-coverage.yaml` -- screenshot coverage and visual-diff status are recorded -- known gaps are explicit and no blocking gap remains unresolved - -The preview validator emits blocker codes such as `VISUAL_PREVIEW_SOURCE_INTAKE_BLOCKED`, `VISUAL_PREVIEW_REQUIRED_ARTIFACT_MISSING`, `VISUAL_PREVIEW_SCHEMA_INVALID`, `VISUAL_PREVIEW_FIGMA_NODE_COVERAGE_INCOMPLETE`, `VISUAL_PREVIEW_COMPONENT_STATE_COVERAGE_INCOMPLETE`, `VISUAL_PREVIEW_IA_MATRIX_INCOMPLETE`, `VISUAL_PREVIEW_PAGE_COVERAGE_INCOMPLETE`, `VISUAL_PREVIEW_ASSET_TRACEABILITY_INCOMPLETE`, `VISUAL_PREVIEW_VIEWPORT_CAPTURE_INCOMPLETE`, `VISUAL_PREVIEW_VISUAL_DIFF_BLOCKED`, and `VISUAL_PREVIEW_KNOWN_GAP_UNRESOLVED`. - -## Visual Spec Package Readiness Gate - -The visual requirements/spec structured asset package passes only when: - -- upstream visual-design intake is ready -- `visual-spec.yaml`, `visual-spec-assertions.yaml`, and `visual-spec-evidence-packet.md` exist -- visual spec items preserve source refs, page, region, role, state, viewport, provider-neutral locator strategy, expectations, acceptance intent, confidence, status, and blockers -- implementation resources, images, and token refs are traceable to the design source; for Figma sources, they must trace back to Figma refs -- assertions reference existing visual spec items and include ready `ci_low_cost` checks -- missing provider evidence and product ambiguity are represented with distinct blocker paths -- locator strategies avoid implementation-owned CSS selectors, XPath, generated class names, downstream test IDs, code component names, tasks, or requirement IDs -- HTML delivery refs, screenshots, and visual diffs remain visual-equivalence evidence derived from intake artifacts rather than source-of-truth records - -The visual spec package validator emits blocker codes such as `VISUAL_SPEC_SOURCE_INTAKE_BLOCKED`, `VISUAL_SPEC_REQUIRED_ARTIFACT_MISSING`, `VISUAL_SPEC_SCHEMA_INVALID`, `VISUAL_SPEC_INTAKE_INCOMPLETE`, `VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING`, `VISUAL_SPEC_PRODUCT_AMBIGUITY_UNRESOLVED`, `VISUAL_SPEC_ASSERTION_COVERAGE_INCOMPLETE`, `VISUAL_SPEC_LOCATOR_STRATEGY_INVALID`, `VISUAL_SPEC_DOWNSTREAM_OWNERSHIP_LEAK`, and `VISUAL_SPEC_READY_WITHOUT_EVIDENCE`. +- all required visual IR artifacts exist and report `ready_gate: PASS` +- required user clarification questions are answered or explicitly out of scope +- `delivery/index.html` is static and offline-runnable +- all assets resolve under `delivery/assets/` +- every page, route state, component state, user operation, motion anchor, viewport, and visual diff record resolves through `render-replay-report.yaml` +- operation replay records have `replay_status: pass` +- required screenshots exist under `delivery/screenshots/` +- visual diff records are `pass` +- `delivery/evidence-packet.md` reports `ready_gate: PASS` with no blockers + +The static HTML delivery validator emits blocker codes such as `STATIC_HTML_SOURCE_INTAKE_BLOCKED`, `STATIC_HTML_REQUIRED_ARTIFACT_MISSING`, `STATIC_HTML_SCHEMA_INVALID`, `STATIC_HTML_IR_BLOCKED`, `STATIC_HTML_CLARIFICATION_REQUIRED`, `STATIC_HTML_ASSET_INCOMPLETE`, `STATIC_HTML_OPERATION_REPLAY_INCOMPLETE`, `STATIC_HTML_MOTION_ANCHOR_INCOMPLETE`, `STATIC_HTML_VIEWPORT_CAPTURE_INCOMPLETE`, and `STATIC_HTML_VISUAL_DIFF_BLOCKED`. ## Development @@ -198,40 +155,16 @@ Validate the manifest from the local `spec-kit` checkout: python -c "from pathlib import Path; from specify_cli.extensions import ExtensionManifest; ExtensionManifest(Path('extension.yml')); print('manifest ok')" ``` -Validate visual-design artifacts: +Validate visual-design source artifacts: ```bash python scripts/python/validate_visual_design_intake.py specs//intake/visual-design ``` -Stage sharded Figma metadata captures before validation: - -```bash -python scripts/python/capture_figma_metadata_shards.py specs//intake/visual-design \ - --metadata-source ./figma-metadata-raw \ - --file-url \ - --file-key \ - --page-id \ - --node-id \ - --overwrite -``` - -Derive Figma layout normalization after metadata parity passes: - -```bash -python scripts/python/normalize_figma_layout.py specs//intake/visual-design -``` - -Validate HTML mock delivery artifacts: - -```bash -python scripts/python/validate_visual_previews.py specs//intake/visual-design/previews -``` - -Validate visual spec package artifacts: +Validate static HTML delivery artifacts: ```bash -python scripts/python/validate_visual_spec_package.py specs//intake/visual-design/visual-spec-package +python scripts/python/validate_static_html_delivery.py specs//intake/visual-design/delivery ``` Validate PRD artifacts: diff --git a/extensions/intake/commands/speckit.intake.visual-design.md b/extensions/intake/commands/speckit.intake.visual-design.md index 9a5c252aea..c098e17788 100644 --- a/extensions/intake/commands/speckit.intake.visual-design.md +++ b/extensions/intake/commands/speckit.intake.visual-design.md @@ -1,5 +1,5 @@ ---- -description: Capture or validate visual design intake for the active Spec Kit feature. +--- +description: Orchestrate visual design intake into a source-backed static HTML delivery for the active Spec Kit feature. --- ## User Input @@ -8,225 +8,187 @@ description: Capture or validate visual design intake for the active Spec Kit fe $ARGUMENTS ``` -Classify the input before proceeding: - -- `source`: image, PDF, Markdown design brief, Figma URL, file, page, frame, node, or exported design asset -- `intake_dir`: existing visual-design intake artifact directory -- `validation_request`: validate, check, gate, readiness, validate-spec-package, or validate-html-mock -- `asset_request`: build-spec-package, build-html-mock, downstream delivery asset request, HTML mock coverage asset request, or CI-low-cost assertion asset request -- `review_guidance`: target platform, required fidelity, capture scope, source precedence, Figma-backed resource requirements, or reviewer instructions +Treat this command as the only external visual-intake entrypoint. Do not expose or require user-facing subcommands. `$ARGUMENTS` may contain a design source, an existing visual-intake directory, reviewer guidance, fidelity constraints, platform constraints, or answers to previously emitted clarification questions. ## Goal -Create, update, validate, and derive delivery artifacts for high-confidence provider-neutral visual design intake. This command is the single orchestration entrypoint for visual-design intake: it captures source-backed visual facts, validates the structured intake asset, builds the visual spec package, and can generate `previews/preview.html` as the static HTML mock equivalent of the visual input design. +Produce a final static HTML delivery that can be opened offline and that restores the submitted design source as faithfully as the available evidence allows: -The structured UI/visual asset remains the source of truth: it records pages, regions, components, states, styles, resources, and interaction cues from UI or visual design sources so downstream product specification, implementation, and acceptance workflows can consume source-backed visual facts. `previews/preview.html` is a derived HTML/CSS mock delivery artifact built from validated intake facts; it supports visual equivalence, page/component/state inspection, and coverage validation, but it must not create, override, or replace structured asset records. +- visual parity with the design source +- complete asset/resource traceability +- layout and box-model reconstruction +- component inventory with exhaustive source-backed states +- page, route, and information-architecture state coverage +- user-operation replay coverage +- motion/event anchors with source-backed timing or explicit blockers -Default artifact directory: +The final delivery artifact is: + +```text +specs//intake/visual-design/delivery/index.html +``` + +The command must continue internally until the delivery is `PASS` or until a stable blocker requires user confirmation, missing source access, or unavailable tooling. + +## Normative Authority + +- JSON Schemas under `templates/schemas/` define machine-readable structure. +- `templates/intake-visual-design-contract.md` defines source intake, visual IR, clarification, static HTML delivery, and readiness policy. +- `templates/intake-static-html-delivery-contract.md` defines the static HTML delivery contract. +- `scripts/python/validate_visual_design_intake.py` validates core source integrity and source-backed visual requirements. +- `scripts/python/validate_static_html_delivery.py` validates the static HTML delivery bundle. +- Figma helpers (`capture_figma_metadata_shards.py` and `normalize_figma_layout.py`) preserve raw provider evidence and derive deterministic layout order before visual IR extraction. + +This command routes and orchestrates. Contracts, schemas, and validators own artifact structure and readiness decisions. + +## Artifact Family + +Default directory: ```text specs//intake/visual-design/ ``` -Normative authority: +Required source artifacts: + +- `design-source-manifest.yaml` +- `source-files/` +- `visual-requirements.yaml` +- `visual-evidence-packet.md` +- Figma sources additionally require raw metadata, node inventory, and `figma-normalized-tree.yaml` + +Required visual IR artifacts: + +- `visual-ir/asset-inventory.yaml` +- `visual-ir/layout-tree.yaml` +- `visual-ir/component-model.yaml` +- `visual-ir/page-route-model.yaml` +- `visual-ir/interaction-model.yaml` +- `visual-ir/motion-anchor-model.yaml` +- `visual-ir/clarification-log.yaml` + +Required static HTML delivery artifacts: -- `templates/schemas/*.json` defines machine-readable structure, required fields, types, and enums. -- `scripts/python/validate_visual_design_intake.py` defines readiness evaluation and blocker emission for the core structured UI/visual asset. -- `scripts/python/capture_figma_metadata_shards.py` stages already-sharded Figma metadata captures into raw shard, index, and inventory artifacts. -- `scripts/python/normalize_figma_layout.py` derives `figma-normalized-tree.yaml` from verified raw Figma metadata using rename, grouper, and re-sort rules without mutating raw provider evidence. -- `templates/schemas/figma-normalized-tree.schema.json` defines the machine-readable Figma layout normalization artifact. -- `scripts/python/validate_visual_spec_package.py` defines downstream structured visual spec package readiness after the core asset passes. -- `scripts/python/validate_visual_previews.py` defines readiness for the generated HTML mock equivalent page and its coverage artifacts. -- `templates/intake-visual-design-contract.md` defines semantic extraction policy, fidelity policy, and provider evidence policy. -- `templates/intake-visual-spec-package-contract.md` defines the visual requirements/spec structured asset package. -- `templates/intake-visual-previews-contract.md` defines HTML mock delivery artifact structure, coverage boundaries, and blocker semantics. -- This command only performs input routing, context loading, capture orchestration, validation invocation, and reporting. +- `delivery/index.html` +- `delivery/assets/` +- `delivery/screenshots/` +- `delivery/render-replay-report.yaml` +- `delivery/evidence-packet.md` ## Operating Boundaries -- Preserve original design sources and record checksums before extraction. -- For Figma sources, implementation resources, images, exported assets, and token refs must trace back to Figma source refs. -- Extract structured UI/visual asset records as traceable engineering input, not as unsupported prose summaries or downstream-specific schema projections. -- Treat `visual-spec-package/` as the downstream structured UI/visual asset package for delivery and CI-low-cost checks. -- Treat `previews/preview.html` as the generated static HTML mock equivalent for the visual input design in `build-html-mock` mode. It must render pages, layout, components, component states, content samples, interaction states, and viewport-specific surfaces from validated `visual-requirements.yaml`, `visual-spec-package/`, coverage YAML, screenshots, and source-backed refs. -- Treat IA matrix sections, stable anchors, coverage YAML, screenshots, visual diffs, and `known-gaps.md` as the validation layer for the HTML mock, not as the primary identity of `preview.html`. -- Treat `previews/component-coverage.yaml` and `previews/viewport-coverage.yaml` as structured coverage evidence for HTML mock completeness checks; they may support readiness but do not replace `visual-spec-package/`. -- Do not place `visual-evidence-packet.md`, `visual-spec-evidence-packet.md`, `preview.html`, screenshots, visual diffs, or other preview artifacts in `source_refs` or `evidence_refs` as source-of-truth records. Use preview-specific helper fields such as `preview_refs` instead. -- Use bounded inference for dirty or incomplete design sources: observed claims are source-backed facts; inferred claims require explicit rules and high confidence; candidate claims are reference-only; unsupported claims must remain blocked. -- Mark low, medium, or high fidelity explicitly and apply the matching extraction rules. -- Use stable provider-neutral evidence IDs and source refs. Do not invent downstream-owned item IDs, requirement IDs, schema fields, code component names, or product semantics. -- Do not mark intake ready unless source integrity, source refs, fidelity rules, bounded inference checks, and intake parity plan pass the validator readiness gates. -- Preserve raw Figma metadata exactly in `figma-metadata.part-*.xml` for Figma sources. -- Treat `figma-normalized-tree.yaml` as the only Figma layout normalization output. It may record derived `normalized_name`, `group_key`, `parent_group_key`, `sort_key`, and `visual_order` values, but it must not rewrite raw Figma metadata or define downstream requirement IDs, implementation tasks, selectors, or code component names. -- Do not place rename maps, grouping strategies, sort indexes, layout fixups, or normalization notes in `visual-requirements.yaml`; use `figma-normalized-tree.yaml` for those derived layout records. -- Do not request complete metadata for a broad Figma page, canvas, or board in one MCP response. Split large Figma scopes into smaller frame/component/node captures and stage them with `capture_figma_metadata_shards.py`. -- Do not modify application source, tests, package manifests, feature implementation files, or existing Spec Kit core templates. -- If required tooling is unavailable, create a blocked evidence packet that records the missing tool and stop before claiming readiness. - -## Context Loading - -1. Verify the current directory is a Spec Kit project by checking for `.specify/`, unless `$ARGUMENTS` points to a standalone artifact directory for extension development. -2. Identify the active feature: - - Prefer `SPECIFY_FEATURE` when set. - - Otherwise use the current Git branch name when it matches a directory under `specs/`. - - Otherwise inspect `specs/` and choose the most recently modified feature directory only when exactly one feature directory exists. - - If the feature cannot be identified and no standalone artifact directory was provided, stop and ask the user to set `SPECIFY_FEATURE` or run from the feature branch. -3. Read `.specify/extensions/intake/intake-config.yml` when present. -4. Read `templates/intake-visual-design-contract.md` and the referenced JSON Schemas from this extension before creating or validating artifacts. -5. Read any existing intake artifacts and preserve valid evidence unless the user explicitly asks to recapture it. - -## Mode Routing - -Apply routing precedence before executing a mode: - -- If both a `source` and `build-spec-package` intent are present, run capture then validate first. Continue to Build spec package mode only when the updated visual-design intake validator returns `PASS`; otherwise stop and report the visual-design blockers. -- If both a `source` and `build-html-mock` intent are present, run capture then validate first. Continue to Build HTML mock mode only when the updated visual-design intake validator returns `PASS`; otherwise stop and report the visual-design blockers. -- If build-spec-package, validate-spec-package, build-html-mock, or validate-html-mock intent is present without a resolvable source or existing intake directory, stop and ask for the visual-design intake directory. - -- Capture mode: use when `$ARGUMENTS` names an image, PDF, Markdown design brief, Figma URL, frame, node, platform, fidelity level, or asks to capture, ingest, update, or recapture visual evidence. -- Build spec package mode: use when `$ARGUMENTS` includes `build-spec-package`, `with spec package`, `structured visual spec`, `CI-low-cost assertions`, or asks for downstream delivery/acceptance assets. -- Validate spec package mode: use when `$ARGUMENTS` includes `validate-spec-package`, `check spec package`, `visual spec readiness`, or only names an existing `visual-spec-package` directory. -- Build HTML mock mode: use when `$ARGUMENTS` includes `build-html-mock`, `HTML mock`, `mock coverage`, `component-coverage`, or `viewport-coverage`. -- Validate HTML mock mode: use when `$ARGUMENTS` includes `validate-html-mock`, `check HTML mock`, `HTML mock readiness`, or only names an existing `previews` directory. -- Validate mode: use when `$ARGUMENTS` includes `validate`, `check`, `gate`, `readiness`, or only names an existing visual-design intake directory. -- Capture then validate: use when both a source and validation intent are present, or after capture artifacts are updated. - -## Capture Procedure - -1. Resolve the source from `$ARGUMENTS` or existing artifact metadata: - - source type: `image`, `pdf`, `markdown`, or `figma` - - source path, URL, file key, page, frame, node, region, or Markdown section scope - - required fidelity: `low`, `medium`, or `high` - - design version or timestamp -2. Create `design-source-manifest.yaml` with contract-required source identity, integrity, coverage, capture method, and fidelity fields. -3. Preserve file-based originals under `source-files/`; for remote or Figma sources, record stable URLs and exported screenshots or assets, or record a structured gap/blocker when unavailable. -4. For Figma sources, preserve raw provider evidence before deriving normalized requirements: - - capture metadata in bounded node batches instead of one broad page/canvas response - - store raw responses outside the target intake directory before staging; do not point `--metadata-source` at the target `visual-design/` directory - - for multiple selected roots, pass `--node-id` once per root and avoid overlapping node scopes because duplicate or missing node parity blocks readiness - - stage raw metadata files with: +- Preserve original sources and checksums before extraction. +- Build visual IR from source-backed evidence only. Do not invent product behavior, hidden states, routes, animation timing, data validation, permissions, analytics, or business rules from visual appearance. +- Use user confirmation to resolve missing operational behavior, route transitions, interaction outcomes, responsive rules, resource substitution, or animation details. +- Record each question in `visual-ir/clarification-log.yaml` with a stable `CQ-*` id, target artifact, blocker code, status, and whether the answer is required for static HTML readiness. +- When `$ARGUMENTS` answers prior questions, update the clarification log and the affected IR artifact before rebuilding delivery. +- Candidate completions may be rendered only when explicitly labeled as blocked/reference-only in the delivery and evidence. They cannot satisfy readiness. +- The final HTML must be static: no backend, no build step, no network-only asset dependency, and no runtime dependency that prevents offline inspection. +- Vanilla JavaScript may be embedded when needed to replay user operations, route state, component state, overlays, forms, or motion anchors. +- Do not modify application source, package manifests, feature implementation files, or Spec Kit core templates. +- Do not create downstream-owned requirement IDs, code component names, production selectors, implementation tasks, or product semantics. -```bash -python .specify/extensions/intake/scripts/python/capture_figma_metadata_shards.py \ - --metadata-source \ - --file-url \ - --file-key \ - --page-id \ - --node-id \ - --overwrite -``` +## Internal Workflow - - write raw metadata shards as `figma-metadata.part-NNN.xml` - - build `figma-metadata.index.yaml` - - build `figma-node-inventory.yaml` - - validate metadata and inventory parity before deriving visual requirements - - derive a provider-neutral normalized Figma layout tree after metadata parity passes: +Run these phases internally. Do not ask the user to invoke a phase name. -```bash -python .specify/extensions/intake/scripts/python/normalize_figma_layout.py -``` +### 1. Resolve Context - - write `figma-normalized-tree.yaml` as the derived rename/grouper/re-sort artifact - - keep `source_node_id`, `original_name`, and `source_refs` traceable to raw metadata for every normalized node - - if any shard is truncated or lacks detectable node ids, keep the intake `BLOCKED` and retry with a smaller node scope or a direct file-based provider export -5. Extract source-specific evidence: - - image: dimensions, regions, OCR status, visual hierarchy, assets, and region coverage - - pdf: original file hash, page count, rendered page refs, text extraction status, and page coverage - - markdown: heading structure, design notes, embedded or linked assets, and visual requirement mappings - - figma: complete descendant metadata, node inventory, variables/styles/components, screenshots, and assets -6. Record source coverage and extraction gaps using `design-source-manifest.yaml`, `visual-requirements.yaml`, and `templates/intake-visual-design-contract.md`; do not define scenario categories in this command. -7. Build `visual-requirements.yaml` according to `templates/schemas/visual-requirements.schema.json` and the semantic policies in `templates/intake-visual-design-contract.md`. - - Record direct facts as `evidence_type: observed`. - - Promote only rule-backed, high-confidence derived claims to `evidence_type: inferred` with `inference_rule`, `confidence_method`, `score_breakdown`, `downstream_use: accepted_claim`, and `blocking_conditions`. - - Keep low- or medium-confidence completions as `evidence_type: candidate` with `downstream_use: reference_only` and `missing_evidence`. - - Record unsupported or conflicting claims as `evidence_type: unsupported` with `blocker_code`, `reason`, `missing_evidence`, and `blockers`. -8. For unavailable required evidence, record a structured gap or blocker instead of omitting the field. Do not infer business rules, permissions, form validation, error copy, dynamic states, data sources, analytics, security, or compliance behavior from visual appearance alone. -9. Create or update `visual-evidence-packet.md` from `templates/intake-visual-design-evidence-packet-template.md` with readiness front matter and human-readable evidence notes; keep structured records in `visual-requirements.yaml`. The evidence packet must summarize validator-backed confidence only and must not create, override, or replace structured asset records. Preserve an existing `figma-evidence-packet.md` only as a legacy compatibility alias when already configured by the host project. -10. Add an intake parity plan that records source-side comparison targets, methods, thresholds, accepted exceptions, and blocking difference categories without defining implementation capture artifacts or downstream delivery approval. -11. Run validation before reporting readiness. - -## Visual Spec Package Procedure - -1. Resolve the upstream visual-design intake directory and target `visual-spec-package/` directory. -2. Ensure visual-design intake passes readiness before building or validating the package: +1. Verify a Spec Kit workspace by checking for `.specify/`, unless `$ARGUMENTS` points to a standalone artifact directory. +2. Resolve the active feature from `SPECIFY_FEATURE`, the current branch, or the only feature directory under `specs/`. +3. Load `.specify/extensions/intake/intake-config.yml` when present. +4. Load this extension's visual contracts and schemas before writing artifacts. +5. Read existing visual artifacts and preserve valid source-backed evidence. -```bash -python .specify/extensions/intake/scripts/python/validate_visual_design_intake.py -``` +### 2. Capture Source Evidence -3. Create or update the `visual-spec-package/` artifact family according to `templates/intake-visual-spec-package-contract.md`, `templates/schemas/visual-spec-package.schema.json`, and `templates/schemas/visual-spec-assertions.schema.json`. -4. Keep provider/source traceability, downstream-ownership exclusions, assertion coverage, and optional HTML mock helper refs aligned with the visual spec package contract. Do not restate or invent package fields in this command. -5. Do not use `preview.html` or rendered preview output as the source of truth for assets, tokens, product behavior, or requirements. Use source refs and structured visual-design intake records as authority. -6. Validate before reporting readiness: +1. Resolve source type: `image`, `pdf`, `markdown`, or `figma`. +2. Create or update `design-source-manifest.yaml` and preserve source files under `source-files/`. +3. For Figma, capture bounded raw metadata shards and derive `figma-normalized-tree.yaml` only after metadata parity passes. +4. Update `visual-requirements.yaml` with source-backed visual facts, bounded inference status, and source-side parity plan. +5. Run `validate_visual_design_intake.py`. -```bash -python .specify/extensions/intake/scripts/python/validate_visual_spec_package.py -``` +If source intake is blocked, stop and report validator blocker codes plus the next concrete recovery action. -## HTML Mock Delivery Procedure +### 3. Build Visual IR -1. Resolve the upstream visual-design intake directory and target `previews/` directory. -2. Ensure visual-design intake passes readiness before building or validating HTML mock coverage: +Create or update the required files under `visual-ir/`: -```bash -python .specify/extensions/intake/scripts/python/validate_visual_design_intake.py -``` +- asset inventory: every font, raster, vector, icon, color resource, media resource, external dependency, local copy path, hash, and substitution gap +- layout tree: page/frame hierarchy, layers, groups, boxes, coordinates, constraints, z-order, overflow, scroll regions, and responsive rules +- component model: component identity, instances, variants, props, visual states, state evidence, accessibility expectations, and missing states +- page-route model: pages, route states, IA regions, navigation targets, overlays, modals, drawers, empty/error/loading states, and viewport variants +- interaction model: user events, targets, preconditions, state changes, route changes, feedback, exception branches, and replay steps +- motion anchor model: trigger, event anchor, affected surface, initial state, end state, duration, easing, delay, repeat behavior, and source refs +- clarification log: unanswered questions, answered confirmations, assumptions, and blockers -3. Ensure `visual-spec-package/` exists and passes readiness before using it as an HTML mock input; build or validate it with the Visual Spec Package Procedure when it is missing or stale. -4. Generate or update `previews/preview.html` as a static HTML/CSS mock equivalent of the visual input design. Render the intake-backed pages, layout regions, components, visual states, content samples, interaction states, and viewport surfaces before adding verification tables or coverage summaries. -5. Add stable anchors on mock page, component, state, interaction, and viewport surfaces so coverage records can point to visualized nodes, not only to IA matrix rows. Use IA matrix sections as the coverage and interaction evidence layer for the mock. -6. Create or update `component-coverage.yaml`, `viewport-coverage.yaml`, `known-gaps.md`, screenshots, and visual-diff outputs according to `templates/intake-visual-previews-contract.md`, `templates/schemas/component-coverage.schema.json`, and `templates/schemas/viewport-coverage.schema.json`. -7. Record missing, blocked, or out-of-scope pages, states, resources, and viewports in the mock and coverage artifacts. Do not silently complete missing visual states or product behavior for visual polish. -8. Do not use preview HTML as a requirements source, production implementation source, product semantic source, token source, or replacement for `visual-spec.yaml`. -9. Validate before reporting readiness: +### 4. Clarification Gate -```bash -python .specify/extensions/intake/scripts/python/validate_visual_previews.py -``` +Before building `delivery/index.html`, scan visual IR for missing information that affects visual parity or operation replay. -## Validation Procedure +Emit `ready_gate: BLOCKED` with `next_action: USER_CONFIRMATION_REQUIRED` when any required confirmation is unanswered. Ask concise, concrete questions. Each question must name: -1. Resolve the visual-design intake directory from `$ARGUMENTS` or the active feature. -2. Run: +- `question_id` +- `target_artifact` +- `blocked_delivery_surface` +- `blocker_code` +- `allowed_answer_shape` -```bash -python .specify/extensions/intake/scripts/python/validate_visual_design_intake.py -``` +Do not continue to final delivery readiness while required questions are unanswered. + +### 5. Build Static HTML Delivery + +Generate or update `delivery/index.html` from validated source intake and visual IR. -3. Prefer `--json` when a machine-readable result is needed. Report the validator result exactly: - - `PASS` means the structured UI/visual asset passed JSON Schema structure checks and validator readiness checks, with evidence chain used only to support confidence. - - `BLOCKED` means downstream workflows must keep design-derived requirements blocked, unresolved, or marked `[NEEDS CLARIFICATION]` instead of promoting unsupported design facts. +The HTML must: -## Readiness Authority +- render all required pages, regions, layout boxes, component instances, component states, page states, routes, overlays, and viewport surfaces +- include local asset references under `delivery/assets/` unless an explicitly blocked resource gap exists +- expose stable anchors using `id`, `data-visual-id`, `data-component-id`, `data-state-id`, `data-route-id`, `data-operation-id`, and `data-motion-id` +- implement user-operation replay with static HTML/CSS/vanilla JS when operation behavior is source-backed or user-confirmed +- expose blocked or unresolved surfaces visibly without substituting guessed behavior +- avoid evidence tables replacing the rendered UI; evidence may follow the rendered surfaces but cannot be the only representation -Use this precedence when sources disagree: +### 6. Validate Delivery -1. JSON Schemas are canonical for structural validity in all modes. -2. `validate_visual_design_intake.py` is canonical for core structured UI/visual asset readiness status and blocker codes. -3. `figma-normalized-tree.yaml` is canonical only for Figma layout normalization records consumed before deriving visual requirements. -4. `validate_visual_spec_package.py` is canonical only for downstream visual spec package readiness status and blocker codes. -5. `validate_visual_previews.py` is canonical only for HTML mock delivery readiness status and blocker codes. -6. `templates/intake-visual-design-contract.md` is canonical for semantic extraction, fidelity, provider evidence policy, and Figma layout normalization policy. -7. `templates/intake-visual-spec-package-contract.md` and `templates/intake-visual-previews-contract.md` are canonical for their artifact families. +Run: -Do not restate, reinterpret, or override blocker codes in this command. +```bash +python .specify/extensions/intake/scripts/python/validate_static_html_delivery.py /delivery +``` -## Report +The validator is canonical for static HTML delivery readiness. Prefer `--json` when machine-readable output is needed. + +Delivery is ready only when: + +- source intake is `PASS` +- required visual IR artifacts exist and have no readiness blockers +- all required clarification questions are answered or marked out of scope with source-backed rationale +- `delivery/index.html` exists and contains required static delivery anchors +- all local assets resolve under `delivery/assets/` or are recorded as blockers +- component, page, route, interaction, motion, and viewport coverage records resolve to rendered HTML anchors +- operation replay steps resolve to event targets and expected result surfaces +- screenshots exist for required viewports +- visual diff status is `pass`; unavailable source comparison must be recorded as `STATIC_HTML_VISUAL_DIFF_BLOCKED` and keeps readiness blocked +- `delivery/evidence-packet.md` reports `ready_gate: PASS` with no blockers + +### 7. Report Return: -- mode executed or sequence executed: capture, validate, capture_then_validate, build_spec_package, validate_spec_package, build_html_mock, validate_html_mock, or an ordered combination when source capture precedes build -- output or validated directory -- source type and source refs captured, or the recorded gap/blocker -- required fidelity, or the recorded gap/blocker -- source file count and processed count, or the recorded gap/blocker -- visual requirement count -- visual spec package item count when built or validated -- visual spec package assertion count and CI-low-cost assertion count when built or validated -- HTML mock page count, component/state coverage count, viewport mock coverage count, and visual parity evidence when built or validated -- Figma-backed resource traceability result when source is Figma -- Figma layout normalization result when source is Figma: normalized node count, coverage, duplicate source node ids, duplicate visual orders, and blocker code when incomplete +- resolved directory +- source type and source refs captured +- fidelity level +- internal workflow phases completed +- visual IR readiness summary +- clarification status and unanswered question count +- static HTML path +- asset count and missing asset count +- page/route/state/component/operation/motion counts +- viewport screenshot count +- visual diff result - readiness result -- blocker lint errors +- blocker codes - next corrective action when blocked -- open questions that must remain `[NEEDS CLARIFICATION]` + +Use exact blocker codes from validators and schemas. Keep unresolved gaps as `[NEEDS CLARIFICATION]` rather than converting them into requirements. diff --git a/extensions/intake/config-template.yml b/extensions/intake/config-template.yml index c63b9fd3c9..af1e4c2c9d 100644 --- a/extensions/intake/config-template.yml +++ b/extensions/intake/config-template.yml @@ -1,10 +1,10 @@ -# Intake Configuration +# Intake Configuration artifacts: base_dir: "specs/{feature}/intake" visual_design_dir: "visual-design" - previews_dir: "previews" - visual_spec_package_dir: "visual-spec-package" + visual_ir_dir: "visual-ir" + static_html_delivery_dir: "delivery" prd_dir: "prd" test_cases_dir: "test-cases" prd_source_manifest: "source-manifest.yaml" @@ -14,29 +14,30 @@ artifacts: prd_intake: "prd-intake.yaml" visual_requirements: "visual-requirements.yaml" test_case_intake: "test-case-intake.yaml" - preview_html: "preview.html" - component_coverage: "component-coverage.yaml" - viewport_coverage: "viewport-coverage.yaml" - visual_previews_known_gaps: "known-gaps.md" - visual_previews_screenshots_dir: "screenshots" + static_html_entry: "index.html" + static_html_assets_dir: "assets" + static_html_screenshots_dir: "screenshots" + static_html_render_replay_report: "render-replay-report.yaml" + static_html_evidence_packet: "evidence-packet.md" + visual_ir_asset_inventory: "asset-inventory.yaml" + visual_ir_layout_tree: "layout-tree.yaml" + visual_ir_component_model: "component-model.yaml" + visual_ir_page_route_model: "page-route-model.yaml" + visual_ir_interaction_model: "interaction-model.yaml" + visual_ir_motion_anchor_model: "motion-anchor-model.yaml" + visual_ir_clarification_log: "clarification-log.yaml" intake_validator_common: "scripts/python/intake_validator_common.py" prd_validator: "scripts/python/validate_prd_intake.py" test_cases_validator: "scripts/python/validate_test_cases_intake.py" visual_design_validator: "scripts/python/validate_visual_design_intake.py" figma_metadata_capture_script: "scripts/python/capture_figma_metadata_shards.py" figma_layout_normalization_script: "scripts/python/normalize_figma_layout.py" - visual_previews_validator: "scripts/python/validate_visual_previews.py" - visual_spec_package: "visual-spec.yaml" - visual_spec_assertions: "visual-spec-assertions.yaml" - visual_spec_package_evidence_packet: "visual-spec-evidence-packet.md" - visual_spec_package_validator: "scripts/python/validate_visual_spec_package.py" + static_html_delivery_validator: "scripts/python/validate_static_html_delivery.py" prd_contract: "templates/intake-prd-contract.md" prd_evidence_packet_template: "templates/intake-prd-evidence-packet-template.md" visual_design_contract: "templates/intake-visual-design-contract.md" visual_design_evidence_packet_template: "templates/intake-visual-design-evidence-packet-template.md" - visual_previews_contract: "templates/intake-visual-previews-contract.md" - visual_spec_package_contract: "templates/intake-visual-spec-package-contract.md" - visual_spec_package_evidence_packet_template: "templates/intake-visual-spec-package-evidence-packet-template.md" + static_html_delivery_contract: "templates/intake-static-html-delivery-contract.md" test_cases_contract: "templates/intake-test-cases-contract.md" test_cases_evidence_packet_template: "templates/intake-test-cases-evidence-packet-template.md" schemas_dir: "templates/schemas" @@ -49,10 +50,7 @@ artifacts: figma_metadata_index_schema: "templates/schemas/figma-metadata-index.schema.json" figma_node_inventory_schema: "templates/schemas/figma-node-inventory.schema.json" figma_normalized_tree_schema: "templates/schemas/figma-normalized-tree.schema.json" - component_coverage_schema: "templates/schemas/component-coverage.schema.json" - viewport_coverage_schema: "templates/schemas/viewport-coverage.schema.json" - visual_spec_package_schema: "templates/schemas/visual-spec-package.schema.json" - visual_spec_assertions_schema: "templates/schemas/visual-spec-assertions.schema.json" + static_html_delivery_schema: "templates/schemas/static-html-delivery.schema.json" metadata_glob: "figma-metadata.part-*.xml" metadata_index: "figma-metadata.index.yaml" node_inventory: "figma-node-inventory.yaml" @@ -81,12 +79,6 @@ readiness: - "high" require_source_integrity: true require_visual_requirements: true - require_visual_spec_package: true - require_visual_spec_package_source_refs: true - require_visual_spec_package_provider_evidence: true - require_visual_spec_package_ci_assertions: true - require_visual_spec_package_provider_product_gap_separation: true - require_visual_preview_ia_matrix_interactions: true require_visual_source_refs: true require_fidelity_rules_applied: true require_visual_parity_plan: true @@ -97,6 +89,13 @@ readiness: require_figma_layout_normalization_for_figma: true require_figma_layout_normalization_node_coverage_for_figma: "100%" require_bounded_inference_for_visual_claims: true + require_static_html_delivery: true + require_static_html_visual_ir: true + require_static_html_operation_replay: true + require_static_html_motion_anchors: true + require_static_html_viewport_screenshots: true + require_static_html_visual_diff: true + require_static_html_clarification_resolution: true fail_on_unbounded_inference_for_figma: true fail_on_unsupported_visual_claims: true fail_on_blocker_lint_errors: true @@ -112,5 +111,5 @@ capture: figma_layout_normalization_mode: "derived_from_verified_metadata" preserve_raw_metadata: true reject_truncated_metadata_shards: true - allow_screenshot_only_intake: true - default_screenshot_level: "L0" + allow_screenshot_only_intake: false + default_screenshot_level: "source_blocked_until_delivery_validation" diff --git a/extensions/intake/extension.yml b/extensions/intake/extension.yml index 707738a966..a329c81ab4 100644 --- a/extensions/intake/extension.yml +++ b/extensions/intake/extension.yml @@ -1,10 +1,10 @@ -schema_version: "1.0" +schema_version: "1.0" extension: id: intake name: "Intake" - version: "0.1.5" - description: "Normalize PRD, visual design/spec packages, preview evidence, and test-case evidence into SDD-ready intake artifacts" + version: "0.2.0" + description: "Normalize PRD, visual design, final static HTML delivery, and test-case evidence into SDD-ready intake artifacts" author: "bigsmartben" repository: "https://github.com/bigsmartben/spec-kit-intake" homepage: "https://github.com/bigsmartben/spec-kit-intake" @@ -22,7 +22,7 @@ provides: commands: - name: "speckit.intake.visual-design" file: "commands/speckit.intake.visual-design.md" - description: "Capture or validate visual design evidence, Figma-backed resources, visual requirements, and structured visual spec packages for the active feature" + description: "Orchestrate visual design intake into source-backed visual IR, user-confirmed operation replay, and final static HTML delivery" - name: "speckit.intake.prd" file: "commands/speckit.intake.prd.md" description: "Capture or validate PRD evidence and normalize product intent, scope, rules, and acceptance inputs for SDD workflows" @@ -40,8 +40,8 @@ hooks: before_specify: command: "speckit.intake.visual-design" optional: true - prompt: "Validate visual design intake evidence before downstream SDD projection?" - description: "Checks visual design intake readiness before downstream workflows project source evidence" + prompt: "Validate visual design static HTML delivery evidence before downstream SDD projection?" + description: "Checks visual intake and static HTML delivery readiness before downstream workflows project source evidence" tags: - "intake" @@ -49,13 +49,14 @@ tags: - "requirements" - "validation" - "figma" + - "static-html" defaults: artifacts: base_dir: "specs/{feature}/intake" visual_design_dir: "visual-design" - previews_dir: "previews" - visual_spec_package_dir: "visual-spec-package" + visual_ir_dir: "visual-ir" + static_html_delivery_dir: "delivery" prd_dir: "prd" test_cases_dir: "test-cases" prd_source_manifest: "source-manifest.yaml" @@ -65,29 +66,30 @@ defaults: prd_intake: "prd-intake.yaml" visual_requirements: "visual-requirements.yaml" test_case_intake: "test-case-intake.yaml" - preview_html: "preview.html" - component_coverage: "component-coverage.yaml" - viewport_coverage: "viewport-coverage.yaml" - visual_previews_known_gaps: "known-gaps.md" - visual_previews_screenshots_dir: "screenshots" + static_html_entry: "index.html" + static_html_assets_dir: "assets" + static_html_screenshots_dir: "screenshots" + static_html_render_replay_report: "render-replay-report.yaml" + static_html_evidence_packet: "evidence-packet.md" + visual_ir_asset_inventory: "asset-inventory.yaml" + visual_ir_layout_tree: "layout-tree.yaml" + visual_ir_component_model: "component-model.yaml" + visual_ir_page_route_model: "page-route-model.yaml" + visual_ir_interaction_model: "interaction-model.yaml" + visual_ir_motion_anchor_model: "motion-anchor-model.yaml" + visual_ir_clarification_log: "clarification-log.yaml" intake_validator_common: "scripts/python/intake_validator_common.py" prd_validator: "scripts/python/validate_prd_intake.py" test_cases_validator: "scripts/python/validate_test_cases_intake.py" visual_design_validator: "scripts/python/validate_visual_design_intake.py" figma_metadata_capture_script: "scripts/python/capture_figma_metadata_shards.py" figma_layout_normalization_script: "scripts/python/normalize_figma_layout.py" - visual_previews_validator: "scripts/python/validate_visual_previews.py" - visual_spec_package: "visual-spec.yaml" - visual_spec_assertions: "visual-spec-assertions.yaml" - visual_spec_package_evidence_packet: "visual-spec-evidence-packet.md" - visual_spec_package_validator: "scripts/python/validate_visual_spec_package.py" + static_html_delivery_validator: "scripts/python/validate_static_html_delivery.py" prd_contract: "templates/intake-prd-contract.md" prd_evidence_packet_template: "templates/intake-prd-evidence-packet-template.md" visual_design_contract: "templates/intake-visual-design-contract.md" visual_design_evidence_packet_template: "templates/intake-visual-design-evidence-packet-template.md" - visual_previews_contract: "templates/intake-visual-previews-contract.md" - visual_spec_package_contract: "templates/intake-visual-spec-package-contract.md" - visual_spec_package_evidence_packet_template: "templates/intake-visual-spec-package-evidence-packet-template.md" + static_html_delivery_contract: "templates/intake-static-html-delivery-contract.md" test_cases_contract: "templates/intake-test-cases-contract.md" test_cases_evidence_packet_template: "templates/intake-test-cases-evidence-packet-template.md" schemas_dir: "templates/schemas" @@ -100,10 +102,7 @@ defaults: figma_metadata_index_schema: "templates/schemas/figma-metadata-index.schema.json" figma_node_inventory_schema: "templates/schemas/figma-node-inventory.schema.json" figma_normalized_tree_schema: "templates/schemas/figma-normalized-tree.schema.json" - component_coverage_schema: "templates/schemas/component-coverage.schema.json" - viewport_coverage_schema: "templates/schemas/viewport-coverage.schema.json" - visual_spec_package_schema: "templates/schemas/visual-spec-package.schema.json" - visual_spec_assertions_schema: "templates/schemas/visual-spec-assertions.schema.json" + static_html_delivery_schema: "templates/schemas/static-html-delivery.schema.json" metadata_glob: "figma-metadata.part-*.xml" metadata_index: "figma-metadata.index.yaml" node_inventory: "figma-node-inventory.yaml" @@ -131,12 +130,6 @@ defaults: - "high" require_source_integrity: true require_visual_requirements: true - require_visual_spec_package: true - require_visual_spec_package_source_refs: true - require_visual_spec_package_provider_evidence: true - require_visual_spec_package_ci_assertions: true - require_visual_spec_package_provider_product_gap_separation: true - require_visual_preview_ia_matrix_interactions: true require_visual_source_refs: true require_fidelity_rules_applied: true require_visual_parity_plan: true @@ -147,6 +140,13 @@ defaults: require_figma_layout_normalization_for_figma: true require_figma_layout_normalization_node_coverage_for_figma: "100%" require_bounded_inference_for_visual_claims: true + require_static_html_delivery: true + require_static_html_visual_ir: true + require_static_html_operation_replay: true + require_static_html_motion_anchors: true + require_static_html_viewport_screenshots: true + require_static_html_visual_diff: true + require_static_html_clarification_resolution: true fail_on_unbounded_inference_for_figma: true fail_on_unsupported_visual_claims: true fail_on_blocker_lint_errors: true @@ -161,5 +161,5 @@ defaults: figma_layout_normalization_mode: "derived_from_verified_metadata" preserve_raw_metadata: true reject_truncated_metadata_shards: true - allow_screenshot_only_intake: true - default_screenshot_level: "L0" + allow_screenshot_only_intake: false + default_screenshot_level: "source_blocked_until_delivery_validation" diff --git a/extensions/intake/scripts/python/intake_validator_common.py b/extensions/intake/scripts/python/intake_validator_common.py index aa6bb510ee..67501eff3a 100644 --- a/extensions/intake/scripts/python/intake_validator_common.py +++ b/extensions/intake/scripts/python/intake_validator_common.py @@ -50,10 +50,9 @@ def is_supporting_visual_artifact_ref(value: Any) -> bool: supporting_markers = ( "visual-evidence-packet.md", "figma-evidence-packet.md", - "visual-spec-evidence-packet.md", - "preview.html", - "/previews/", - "previews/", + "delivery/index.html", + "/delivery/", + "delivery/", "/screenshots/", "screenshots/", "visual-diff", diff --git a/extensions/intake/scripts/python/validate_static_html_delivery.py b/extensions/intake/scripts/python/validate_static_html_delivery.py new file mode 100644 index 0000000000..a64de2c23c --- /dev/null +++ b/extensions/intake/scripts/python/validate_static_html_delivery.py @@ -0,0 +1,675 @@ +#!/usr/bin/env python3 +"""Validate Spec Kit visual static HTML delivery bundles.""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +from html.parser import HTMLParser +from pathlib import Path +from typing import Any + +try: + import yaml +except ImportError: # pragma: no cover - exercised in user environments + yaml = None + +from intake_validator_common import parse_evidence_packet_status, validate_json_schema + + +BLOCKERS = { + "SOURCE_INTAKE_BLOCKED": "STATIC_HTML_SOURCE_INTAKE_BLOCKED", + "REQUIRED_ARTIFACT_MISSING": "STATIC_HTML_REQUIRED_ARTIFACT_MISSING", + "SCHEMA_INVALID": "STATIC_HTML_SCHEMA_INVALID", + "IR_BLOCKED": "STATIC_HTML_IR_BLOCKED", + "CLARIFICATION_REQUIRED": "STATIC_HTML_CLARIFICATION_REQUIRED", + "ASSET_INCOMPLETE": "STATIC_HTML_ASSET_INCOMPLETE", + "LAYOUT_INCOMPLETE": "STATIC_HTML_LAYOUT_INCOMPLETE", + "COMPONENT_STATE_INCOMPLETE": "STATIC_HTML_COMPONENT_STATE_INCOMPLETE", + "PAGE_ROUTE_INCOMPLETE": "STATIC_HTML_PAGE_ROUTE_INCOMPLETE", + "OPERATION_REPLAY_INCOMPLETE": "STATIC_HTML_OPERATION_REPLAY_INCOMPLETE", + "MOTION_ANCHOR_INCOMPLETE": "STATIC_HTML_MOTION_ANCHOR_INCOMPLETE", + "VIEWPORT_CAPTURE_INCOMPLETE": "STATIC_HTML_VIEWPORT_CAPTURE_INCOMPLETE", + "VISUAL_DIFF_BLOCKED": "STATIC_HTML_VISUAL_DIFF_BLOCKED", + "READY_WITHOUT_EVIDENCE": "STATIC_HTML_READY_WITHOUT_EVIDENCE", +} + +IR_FILES = { + "asset_inventory": "asset-inventory.yaml", + "layout_tree": "layout-tree.yaml", + "component_model": "component-model.yaml", + "page_route_model": "page-route-model.yaml", + "interaction_model": "interaction-model.yaml", + "motion_anchor_model": "motion-anchor-model.yaml", + "clarification_log": "clarification-log.yaml", +} + +ANCHOR_ATTRS = ( + "id", + "data-visual-id", + "data-page-id", + "data-route-id", + "data-component-id", + "data-state-id", + "data-operation-id", + "data-motion-id", +) + +PAGE_ANCHOR_ATTRS = ("id", "data-page-id", "data-route-id") +COMPONENT_ANCHOR_ATTRS = ("id", "data-component-id", "data-state-id") +OPERATION_TARGET_ANCHOR_ATTRS = ("id", "data-operation-id", "data-component-id") +OPERATION_RESULT_ANCHOR_ATTRS = ("id", "data-state-id", "data-page-id", "data-route-id") +MOTION_ANCHOR_ATTRS = ("id", "data-motion-id", "data-state-id", "data-component-id") +VISUAL_ANCHOR_ATTRS = ("id", "data-visual-id") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("delivery_dir", help="Directory containing delivery/index.html static HTML artifacts") + parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON") + args = parser.parse_args() + + delivery_dir = Path(args.delivery_dir) + blocker_codes: list[str] = [] + warnings: list[str] = [] + details: dict[str, Any] = {"delivery_dir": str(delivery_dir)} + + if not delivery_dir.exists() or not delivery_dir.is_dir(): + blocker_codes.append(BLOCKERS["REQUIRED_ARTIFACT_MISSING"]) + return emit(args.json, details, sorted(set(blocker_codes)), warnings) + + intake_dir = delivery_dir.parent + validate_source_intake(intake_dir, details, blocker_codes) + validate_required_artifacts(delivery_dir, details, blocker_codes) + ir_docs = validate_visual_ir(intake_dir, details, blocker_codes) + report = load_render_replay_report(delivery_dir, details, blocker_codes) + + html_path = delivery_dir / "index.html" + html_text = html_path.read_text(encoding="utf-8", errors="replace") if html_path.exists() else "" + html_index = HtmlIndex.from_text(html_text) + validate_html_entry(html_text, html_index, details, blocker_codes) + validate_report_refs(delivery_dir, report, html_index, ir_docs, details, blocker_codes) + validate_clarifications(ir_docs.get("clarification_log", {}), report, details, blocker_codes) + validate_evidence_packet(delivery_dir, details, blocker_codes, warnings) + + return emit(args.json, details, sorted(set(blocker_codes)), warnings) + + +def validate_source_intake( + intake_dir: Path, + details: dict[str, Any], + blocker_codes: list[str], +) -> None: + validator = Path(__file__).resolve().with_name("validate_visual_design_intake.py") + result = subprocess.run( + [sys.executable, str(validator), str(intake_dir), "--json"], + text=True, + capture_output=True, + ) + try: + payload = json.loads(result.stdout or "{}") + except json.JSONDecodeError: + payload = {} + details["source_intake"] = { + "path": str(intake_dir), + "validator": str(validator), + "status": payload.get("status"), + "blockers": payload.get("blockers"), + } + if result.returncode != 0 or payload.get("status") != "PASS": + blocker_codes.append(BLOCKERS["SOURCE_INTAKE_BLOCKED"]) + + +def validate_required_artifacts( + delivery_dir: Path, + details: dict[str, Any], + blocker_codes: list[str], +) -> None: + required = { + "index_html": delivery_dir / "index.html", + "render_replay_report": delivery_dir / "render-replay-report.yaml", + "evidence_packet": delivery_dir / "evidence-packet.md", + "assets": delivery_dir / "assets", + "screenshots": delivery_dir / "screenshots", + } + missing = [ + name + for name, path in required.items() + if not path.exists() or (name in {"assets", "screenshots"} and not path.is_dir()) + ] + details["required_artifacts"] = {"missing": missing} + if missing: + blocker_codes.append(BLOCKERS["REQUIRED_ARTIFACT_MISSING"]) + + +def load_yaml(path: Path, blocker_codes: list[str]) -> dict[str, Any]: + if yaml is None: + blocker_codes.append(BLOCKERS["SCHEMA_INVALID"]) + return {} + try: + data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + except yaml.YAMLError: + blocker_codes.append(BLOCKERS["SCHEMA_INVALID"]) + return {} + return data if isinstance(data, dict) else {} + + +def validate_visual_ir( + intake_dir: Path, + details: dict[str, Any], + blocker_codes: list[str], +) -> dict[str, dict[str, Any]]: + ir_dir = intake_dir / "visual-ir" + docs: dict[str, dict[str, Any]] = {} + summary: dict[str, Any] = {"dir": str(ir_dir), "missing": [], "blocked": []} + for key, filename in IR_FILES.items(): + path = ir_dir / filename + if not path.exists(): + summary["missing"].append(filename) + continue + doc = load_yaml(path, blocker_codes) + docs[key] = doc + blockers = doc.get("blockers") + ready_gate = str(doc.get("ready_gate") or "").upper() + if ready_gate != "PASS" or (isinstance(blockers, list) and blockers): + summary["blocked"].append(filename) + details["visual_ir"] = summary + if summary["missing"]: + blocker_codes.append(BLOCKERS["REQUIRED_ARTIFACT_MISSING"]) + if summary["blocked"]: + blocker_codes.append(BLOCKERS["IR_BLOCKED"]) + if "asset-inventory.yaml" in summary["missing"] or "asset-inventory.yaml" in summary["blocked"]: + blocker_codes.append(BLOCKERS["ASSET_INCOMPLETE"]) + if "layout-tree.yaml" in summary["missing"] or "layout-tree.yaml" in summary["blocked"]: + blocker_codes.append(BLOCKERS["LAYOUT_INCOMPLETE"]) + if "component-model.yaml" in summary["missing"] or "component-model.yaml" in summary["blocked"]: + blocker_codes.append(BLOCKERS["COMPONENT_STATE_INCOMPLETE"]) + if "page-route-model.yaml" in summary["missing"] or "page-route-model.yaml" in summary["blocked"]: + blocker_codes.append(BLOCKERS["PAGE_ROUTE_INCOMPLETE"]) + if "interaction-model.yaml" in summary["missing"] or "interaction-model.yaml" in summary["blocked"]: + blocker_codes.append(BLOCKERS["OPERATION_REPLAY_INCOMPLETE"]) + if "motion-anchor-model.yaml" in summary["missing"] or "motion-anchor-model.yaml" in summary["blocked"]: + blocker_codes.append(BLOCKERS["MOTION_ANCHOR_INCOMPLETE"]) + return docs + + +def load_render_replay_report( + delivery_dir: Path, + details: dict[str, Any], + blocker_codes: list[str], +) -> dict[str, Any]: + report_path = delivery_dir / "render-replay-report.yaml" + if not report_path.exists(): + return {} + validate_json_schema( + instance_path=report_path, + schema_name="static-html-delivery.schema.json", + details_key="static_html_delivery", + details=details, + blocker_codes=blocker_codes, + schema_error_code=BLOCKERS["SCHEMA_INVALID"], + ) + report = load_yaml(report_path, blocker_codes) + details["render_replay_report"] = { + "ready_gate": report.get("ready_gate"), + "blockers": report.get("blockers"), + "asset_count": len(report.get("assets", []) if isinstance(report.get("assets"), list) else []), + "page_count": len(report.get("pages", []) if isinstance(report.get("pages"), list) else []), + "component_count": len(report.get("components", []) if isinstance(report.get("components"), list) else []), + "operation_count": len(report.get("operations", []) if isinstance(report.get("operations"), list) else []), + "motion_count": len(report.get("motion_anchors", []) if isinstance(report.get("motion_anchors"), list) else []), + "viewport_count": len(report.get("viewports", []) if isinstance(report.get("viewports"), list) else []), + } + if report.get("ready_gate") != "PASS" or report.get("blockers"): + blocker_codes.append(BLOCKERS["READY_WITHOUT_EVIDENCE"]) + return report + + +class HtmlIndex(HTMLParser): + def __init__(self) -> None: + super().__init__(convert_charrefs=True) + self.anchor_tags: dict[str, list[dict[str, Any]]] = {} + self.anchor_attrs: dict[str, set[str]] = {} + self.delivery_root_found = False + + @classmethod + def from_text(cls, html_text: str) -> "HtmlIndex": + index = cls() + if html_text: + index.feed(html_text) + return index + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + attr_map = {name: value or "" for name, value in attrs} + if "data-delivery-root" in attr_map: + self.delivery_root_found = True + tag_record = {"tag": tag, "attrs": attr_map} + for attr in ANCHOR_ATTRS: + value = attr_map.get(attr) + if value: + self.anchor_tags.setdefault(value, []).append(tag_record) + self.anchor_attrs.setdefault(value, set()).add(attr) + + def has_ref(self, ref: str) -> bool: + fragment = ref_fragment(ref) + if not fragment: + return False + return fragment in self.anchor_tags + + def has_typed_ref(self, ref: str, allowed_attrs: tuple[str, ...]) -> bool: + fragment = ref_fragment(ref) + if not fragment: + return False + attrs = self.anchor_attrs.get(fragment, set()) + return bool(attrs.intersection(allowed_attrs)) + + def duplicate_anchor_values(self) -> list[str]: + return sorted(value for value, tags in self.anchor_tags.items() if len(tags) > 1) + + +def ref_fragment(ref: str) -> str: + normalized = str(ref or "").strip().replace("\\", "/") + if "#" in normalized: + return normalized.rsplit("#", 1)[1].strip() + return normalized.strip() + + +def validate_html_entry( + html_text: str, + html_index: HtmlIndex, + details: dict[str, Any], + blocker_codes: list[str], +) -> None: + duplicate_anchors = html_index.duplicate_anchor_values() + details["html_entry"] = { + "present": bool(html_text), + "delivery_root_found": html_index.delivery_root_found, + "duplicate_anchor_values": duplicate_anchors, + } + if not html_text or not html_index.delivery_root_found or duplicate_anchors: + blocker_codes.append(BLOCKERS["REQUIRED_ARTIFACT_MISSING"]) + + +def asset_path_exists(delivery_dir: Path, asset_path: str) -> bool: + value = str(asset_path or "").strip().replace("\\", "/") + if not value: + return False + candidate = (delivery_dir / value).resolve() + assets_root = (delivery_dir / "assets").resolve() + try: + candidate.relative_to(assets_root) + except ValueError: + return False + return candidate.is_file() + + +def screenshot_ref_exists(delivery_dir: Path, screenshot_ref: str) -> bool: + value = str(screenshot_ref or "").strip().replace("\\", "/") + if not value: + return False + candidate = (delivery_dir / value).resolve() + screenshot_root = (delivery_dir / "screenshots").resolve() + try: + candidate.relative_to(screenshot_root) + except ValueError: + return False + return candidate.is_file() + + +def split_ref(ref: str) -> tuple[str, str]: + normalized = str(ref or "").strip().replace("\\", "/") + if "#" not in normalized: + return normalized, "" + path_part, fragment = normalized.rsplit("#", 1) + return path_part.strip(), fragment.strip() + + +def resolved_local_ref_path(delivery_dir: Path, ref: str) -> Path | None: + path_part, _fragment = split_ref(ref) + if not path_part or is_remote_ref(path_part): + return None + return (delivery_dir / path_part).resolve() + + +def is_remote_ref(value: str) -> bool: + return value.startswith(("http://", "https://", "figma://")) + + +def collect_ir_ids(value: Any, parent_id: str | None = None) -> set[str]: + ids: set[str] = set() + if isinstance(value, dict): + current_id = value.get("id") + if isinstance(current_id, str) and current_id: + ids.add(current_id) + parent_id = current_id + for key, child in value.items(): + if key in {"states", "variants"} and isinstance(child, list) and parent_id: + for item in child: + if isinstance(item, str) and item: + ids.add(f"{parent_id}-{item}") + elif isinstance(item, dict): + item_id = item.get("id") + if isinstance(item_id, str) and item_id: + ids.add(item_id) + ids.update(collect_ir_ids(child, parent_id)) + elif isinstance(value, list): + for item in value: + ids.update(collect_ir_ids(item, parent_id)) + return ids + + +def build_ir_indexes(delivery_dir: Path, ir_docs: dict[str, dict[str, Any]]) -> tuple[set[Path], dict[Path, set[str]]]: + ir_dir = delivery_dir.parent / "visual-ir" + required_paths = {(ir_dir / filename).resolve() for filename in IR_FILES.values()} + ids_by_path: dict[Path, set[str]] = {} + for key, filename in IR_FILES.items(): + doc = ir_docs.get(key, {}) + ids_by_path[(ir_dir / filename).resolve()] = collect_ir_ids(doc) + return required_paths, ids_by_path + + +def ir_ref_exists(delivery_dir: Path, ids_by_path: dict[Path, set[str]], ref: str) -> bool: + path_part, fragment = split_ref(ref) + if not path_part or not fragment: + return False + path = (delivery_dir / path_part).resolve() + return fragment in ids_by_path.get(path, set()) + + +def validate_report_refs( + delivery_dir: Path, + report: dict[str, Any], + html_index: HtmlIndex, + ir_docs: dict[str, dict[str, Any]], + details: dict[str, Any], + blocker_codes: list[str], +) -> None: + missing_html_refs: list[str] = [] + missing_assets: list[str] = [] + missing_source_refs: list[str] = [] + mismatched_visual_ir_refs: list[str] = [] + missing_ir_refs: list[str] = [] + invalid_operation_refs: list[str] = [] + blocked_assets: list[str] = [] + blocked_pages: list[str] = [] + blocked_components: list[str] = [] + blocked_operations: list[str] = [] + blocked_motion: list[str] = [] + missing_screenshots: list[str] = [] + visual_diff_blocked: list[str] = [] + + required_ir_paths, ir_ids_by_path = build_ir_indexes(delivery_dir, ir_docs) + reported_ir_refs = report.get("visual_ir_refs", []) if isinstance(report.get("visual_ir_refs"), list) else [] + reported_ir_paths = { + path for ref in reported_ir_refs if (path := resolved_local_ref_path(delivery_dir, str(ref))) is not None + } + if reported_ir_paths != required_ir_paths: + missing_from_report = sorted(str(path) for path in required_ir_paths - reported_ir_paths) + unexpected_in_report = sorted(str(path) for path in reported_ir_paths - required_ir_paths) + mismatched_visual_ir_refs.extend(missing_from_report + unexpected_in_report) + + source_intake_path = resolved_local_ref_path(delivery_dir, str(report.get("source_intake_ref") or "")) + if source_intake_path is None or not source_intake_path.exists(): + missing_source_refs.append(str(report.get("source_intake_ref") or "")) + + operation_ids = { + str(operation.get("id")) + for operation in report.get("operations", []) + if isinstance(operation, dict) and operation.get("id") + } + + for asset in report.get("assets", []) if isinstance(report.get("assets"), list) else []: + if not isinstance(asset, dict): + continue + asset_id = str(asset.get("id") or "") + if not ir_ref_exists(delivery_dir, ir_ids_by_path, str(asset.get("inventory_ref") or "")): + missing_ir_refs.append(str(asset.get("inventory_ref") or asset_id)) + if asset.get("status") == "blocked" or asset.get("blockers"): + blocked_assets.append(asset_id) + for html_ref in asset.get("html_refs", []) or []: + if not html_index.has_typed_ref(str(html_ref), VISUAL_ANCHOR_ATTRS): + missing_html_refs.append(str(html_ref)) + for local_path in asset.get("local_paths", []) or []: + if not asset_path_exists(delivery_dir, str(local_path)): + missing_assets.append(str(local_path)) + + for page in report.get("pages", []) if isinstance(report.get("pages"), list) else []: + if not isinstance(page, dict): + continue + page_id = str(page.get("id") or "") + if not html_index.has_typed_ref(str(page.get("html_ref") or ""), PAGE_ANCHOR_ATTRS): + missing_html_refs.append(str(page.get("html_ref") or page_id)) + for ref in (page.get("state_refs", []) or []) + (page.get("layout_refs", []) or []): + if not ir_ref_exists(delivery_dir, ir_ids_by_path, str(ref)): + missing_ir_refs.append(str(ref)) + if page.get("status") == "blocked" or page.get("blockers"): + blocked_pages.append(page_id) + + for component in report.get("components", []) if isinstance(report.get("components"), list) else []: + if not isinstance(component, dict): + continue + component_id = str(component.get("id") or "") + if not html_index.has_typed_ref(str(component.get("html_ref") or ""), COMPONENT_ANCHOR_ATTRS): + missing_html_refs.append(str(component.get("html_ref") or component_id)) + if not ir_ref_exists(delivery_dir, ir_ids_by_path, str(component.get("component_model_ref") or "")): + missing_ir_refs.append(str(component.get("component_model_ref") or component_id)) + for ref in component.get("state_refs", []) or []: + if not ir_ref_exists(delivery_dir, ir_ids_by_path, str(ref)): + missing_ir_refs.append(str(ref)) + for operation_ref in component.get("operation_refs", []) or []: + if str(operation_ref) not in operation_ids: + invalid_operation_refs.append(str(operation_ref)) + if component.get("status") == "blocked" or component.get("blockers"): + blocked_components.append(component_id) + + for operation in report.get("operations", []) if isinstance(report.get("operations"), list) else []: + if not isinstance(operation, dict): + continue + operation_id = str(operation.get("id") or "") + if not ir_ref_exists(delivery_dir, ir_ids_by_path, str(operation.get("interaction_model_ref") or "")): + missing_ir_refs.append(str(operation.get("interaction_model_ref") or operation_id)) + if not html_index.has_typed_ref(str(operation.get("target_ref") or ""), OPERATION_TARGET_ANCHOR_ATTRS): + missing_html_refs.append(str(operation.get("target_ref") or operation_id)) + if not html_index.has_typed_ref(str(operation.get("result_ref") or ""), OPERATION_RESULT_ANCHOR_ATTRS): + missing_html_refs.append(str(operation.get("result_ref") or operation_id)) + if operation.get("replay_status") != "pass" or operation.get("blockers"): + blocked_operations.append(operation_id) + + for motion in report.get("motion_anchors", []) if isinstance(report.get("motion_anchors"), list) else []: + if not isinstance(motion, dict): + continue + motion_id = str(motion.get("id") or "") + if not ir_ref_exists(delivery_dir, ir_ids_by_path, str(motion.get("motion_model_ref") or "")): + missing_ir_refs.append(str(motion.get("motion_model_ref") or motion_id)) + for key in ("trigger_ref", "affected_ref", "end_state_ref"): + if not html_index.has_typed_ref(str(motion.get(key) or ""), MOTION_ANCHOR_ATTRS): + missing_html_refs.append(str(motion.get(key) or motion_id)) + if motion.get("replay_status") not in {"pass", "not_applicable"} or motion.get("blockers"): + blocked_motion.append(motion_id) + + for viewport in report.get("viewports", []) if isinstance(report.get("viewports"), list) else []: + if not isinstance(viewport, dict): + continue + viewport_id = str(viewport.get("id") or "") + for page_ref in viewport.get("page_refs", []) or []: + if not html_index.has_typed_ref(str(page_ref), PAGE_ANCHOR_ATTRS): + missing_html_refs.append(str(page_ref)) + for screenshot_ref in viewport.get("screenshot_refs", []) or []: + if not screenshot_ref_exists(delivery_dir, str(screenshot_ref)): + missing_screenshots.append(str(screenshot_ref)) + if viewport.get("render_status") != "pass" or viewport.get("blockers"): + missing_screenshots.append(viewport_id) + + for visual_diff in report.get("visual_diffs", []) if isinstance(report.get("visual_diffs"), list) else []: + if not isinstance(visual_diff, dict): + continue + diff_id = str(visual_diff.get("id") or "") + screenshot_ref = str(visual_diff.get("screenshot_ref") or "") + if screenshot_ref and not screenshot_ref_exists(delivery_dir, screenshot_ref): + missing_screenshots.append(screenshot_ref) + if visual_diff.get("status") != "pass" or visual_diff.get("blockers"): + visual_diff_blocked.append(diff_id) + + details["delivery_refs"] = { + "missing_html_refs": sorted(set(missing_html_refs)), + "missing_assets": sorted(set(missing_assets)), + "missing_source_refs": sorted(set(missing_source_refs)), + "mismatched_visual_ir_refs": sorted(set(mismatched_visual_ir_refs)), + "missing_ir_refs": sorted(set(missing_ir_refs)), + "invalid_operation_refs": sorted(set(invalid_operation_refs)), + "blocked_assets": sorted(set(blocked_assets)), + "blocked_pages": sorted(set(blocked_pages)), + "blocked_components": sorted(set(blocked_components)), + "blocked_operations": sorted(set(blocked_operations)), + "blocked_motion": sorted(set(blocked_motion)), + "missing_screenshots": sorted(set(missing_screenshots)), + "visual_diff_blocked": sorted(set(visual_diff_blocked)), + } + if missing_html_refs: + blocker_codes.append(BLOCKERS["REQUIRED_ARTIFACT_MISSING"]) + if missing_source_refs: + blocker_codes.append(BLOCKERS["SOURCE_INTAKE_BLOCKED"]) + if mismatched_visual_ir_refs or missing_ir_refs: + blocker_codes.append(BLOCKERS["IR_BLOCKED"]) + if invalid_operation_refs: + blocker_codes.append(BLOCKERS["OPERATION_REPLAY_INCOMPLETE"]) + if missing_assets or blocked_assets: + blocker_codes.append(BLOCKERS["ASSET_INCOMPLETE"]) + if blocked_pages: + blocker_codes.append(BLOCKERS["PAGE_ROUTE_INCOMPLETE"]) + if blocked_components: + blocker_codes.append(BLOCKERS["COMPONENT_STATE_INCOMPLETE"]) + if blocked_operations: + blocker_codes.append(BLOCKERS["OPERATION_REPLAY_INCOMPLETE"]) + if blocked_motion: + blocker_codes.append(BLOCKERS["MOTION_ANCHOR_INCOMPLETE"]) + if missing_screenshots: + blocker_codes.append(BLOCKERS["VIEWPORT_CAPTURE_INCOMPLETE"]) + if visual_diff_blocked: + blocker_codes.append(BLOCKERS["VISUAL_DIFF_BLOCKED"]) + + +def validate_clarifications( + clarification_log: dict[str, Any], + report: dict[str, Any], + details: dict[str, Any], + blocker_codes: list[str], +) -> None: + unanswered: list[str] = [] + shape_errors: list[str] = [] + required_count = 0 + resolved_required_count = 0 + valid_blockers = set(BLOCKERS.values()) + for question in clarification_log.get("questions", []) if isinstance(clarification_log.get("questions"), list) else []: + if not isinstance(question, dict): + shape_errors.append("") + continue + question_id = str(question.get("id") or "") + required_fields = ( + "id", + "target_artifact", + "blocked_delivery_surface", + "blocker_code", + "question", + "allowed_answer_shape", + "required_for_html", + "status", + "confirmed_by_user", + "source_refs", + ) + missing_fields = [field for field in required_fields if field not in question] + if missing_fields: + shape_errors.append(f"{question_id}:missing:{','.join(missing_fields)}") + if question.get("status") not in {"unanswered", "answered", "out_of_scope"}: + shape_errors.append(f"{question_id}:invalid_status") + if question.get("blocker_code") not in valid_blockers: + shape_errors.append(f"{question_id}:invalid_blocker_code") + if not isinstance(question.get("source_refs"), list): + shape_errors.append(f"{question_id}:invalid_source_refs") + if question.get("status") in {"answered", "out_of_scope"} and not question.get("answer"): + shape_errors.append(f"{question_id}:missing_answer") + if question.get("status") == "answered" and question.get("confirmed_by_user") is not True: + shape_errors.append(f"{question_id}:missing_user_confirmation") + if question.get("required_for_html") is True and question.get("status") == "unanswered": + unanswered.append(question_id) + if question.get("required_for_html") is True: + required_count += 1 + if question.get("status") in {"answered", "out_of_scope"}: + resolved_required_count += 1 + report_unanswered = ( + report.get("clarifications", {}).get("unanswered_required_question_ids", []) + if isinstance(report.get("clarifications"), dict) + else [] + ) + for question_id in report_unanswered if isinstance(report_unanswered, list) else []: + unanswered.append(str(question_id)) + report_required_count = ( + report.get("clarifications", {}).get("required_question_count") + if isinstance(report.get("clarifications"), dict) + else None + ) + report_answered_count = ( + report.get("clarifications", {}).get("answered_required_question_count") + if isinstance(report.get("clarifications"), dict) + else None + ) + if report_required_count != required_count: + shape_errors.append("report_required_question_count_mismatch") + if report_answered_count != resolved_required_count: + shape_errors.append("report_answered_required_question_count_mismatch") + details["clarifications"] = { + "required_question_count": required_count, + "resolved_required_question_count": resolved_required_count, + "unanswered_required_question_ids": sorted(set(unanswered)), + "shape_errors": sorted(set(shape_errors)), + } + if unanswered or shape_errors: + blocker_codes.append(BLOCKERS["CLARIFICATION_REQUIRED"]) + + +def validate_evidence_packet( + delivery_dir: Path, + details: dict[str, Any], + blocker_codes: list[str], + warnings: list[str], +) -> None: + packet = delivery_dir / "evidence-packet.md" + if not packet.exists(): + blocker_codes.append(BLOCKERS["READY_WITHOUT_EVIDENCE"]) + return + status = parse_evidence_packet_status(packet.read_text(encoding="utf-8", errors="replace")) + details["delivery_evidence_packet"] = status["metadata"] + warnings.extend(status["warnings"]) + packet_blockers = status["metadata"].get("blockers") + has_packet_blockers = isinstance(packet_blockers, list) and bool(packet_blockers) + if status["errors"] or status["ready_gate"] != "PASS" or has_packet_blockers: + blocker_codes.append(BLOCKERS["READY_WITHOUT_EVIDENCE"]) + + +def emit(json_mode: bool, details: dict[str, Any], blockers: list[str], warnings: list[str]) -> int: + result = { + "status": "BLOCKED" if blockers else "PASS", + "blockers": blockers, + "warnings": warnings, + "details": details, + } + if json_mode: + print(json.dumps(result, indent=2, sort_keys=True)) + else: + print(f"Static HTML delivery readiness: {result['status']}") + if blockers: + print("Blockers:") + for blocker in blockers: + print(f"- {blocker}") + if warnings: + print("Warnings:") + for warning in warnings: + print(f"- {warning}") + return 1 if blockers else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/extensions/intake/scripts/python/validate_visual_previews.py b/extensions/intake/scripts/python/validate_visual_previews.py deleted file mode 100644 index d054072230..0000000000 --- a/extensions/intake/scripts/python/validate_visual_previews.py +++ /dev/null @@ -1,677 +0,0 @@ -#!/usr/bin/env python3 -"""Validate Spec Kit provider-neutral HTML mock delivery bundles.""" - -from __future__ import annotations - -import argparse -import json -import re -import subprocess -import sys -from html.parser import HTMLParser -from pathlib import Path -from typing import Any - -try: - import yaml -except ImportError: # pragma: no cover - exercised in user environments - yaml = None - -from intake_validator_common import supporting_visual_artifact_refs, validate_json_schema - - -BLOCKERS = { - "SOURCE_INTAKE_BLOCKED": "VISUAL_PREVIEW_SOURCE_INTAKE_BLOCKED", - "REQUIRED_ARTIFACT_MISSING": "VISUAL_PREVIEW_REQUIRED_ARTIFACT_MISSING", - "FIGMA_NODE_COVERAGE_INCOMPLETE": "VISUAL_PREVIEW_FIGMA_NODE_COVERAGE_INCOMPLETE", - "COMPONENT_STATE_COVERAGE_INCOMPLETE": "VISUAL_PREVIEW_COMPONENT_STATE_COVERAGE_INCOMPLETE", - "IA_MATRIX_INCOMPLETE": "VISUAL_PREVIEW_IA_MATRIX_INCOMPLETE", - "PAGE_COVERAGE_INCOMPLETE": "VISUAL_PREVIEW_PAGE_COVERAGE_INCOMPLETE", - "ASSET_TRACEABILITY_INCOMPLETE": "VISUAL_PREVIEW_ASSET_TRACEABILITY_INCOMPLETE", - "VIEWPORT_CAPTURE_INCOMPLETE": "VISUAL_PREVIEW_VIEWPORT_CAPTURE_INCOMPLETE", - "VISUAL_DIFF_BLOCKED": "VISUAL_PREVIEW_VISUAL_DIFF_BLOCKED", - "KNOWN_GAP_UNRESOLVED": "VISUAL_PREVIEW_KNOWN_GAP_UNRESOLVED", - "SCHEMA_INVALID": "VISUAL_PREVIEW_SCHEMA_INVALID", -} - -REQUIRED_PREVIEW_SECTIONS = { - "mock-page", - "ia-matrix-overview", - "page-state-enumeration", - "page-ia-matrix", - "component-state-enumeration", - "component-ia-matrix", - "coverage-evidence-conclusion", -} - -VISUALIZED_COMPONENT_KINDS = { - "component", - "component-state", - "component-instance", - "mock-component", - "mock-component-state", - "visual-state", -} - -ANCHOR_ATTRS = ("id", "data-preview-id", "data-interaction-id") - -REQUIRED_PAGE_IA_FIELDS = { - "page_region", - "visual_state", - "user_event", - "precondition", - "system_response", - "state_change", - "transition_or_overlay", - "exception_branch", - "evidence_ref", - "coverage_status", -} - -REQUIRED_COMPONENT_IA_FIELDS = { - "component_state", - "visible_elements", - "action_target", - "user_event", - "precondition", - "immediate_feedback", - "state_change", - "affected_surface", - "disabled_or_error_rule", - "evidence_ref", - "coverage_status", -} - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("preview_dir", help="Directory containing visual-design preview artifacts") - parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON") - args = parser.parse_args() - - html_dir = Path(args.preview_dir) - blocker_codes: list[str] = [] - warnings: list[str] = [] - details: dict[str, Any] = {"preview_dir": str(html_dir)} - - if not html_dir.exists() or not html_dir.is_dir(): - blocker_codes.append(BLOCKERS["REQUIRED_ARTIFACT_MISSING"]) - return emit(args.json, details, sorted(set(blocker_codes)), warnings) - - validate_source_intake(html_dir, details, blocker_codes) - - required_files = { - "preview_html": html_dir / "preview.html", - "component_coverage": html_dir / "component-coverage.yaml", - "viewport_coverage": html_dir / "viewport-coverage.yaml", - "known_gaps": html_dir / "known-gaps.md", - } - missing = [name for name, path in required_files.items() if not path.exists()] - screenshots_dir = html_dir / "screenshots" - if not screenshots_dir.exists() or not screenshots_dir.is_dir(): - missing.append("screenshots") - if missing: - blocker_codes.append(BLOCKERS["REQUIRED_ARTIFACT_MISSING"]) - details["required_artifacts"] = {"missing": missing} - - validate_visual_spec_package_readiness(html_dir, details, blocker_codes) - - component_coverage = load_yaml_artifact( - required_files["component_coverage"], - "component-coverage.schema.json", - "component_coverage", - details, - blocker_codes, - ) - viewport_coverage = load_yaml_artifact( - required_files["viewport_coverage"], - "viewport-coverage.schema.json", - "viewport_coverage", - details, - blocker_codes, - ) - - html_text = "" - if required_files["preview_html"].exists(): - html_text = required_files["preview_html"].read_text(encoding="utf-8", errors="replace") - - visual_spec_ids = load_visual_spec_ids(html_dir, details, blocker_codes) - html_index = HtmlIndex.from_text(html_text) - - validate_preview_html_structure(html_text, html_index, details, blocker_codes) - validate_component_coverage(component_coverage, html_dir, html_text, html_index, visual_spec_ids, details, blocker_codes) - validate_viewport_coverage(html_dir, viewport_coverage, html_text, visual_spec_ids, details, blocker_codes) - validate_known_gaps(required_files["known_gaps"], details, blocker_codes) - - return emit(args.json, details, sorted(set(blocker_codes)), warnings) - - -def validate_source_intake( - html_dir: Path, - details: dict[str, Any], - blocker_codes: list[str], -) -> None: - upstream = html_dir.parent - validator = Path(__file__).resolve().with_name("validate_visual_design_intake.py") - result = subprocess.run( - [sys.executable, str(validator), str(upstream), "--json"], - text=True, - capture_output=True, - ) - try: - payload = json.loads(result.stdout or "{}") - except json.JSONDecodeError: - payload = {} - details["source_intake"] = { - "path": str(upstream), - "validator": str(validator), - "status": payload.get("status"), - "blockers": payload.get("blockers"), - } - if result.returncode != 0 or payload.get("status") != "PASS": - blocker_codes.append(BLOCKERS["SOURCE_INTAKE_BLOCKED"]) - - -def validate_visual_spec_package_readiness( - html_dir: Path, - details: dict[str, Any], - blocker_codes: list[str], -) -> None: - package_dir = html_dir.parent / "visual-spec-package" - validator = Path(__file__).resolve().with_name("validate_visual_spec_package.py") - result = subprocess.run( - [sys.executable, str(validator), str(package_dir), "--json"], - text=True, - capture_output=True, - ) - try: - payload = json.loads(result.stdout or "{}") - except json.JSONDecodeError: - payload = {} - details["visual_spec_package_readiness"] = { - "path": str(package_dir), - "validator": str(validator), - "status": payload.get("status"), - "blockers": payload.get("blockers"), - } - if result.returncode != 0 or payload.get("status") != "PASS": - blocker_codes.append(BLOCKERS["COMPONENT_STATE_COVERAGE_INCOMPLETE"]) - - -def load_yaml_artifact( - path: Path, - schema_name: str, - details_key: str, - details: dict[str, Any], - blocker_codes: list[str], -) -> dict[str, Any]: - if not path.exists(): - return {} - validate_json_schema( - instance_path=path, - schema_name=schema_name, - details_key=details_key, - details=details, - blocker_codes=blocker_codes, - schema_error_code=BLOCKERS["SCHEMA_INVALID"], - ) - if yaml is None: - blocker_codes.append(BLOCKERS["SCHEMA_INVALID"]) - return {} - try: - data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} - except yaml.YAMLError: - blocker_codes.append(BLOCKERS["SCHEMA_INVALID"]) - return {} - return data if isinstance(data, dict) else {} - - -def visual_spec_ref_fragment(ref: str) -> str | None: - normalized_ref = ref.strip().replace("\\", "/") - if "#" not in normalized_ref: - return None - ref_path, fragment = normalized_ref.rsplit("#", 1) - if ref_path.rsplit("/", 1)[-1] != "visual-spec.yaml": - return None - path_parts = {part for part in ref_path.split("/") if part not in {"", ".", ".."}} - if "visual-spec-package" not in path_parts: - return None - fragment = fragment.strip() - return fragment or None - - -def load_visual_spec_ids( - html_dir: Path, - details: dict[str, Any], - blocker_codes: list[str], -) -> set[str]: - visual_spec_path = html_dir.parent / "visual-spec-package" / "visual-spec.yaml" - visual_spec_ids: set[str] = set() - if not visual_spec_path.exists(): - details["visual_spec_refs"] = { - "visual_spec_path": str(visual_spec_path), - "missing_visual_spec_package": True, - "item_count": 0, - } - blocker_codes.append(BLOCKERS["COMPONENT_STATE_COVERAGE_INCOMPLETE"]) - return visual_spec_ids - if yaml is None: - blocker_codes.append(BLOCKERS["SCHEMA_INVALID"]) - return visual_spec_ids - try: - visual_spec = yaml.safe_load(visual_spec_path.read_text(encoding="utf-8")) or {} - except yaml.YAMLError: - blocker_codes.append(BLOCKERS["SCHEMA_INVALID"]) - visual_spec = {} - items = visual_spec.get("items", []) - for item in items if isinstance(items, list) else []: - if isinstance(item, dict) and item.get("id"): - visual_spec_ids.add(str(item["id"])) - details["visual_spec_refs"] = { - "visual_spec_path": str(visual_spec_path), - "missing_visual_spec_package": False, - "item_count": len(visual_spec_ids), - } - if not visual_spec_ids: - blocker_codes.append(BLOCKERS["COMPONENT_STATE_COVERAGE_INCOMPLETE"]) - return visual_spec_ids - - -class HtmlIndex(HTMLParser): - def __init__(self) -> None: - super().__init__(convert_charrefs=True) - self.tags: list[dict[str, Any]] = [] - self.anchor_tags: dict[str, list[dict[str, Any]]] = {} - - @classmethod - def from_text(cls, html_text: str) -> "HtmlIndex": - index = cls() - if html_text: - index.feed(html_text) - return index - - def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: - attr_map = {name: value or "" for name, value in attrs} - tag_record = {"tag": tag, "attrs": attr_map} - self.tags.append(tag_record) - for attr in ANCHOR_ATTRS: - value = attr_map.get(attr) - if value: - tags = self.anchor_tags.setdefault(value, []) - if tag_record not in tags: - tags.append(tag_record) - - def tags_for_fragment(self, fragment: str) -> list[dict[str, Any]]: - return self.anchor_tags.get(fragment, []) - - def duplicate_anchor_values(self) -> list[str]: - return sorted(value for value, tags in self.anchor_tags.items() if len(tags) > 1) - - -def preview_ref_fragment(preview_ref: str) -> str | None: - normalized_ref = preview_ref.strip().replace("\\", "/") - if "#" in normalized_ref: - ref_path, fragment = normalized_ref.rsplit("#", 1) - if ref_path and ref_path.rsplit("/", 1)[-1] != "preview.html": - return None - else: - fragment = normalized_ref - fragment = fragment.strip() - return fragment or None - - -def html_opening_tags_for_fragment(fragment: str, html_text: str) -> list[str]: - tags: list[str] = [] - for match in re.finditer(r"<(?!/|!)[^>]+>", html_text): - tag = match.group(0) - if ( - html_attr_value_exists(tag, "id", fragment) - or html_attr_value_exists(tag, "data-preview-id", fragment) - or html_attr_value_exists(tag, "data-interaction-id", fragment) - ): - tags.append(tag) - return tags - - -def preview_ref_in_html(preview_ref: str, html_text: str) -> bool: - fragment = preview_ref_fragment(preview_ref) - if not fragment: - return False - if fragment.startswith("[") and fragment.endswith("]"): - return fragment in html_text or fragment[1:-1] in html_text - return bool(html_opening_tags_for_fragment(fragment, html_text)) - - -def preview_ref_has_attr(preview_ref: str, html_text: str, attr: str, value: str) -> bool: - fragment = preview_ref_fragment(preview_ref) - if not fragment: - return False - return any( - html_attr_value_exists(tag, attr, value) - for tag in html_opening_tags_for_fragment(fragment, html_text) - ) - - -def preview_ref_is_visualized_component(preview_ref: str, html_text: str) -> bool: - fragment = preview_ref_fragment(preview_ref) - if not fragment: - return False - for tag in html_opening_tags_for_fragment(fragment, html_text): - if any( - html_attr_value_exists(tag, "data-preview-kind", kind) - for kind in VISUALIZED_COMPONENT_KINDS - ): - return True - return False - - -def visual_spec_ref_exists(visual_spec_ref: str, visual_spec_ids: set[str]) -> bool: - fragment = visual_spec_ref_fragment(visual_spec_ref) - return bool(fragment and fragment in visual_spec_ids) - - -def screenshot_ref_exists(html_dir: Path, screenshot_ref: str) -> bool: - ref_path = screenshot_ref.strip().replace("\\", "/") - if not ref_path or ref_path.startswith(("http://", "https://")): - return False - candidate = (html_dir / ref_path).resolve() - root = html_dir.resolve() - try: - candidate.relative_to(root) - except ValueError: - return False - return candidate.is_file() - - -def missing_row_fields(html_text: str, row_pattern: str, required_fields: set[str], attr: str) -> list[str]: - missing_rows: list[str] = [] - for index, match in enumerate(re.finditer(row_pattern, html_text, flags=re.IGNORECASE | re.DOTALL), start=1): - row_html = match.group(0) - present_fields = { - field - for field in required_fields - if html_attr_value_exists(row_html, attr, field) - } - if present_fields and present_fields != required_fields: - missing = ",".join(sorted(required_fields - present_fields)) - missing_rows.append(f"row-{index}:{missing}") - return missing_rows - - -def html_attr_value_exists(html_text: str, attr: str, value: str) -> bool: - return ( - f'{attr}="{value}"' in html_text - or f"{attr}='{value}'" in html_text - ) - - -def validate_preview_html_structure( - html_text: str, - html_index: HtmlIndex, - details: dict[str, Any], - blocker_codes: list[str], -) -> None: - if not html_text: - details["preview_html_structure"] = { - "missing_sections": sorted(REQUIRED_PREVIEW_SECTIONS), - "missing_page_ia_fields": sorted(REQUIRED_PAGE_IA_FIELDS), - "missing_component_ia_fields": sorted(REQUIRED_COMPONENT_IA_FIELDS), - "duplicate_anchor_values": [], - "incomplete_page_ia_rows": [], - "incomplete_component_ia_rows": [], - } - blocker_codes.append(BLOCKERS["IA_MATRIX_INCOMPLETE"]) - return - - missing_sections = [ - section - for section in sorted(REQUIRED_PREVIEW_SECTIONS) - if not html_attr_value_exists(html_text, "data-preview-section", section) - ] - missing_page_ia_fields = [ - field - for field in sorted(REQUIRED_PAGE_IA_FIELDS) - if not html_attr_value_exists(html_text, "data-page-ia-field", field) - ] - missing_component_ia_fields = [ - field - for field in sorted(REQUIRED_COMPONENT_IA_FIELDS) - if not html_attr_value_exists(html_text, "data-component-ia-field", field) - ] - duplicate_anchor_values = html_index.duplicate_anchor_values() - incomplete_page_ia_rows = missing_row_fields( - html_text, - r"]*>.*?", - REQUIRED_PAGE_IA_FIELDS, - "data-page-ia-field", - ) - incomplete_component_ia_rows = missing_row_fields( - html_text, - r"]*>.*?", - REQUIRED_COMPONENT_IA_FIELDS, - "data-component-ia-field", - ) - details["preview_html_structure"] = { - "missing_sections": missing_sections, - "missing_page_ia_fields": missing_page_ia_fields, - "missing_component_ia_fields": missing_component_ia_fields, - "duplicate_anchor_values": duplicate_anchor_values, - "incomplete_page_ia_rows": incomplete_page_ia_rows, - "incomplete_component_ia_rows": incomplete_component_ia_rows, - } - if ( - missing_sections - or missing_page_ia_fields - or missing_component_ia_fields - or duplicate_anchor_values - or incomplete_page_ia_rows - or incomplete_component_ia_rows - ): - blocker_codes.append(BLOCKERS["IA_MATRIX_INCOMPLETE"]) - - -def validate_component_coverage( - component_coverage: dict[str, Any], - html_dir: Path, - html_text: str, - html_index: HtmlIndex, - visual_spec_ids: set[str], - details: dict[str, Any], - blocker_codes: list[str], -) -> None: - components = component_coverage.get("components", []) - missing_preview_refs: list[str] = [] - missing_interaction_refs: list[str] = [] - missing_visual_spec_refs: list[str] = [] - missing_component_screenshots: list[str] = [] - missing_records: list[str] = [] - asset_or_resource_gaps: list[str] = [] - supporting_source_refs: list[dict[str, Any]] = [] - covered_count = 0 - missing_count = 0 - - for component in components if isinstance(components, list) else []: - if not isinstance(component, dict): - continue - component_id = str(component.get("id") or "") - component_helper_ref = supporting_visual_artifact_refs([component.get("source_ref")]) - if component_helper_ref: - supporting_source_refs.append({"id": component_id, "refs": component_helper_ref}) - covered = component.get("covered", []) - missing = component.get("missing", []) - if isinstance(covered, list): - covered_count += len(covered) - for record in covered: - if not isinstance(record, dict): - continue - preview_ref = str(record.get("preview_ref") or "") - interaction_ref = str(record.get("interaction_ref") or "") - visual_spec_ref = str(record.get("visual_spec_ref") or "") - record_helper_refs = supporting_visual_artifact_refs([record.get("source_ref")]) - if record_helper_refs: - supporting_source_refs.append({"id": component_id, "refs": record_helper_refs}) - if not visual_spec_ref or not visual_spec_ref_exists(visual_spec_ref, visual_spec_ids): - missing_visual_spec_refs.append(component_id) - if not preview_ref or not preview_ref_in_html(preview_ref, html_text): - missing_preview_refs.append(preview_ref or component_id) - elif not preview_ref_is_visualized_component(preview_ref, html_text): - missing_preview_refs.append(preview_ref or component_id) - if not interaction_ref or not preview_ref_in_html(interaction_ref, html_text): - missing_interaction_refs.append(interaction_ref or component_id) - screenshot_refs = record.get("screenshot_refs", []) - for screenshot_ref in screenshot_refs if isinstance(screenshot_refs, list) else []: - screenshot_text = str(screenshot_ref) - if not screenshot_ref_exists(html_dir, screenshot_text): - missing_component_screenshots.append(screenshot_text or component_id) - if isinstance(missing, list): - missing_count += len(missing) - for record in missing: - if not isinstance(record, dict): - continue - missing_records.append(component_id) - missing_type = str(record.get("missing_type") or "") - if missing_type in {"asset", "resource", "token"}: - asset_or_resource_gaps.append(component_id) - - blockers = component_coverage.get("blockers") - details["component_coverage"] = { - "component_count": len(components) if isinstance(components, list) else 0, - "covered_count": covered_count, - "missing_count": missing_count, - "missing_preview_refs": missing_preview_refs, - "missing_interaction_refs": missing_interaction_refs, - "missing_visual_spec_refs": missing_visual_spec_refs, - "missing_component_screenshots": missing_component_screenshots, - "missing_records": missing_records, - "asset_or_resource_gaps": asset_or_resource_gaps, - "supporting_artifact_source_refs": supporting_source_refs, - "ready_gate": component_coverage.get("ready_gate"), - "blockers": blockers, - } - if not components or missing_preview_refs: - blocker_codes.append(BLOCKERS["FIGMA_NODE_COVERAGE_INCOMPLETE"]) - if missing_interaction_refs: - blocker_codes.append(BLOCKERS["IA_MATRIX_INCOMPLETE"]) - if missing_records or missing_visual_spec_refs: - blocker_codes.append(BLOCKERS["COMPONENT_STATE_COVERAGE_INCOMPLETE"]) - if missing_component_screenshots: - blocker_codes.append(BLOCKERS["VIEWPORT_CAPTURE_INCOMPLETE"]) - if asset_or_resource_gaps or supporting_source_refs: - blocker_codes.append(BLOCKERS["ASSET_TRACEABILITY_INCOMPLETE"]) - if component_coverage.get("ready_gate") != "PASS" or (isinstance(blockers, list) and blockers): - blocker_codes.append(BLOCKERS["KNOWN_GAP_UNRESOLVED"]) - - -def validate_viewport_coverage( - html_dir: Path, - viewport_coverage: dict[str, Any], - html_text: str, - visual_spec_ids: set[str], - details: dict[str, Any], - blocker_codes: list[str], -) -> None: - viewports = viewport_coverage.get("viewports", []) - missing_screenshots: list[str] = [] - missing_page_refs: list[str] = [] - uncovered_viewports: list[str] = [] - visual_diff_blocked: list[str] = [] - missing_visual_spec_refs: list[str] = [] - supporting_source_refs: list[dict[str, Any]] = [] - page_refs = 0 - - for viewport in viewports if isinstance(viewports, list) else []: - if not isinstance(viewport, dict): - continue - viewport_id = str(viewport.get("id") or "") - if viewport.get("covered") is not True: - uncovered_viewports.append(viewport_id) - helper_refs = supporting_visual_artifact_refs(viewport.get("source_refs")) - if helper_refs: - supporting_source_refs.append({"id": viewport_id, "refs": helper_refs}) - refs = viewport.get("screenshot_refs", []) - if not refs: - missing_screenshots.append(viewport_id) - for ref in refs if isinstance(refs, list) else []: - ref_path = str(ref) - if ref_path and not screenshot_ref_exists(html_dir, ref_path): - missing_screenshots.append(ref_path) - page_refs += len(viewport.get("page_refs", []) or []) - for ref in viewport.get("visual_spec_refs", []) or []: - visual_spec_ref = str(ref) - if not visual_spec_ref_exists(visual_spec_ref, visual_spec_ids): - missing_visual_spec_refs.append(visual_spec_ref or viewport_id) - for ref in viewport.get("page_refs", []) or []: - page_ref = str(ref) - if page_ref and ( - not preview_ref_in_html(page_ref, html_text) - or not preview_ref_has_attr(page_ref, html_text, "data-preview-section", "mock-page") - ): - missing_page_refs.append(page_ref) - if viewport.get("visual_diff_status") == "blocked": - visual_diff_blocked.append(viewport_id) - - blockers = viewport_coverage.get("blockers") - details["viewport_coverage"] = { - "viewport_count": len(viewports) if isinstance(viewports, list) else 0, - "missing_screenshots": missing_screenshots, - "uncovered_viewports": uncovered_viewports, - "visual_diff_blocked": visual_diff_blocked, - "missing_visual_spec_refs": missing_visual_spec_refs, - "page_ref_count": page_refs, - "missing_page_refs": missing_page_refs, - "supporting_artifact_source_refs": supporting_source_refs, - "ready_gate": viewport_coverage.get("ready_gate"), - "blockers": blockers, - } - if not viewports or uncovered_viewports or missing_screenshots: - blocker_codes.append(BLOCKERS["VIEWPORT_CAPTURE_INCOMPLETE"]) - if page_refs == 0 or missing_page_refs: - blocker_codes.append(BLOCKERS["PAGE_COVERAGE_INCOMPLETE"]) - if missing_visual_spec_refs: - blocker_codes.append(BLOCKERS["PAGE_COVERAGE_INCOMPLETE"]) - if visual_diff_blocked: - blocker_codes.append(BLOCKERS["VISUAL_DIFF_BLOCKED"]) - if supporting_source_refs: - blocker_codes.append(BLOCKERS["ASSET_TRACEABILITY_INCOMPLETE"]) - if viewport_coverage.get("ready_gate") != "PASS" or (isinstance(blockers, list) and blockers): - blocker_codes.append(BLOCKERS["KNOWN_GAP_UNRESOLVED"]) - - -def validate_known_gaps( - known_gaps_path: Path, - details: dict[str, Any], - blocker_codes: list[str], -) -> None: - if not known_gaps_path.exists(): - return - text = known_gaps_path.read_text(encoding="utf-8", errors="replace") - unresolved = bool(re.search(r"\b(UNRESOLVED|BLOCKED|TODO)\b", text, re.IGNORECASE)) - details["known_gaps"] = {"unresolved": unresolved} - if unresolved: - blocker_codes.append(BLOCKERS["KNOWN_GAP_UNRESOLVED"]) - - -def emit(json_mode: bool, details: dict[str, Any], blockers: list[str], warnings: list[str]) -> int: - result = { - "status": "BLOCKED" if blockers else "PASS", - "blockers": blockers, - "warnings": warnings, - "details": details, - } - if json_mode: - print(json.dumps(result, indent=2, sort_keys=True)) - else: - print(f"HTML mock readiness: {result['status']}") - if blockers: - print("Blockers:") - for blocker in blockers: - print(f"- {blocker}") - if warnings: - print("Warnings:") - for warning in warnings: - print(f"- {warning}") - return 1 if blockers else 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/extensions/intake/scripts/python/validate_visual_spec_package.py b/extensions/intake/scripts/python/validate_visual_spec_package.py deleted file mode 100644 index ee53088f10..0000000000 --- a/extensions/intake/scripts/python/validate_visual_spec_package.py +++ /dev/null @@ -1,528 +0,0 @@ -#!/usr/bin/env python3 -"""Validate Spec Kit visual requirements/spec structured asset package intake artifacts.""" - -from __future__ import annotations - -import argparse -import json -import re -import subprocess -import sys -from pathlib import Path -from typing import Any - -from intake_validator_common import ( - emit, - is_truthy, - load_yaml, - non_empty, - parse_evidence_packet_status, - supporting_visual_artifact_refs, - validate_json_schema, -) - - -BLOCKERS = { - "SOURCE_INTAKE_BLOCKED": "VISUAL_SPEC_SOURCE_INTAKE_BLOCKED", - "REQUIRED_ARTIFACT_MISSING": "VISUAL_SPEC_REQUIRED_ARTIFACT_MISSING", - "SCHEMA_INVALID": "VISUAL_SPEC_SCHEMA_INVALID", - "INTAKE_INCOMPLETE": "VISUAL_SPEC_INTAKE_INCOMPLETE", - "PROVIDER_EVIDENCE_MISSING": "VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING", - "PRODUCT_AMBIGUITY_UNRESOLVED": "VISUAL_SPEC_PRODUCT_AMBIGUITY_UNRESOLVED", - "ASSERTION_COVERAGE_INCOMPLETE": "VISUAL_SPEC_ASSERTION_COVERAGE_INCOMPLETE", - "LOCATOR_STRATEGY_INVALID": "VISUAL_SPEC_LOCATOR_STRATEGY_INVALID", - "DOWNSTREAM_OWNERSHIP_LEAK": "VISUAL_SPEC_DOWNSTREAM_OWNERSHIP_LEAK", - "READY_WITHOUT_EVIDENCE": "VISUAL_SPEC_READY_WITHOUT_EVIDENCE", -} - -FORBIDDEN_FIELD_NAMES = { - "requirement_id", - "requirement_ids", - "task_id", - "task_ids", - "implementation_task", - "implementation_tasks", - "code_component", - "code_components", - "component_name", - "component_names", - "css_selector", - "xpath", - "selector", -} -FORBIDDEN_LOCATOR_PATTERNS = [ - re.compile(r"^\s*[.#][A-Za-z0-9_-]+"), - re.compile(r"^\s*//"), - re.compile(r"^\s*/html\b"), - re.compile(r"\b(css|xpath|querySelector)\b", re.IGNORECASE), -] -PRODUCT_AMBIGUITY_MARKERS = { - "PRODUCT_AMBIGUITY", - "PRODUCT_AMBIGUITY_UNRESOLVED", - "VISUAL_SPEC_PRODUCT_AMBIGUITY_UNRESOLVED", -} -PROVIDER_EVIDENCE_MARKERS = { - "MISSING_PROVIDER_EVIDENCE", - "PROVIDER_EVIDENCE_MISSING", - "VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING", -} -LOCATOR_MARKERS = {"LOCATOR_STRATEGY_INVALID", "VISUAL_SPEC_LOCATOR_STRATEGY_INVALID"} -OWNERSHIP_MARKERS = {"DOWNSTREAM_OWNERSHIP_LEAK", "VISUAL_SPEC_DOWNSTREAM_OWNERSHIP_LEAK"} - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("package_dir", help="Directory containing visual spec package artifacts") - parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON") - args = parser.parse_args() - - package_dir = Path(args.package_dir) - blocker_codes: list[str] = [] - warnings: list[str] = [] - details: dict[str, Any] = {"package_dir": str(package_dir)} - - if not package_dir.exists() or not package_dir.is_dir(): - blocker_codes.extend( - [ - BLOCKERS["SOURCE_INTAKE_BLOCKED"], - BLOCKERS["REQUIRED_ARTIFACT_MISSING"], - BLOCKERS["READY_WITHOUT_EVIDENCE"], - ] - ) - return emit( - label="Visual Spec Package", - json_mode=args.json, - details=details, - blockers=blocker_codes, - warnings=warnings, - ) - - validate_source_intake(package_dir, details, blocker_codes) - package_doc = validate_visual_spec_package(package_dir, details, blocker_codes) - assertions_doc = validate_visual_spec_assertions(package_dir, details, blocker_codes) - validate_cross_references(package_doc, assertions_doc, details, blocker_codes) - validate_visual_spec_evidence_packet(package_dir, details, blocker_codes, warnings) - - return emit( - label="Visual Spec Package", - json_mode=args.json, - details=details, - blockers=blocker_codes, - warnings=warnings, - ) - - -def validate_source_intake( - package_dir: Path, - details: dict[str, Any], - blocker_codes: list[str], -) -> None: - upstream = package_dir.parent - validator = Path(__file__).resolve().with_name("validate_visual_design_intake.py") - result = subprocess.run( - [sys.executable, str(validator), str(upstream), "--json"], - text=True, - capture_output=True, - ) - try: - payload = json.loads(result.stdout or "{}") - except json.JSONDecodeError: - payload = {} - details["source_intake"] = { - "path": str(upstream), - "validator": str(validator), - "status": payload.get("status"), - "blockers": payload.get("blockers"), - "errors": payload.get("details", {}).get("evidence_packet_metadata", {}).get("errors"), - } - if result.returncode != 0 or payload.get("status") != "PASS": - blocker_codes.append(BLOCKERS["SOURCE_INTAKE_BLOCKED"]) - - -def validate_visual_spec_package( - package_dir: Path, - details: dict[str, Any], - blocker_codes: list[str], -) -> dict[str, Any]: - package_path = package_dir / "visual-spec.yaml" - if not package_path.exists(): - blocker_codes.append(BLOCKERS["REQUIRED_ARTIFACT_MISSING"]) - details["visual_spec_package"] = {"missing": True} - return {} - - validate_json_schema( - instance_path=package_path, - schema_name="visual-spec-package.schema.json", - details_key="visual_spec_package", - details=details, - blocker_codes=blocker_codes, - schema_error_code=BLOCKERS["SCHEMA_INVALID"], - ) - - package_doc = load_yaml(package_path) - items = package_doc.get("items") - if not isinstance(items, list): - items = [] - - declared_count = parse_count(package_doc.get("visual_spec_item_count"), len(items)) - item_errors: list[dict[str, Any]] = [] - invalid_locators: list[str] = [] - ownership_leaks: list[str] = [] - provider_evidence_gaps: list[str] = [] - product_ambiguity_gaps: list[str] = [] - blocker_lint_items: list[str] = [] - supporting_source_refs: list[dict[str, Any]] = [] - has_ready_item = False - - details["visual_spec_package"] = { - "visual_spec_package_complete": package_doc.get("visual_spec_package_complete"), - "source_refs_complete": package_doc.get("source_refs_complete"), - "provider_evidence_complete": package_doc.get("provider_evidence_complete"), - "product_ambiguities_recorded": package_doc.get("product_ambiguities_recorded"), - "resources_traceable_to_design_source": package_doc.get("resources_traceable_to_design_source"), - "downstream_ownership_free": package_doc.get("downstream_ownership_free"), - "visual_spec_item_count": declared_count, - "actual_item_count": len(items), - "blocker_lint_errors": package_doc.get("blocker_lint_errors"), - } - - if not is_truthy(package_doc.get("visual_spec_package_complete")) or declared_count <= 0: - blocker_codes.append(BLOCKERS["INTAKE_INCOMPLETE"]) - if declared_count != len(items): - blocker_codes.append(BLOCKERS["INTAKE_INCOMPLETE"]) - if not is_truthy(package_doc.get("source_refs_complete")): - blocker_codes.append(BLOCKERS["PROVIDER_EVIDENCE_MISSING"]) - if not is_truthy(package_doc.get("provider_evidence_complete")): - blocker_codes.append(BLOCKERS["PROVIDER_EVIDENCE_MISSING"]) - if not is_truthy(package_doc.get("resources_traceable_to_design_source")): - blocker_codes.append(BLOCKERS["PROVIDER_EVIDENCE_MISSING"]) - if not is_truthy(package_doc.get("product_ambiguities_recorded")): - blocker_codes.append(BLOCKERS["PRODUCT_AMBIGUITY_UNRESOLVED"]) - if not is_truthy(package_doc.get("downstream_ownership_free")): - blocker_codes.append(BLOCKERS["DOWNSTREAM_OWNERSHIP_LEAK"]) - if non_empty(package_doc.get("product_ambiguities")): - blocker_codes.append(BLOCKERS["PRODUCT_AMBIGUITY_UNRESOLVED"]) - if non_empty(package_doc.get("blocker_lint_errors")): - blocker_codes.append(BLOCKERS["INTAKE_INCOMPLETE"]) - - for index, item in enumerate(items): - if not isinstance(item, dict): - item_errors.append({"index": index, "error": "item must be a mapping"}) - continue - - item_id = str(item.get("id") or f"item-{index}") - if item.get("status") == "ready": - has_ready_item = True - - required_non_empty = [ - "id", - "source_refs", - "page", - "region", - "role", - "state", - "viewport", - "locator", - "expectations", - "acceptance_intent", - "evidence_type", - "confidence", - "status", - ] - missing = [ - field - for field in required_non_empty - if not non_empty(item.get(field)) - ] - if "blockers" not in item: - missing.append("blockers") - if missing: - item_errors.append({"id": item_id, "missing_fields": missing}) - - helper_refs = supporting_visual_artifact_refs(item.get("source_refs")) - if not valid_source_refs(item.get("source_refs")) or helper_refs: - provider_evidence_gaps.append(item_id) - if helper_refs: - supporting_source_refs.append({"id": item_id, "refs": helper_refs}) - - evidence_type = str(item.get("evidence_type") or "") - if evidence_type in {"missing", "unsupported"} or non_empty(item.get("missing_evidence")): - provider_evidence_gaps.append(item_id) - - blockers = as_string_set(item.get("blockers")) - if blockers: - blocker_lint_items.append(item_id) - if blockers & PROVIDER_EVIDENCE_MARKERS: - provider_evidence_gaps.append(item_id) - if blockers & PRODUCT_AMBIGUITY_MARKERS or non_empty(item.get("ambiguity_refs")): - product_ambiguity_gaps.append(item_id) - if blockers & LOCATOR_MARKERS: - invalid_locators.append(item_id) - if blockers & OWNERSHIP_MARKERS: - ownership_leaks.append(item_id) - - locator = item.get("locator") - if not locator_is_valid(locator): - invalid_locators.append(item_id) - if has_downstream_ownership_leak(item): - ownership_leaks.append(item_id) - - details["visual_spec_package"]["item_errors"] = item_errors - details["visual_spec_package"]["provider_evidence_gaps"] = sorted(set(provider_evidence_gaps)) - details["visual_spec_package"]["product_ambiguity_gaps"] = sorted(set(product_ambiguity_gaps)) - details["visual_spec_package"]["invalid_locators"] = sorted(set(invalid_locators)) - details["visual_spec_package"]["ownership_leaks"] = sorted(set(ownership_leaks)) - details["visual_spec_package"]["blocker_lint_items"] = sorted(set(blocker_lint_items)) - details["visual_spec_package"]["supporting_artifact_source_refs"] = supporting_source_refs - - if item_errors or blocker_lint_items or not has_ready_item: - blocker_codes.append(BLOCKERS["INTAKE_INCOMPLETE"]) - if provider_evidence_gaps: - blocker_codes.append(BLOCKERS["PROVIDER_EVIDENCE_MISSING"]) - if product_ambiguity_gaps: - blocker_codes.append(BLOCKERS["PRODUCT_AMBIGUITY_UNRESOLVED"]) - if invalid_locators: - blocker_codes.append(BLOCKERS["LOCATOR_STRATEGY_INVALID"]) - if ownership_leaks: - blocker_codes.append(BLOCKERS["DOWNSTREAM_OWNERSHIP_LEAK"]) - - return package_doc - - -def validate_visual_spec_assertions( - package_dir: Path, - details: dict[str, Any], - blocker_codes: list[str], -) -> dict[str, Any]: - assertions_path = package_dir / "visual-spec-assertions.yaml" - if not assertions_path.exists(): - blocker_codes.append(BLOCKERS["REQUIRED_ARTIFACT_MISSING"]) - details["visual_spec_assertions"] = {"missing": True} - return {} - - validate_json_schema( - instance_path=assertions_path, - schema_name="visual-spec-assertions.schema.json", - details_key="visual_spec_assertions", - details=details, - blocker_codes=blocker_codes, - schema_error_code=BLOCKERS["SCHEMA_INVALID"], - ) - - assertions_doc = load_yaml(assertions_path) - assertions = assertions_doc.get("assertions") - if not isinstance(assertions, list): - assertions = [] - - declared_count = parse_count(assertions_doc.get("assertion_count"), len(assertions)) - assertion_errors: list[dict[str, Any]] = [] - non_ci_assertions: list[str] = [] - product_ambiguity_gaps: list[str] = [] - provider_evidence_gaps: list[str] = [] - blocker_lint_assertions: list[str] = [] - supporting_evidence_refs: list[dict[str, Any]] = [] - ready_ci_count = 0 - - details["visual_spec_assertions"] = { - "assertions_complete": assertions_doc.get("assertions_complete"), - "ci_assertions_complete": assertions_doc.get("ci_assertions_complete"), - "assertion_count": declared_count, - "actual_assertion_count": len(assertions), - "blocker_lint_errors": assertions_doc.get("blocker_lint_errors"), - } - - if not is_truthy(assertions_doc.get("assertions_complete")) or declared_count <= 0: - blocker_codes.append(BLOCKERS["ASSERTION_COVERAGE_INCOMPLETE"]) - if not is_truthy(assertions_doc.get("ci_assertions_complete")): - blocker_codes.append(BLOCKERS["ASSERTION_COVERAGE_INCOMPLETE"]) - if declared_count != len(assertions): - blocker_codes.append(BLOCKERS["ASSERTION_COVERAGE_INCOMPLETE"]) - if non_empty(assertions_doc.get("blocker_lint_errors")): - blocker_codes.append(BLOCKERS["ASSERTION_COVERAGE_INCOMPLETE"]) - - for index, assertion in enumerate(assertions): - if not isinstance(assertion, dict): - assertion_errors.append({"index": index, "error": "assertion must be a mapping"}) - continue - - assertion_id = str(assertion.get("id") or f"assertion-{index}") - required_non_empty = [ - "id", - "visual_spec_refs", - "assertion_type", - "acceptance_intent", - "expected", - "evidence_refs", - "ci_suitability", - "status", - ] - missing = [ - field - for field in required_non_empty - if not non_empty(assertion.get(field)) - ] - if "blockers" not in assertion: - missing.append("blockers") - if missing: - assertion_errors.append({"id": assertion_id, "missing_fields": missing}) - - status = str(assertion.get("status") or "") - ci_suitability = str(assertion.get("ci_suitability") or "") - if status == "ready" and ci_suitability == "ci_low_cost": - ready_ci_count += 1 - elif status == "ready": - non_ci_assertions.append(assertion_id) - - helper_refs = supporting_visual_artifact_refs(assertion.get("evidence_refs")) - if not valid_source_refs(assertion.get("evidence_refs")) or helper_refs: - provider_evidence_gaps.append(assertion_id) - if helper_refs: - supporting_evidence_refs.append({"id": assertion_id, "refs": helper_refs}) - - blockers = as_string_set(assertion.get("blockers")) - if blockers: - blocker_lint_assertions.append(assertion_id) - if blockers & PRODUCT_AMBIGUITY_MARKERS: - product_ambiguity_gaps.append(assertion_id) - if blockers & PROVIDER_EVIDENCE_MARKERS: - provider_evidence_gaps.append(assertion_id) - if blockers & LOCATOR_MARKERS: - blocker_codes.append(BLOCKERS["LOCATOR_STRATEGY_INVALID"]) - if blockers & OWNERSHIP_MARKERS: - blocker_codes.append(BLOCKERS["DOWNSTREAM_OWNERSHIP_LEAK"]) - if has_downstream_ownership_leak(assertion): - blocker_codes.append(BLOCKERS["DOWNSTREAM_OWNERSHIP_LEAK"]) - - details["visual_spec_assertions"]["assertion_errors"] = assertion_errors - details["visual_spec_assertions"]["ready_ci_assertion_count"] = ready_ci_count - details["visual_spec_assertions"]["non_ci_ready_assertions"] = non_ci_assertions - details["visual_spec_assertions"]["provider_evidence_gaps"] = sorted(set(provider_evidence_gaps)) - details["visual_spec_assertions"]["product_ambiguity_gaps"] = sorted(set(product_ambiguity_gaps)) - details["visual_spec_assertions"]["blocker_lint_assertions"] = sorted(set(blocker_lint_assertions)) - details["visual_spec_assertions"]["supporting_artifact_evidence_refs"] = supporting_evidence_refs - - if assertion_errors or blocker_lint_assertions or non_ci_assertions or ready_ci_count == 0: - blocker_codes.append(BLOCKERS["ASSERTION_COVERAGE_INCOMPLETE"]) - if provider_evidence_gaps: - blocker_codes.append(BLOCKERS["PROVIDER_EVIDENCE_MISSING"]) - if product_ambiguity_gaps: - blocker_codes.append(BLOCKERS["PRODUCT_AMBIGUITY_UNRESOLVED"]) - - return assertions_doc - - -def validate_cross_references( - package_doc: dict[str, Any], - assertions_doc: dict[str, Any], - details: dict[str, Any], - blocker_codes: list[str], -) -> None: - items = package_doc.get("items") - assertions = assertions_doc.get("assertions") - if not isinstance(items, list) or not isinstance(assertions, list): - return - - visual_spec_ids = {item.get("id") for item in items if isinstance(item, dict)} - missing_refs: list[str] = [] - for assertion in assertions: - if not isinstance(assertion, dict): - continue - for visual_spec_ref in assertion.get("visual_spec_refs") or []: - if visual_spec_ref not in visual_spec_ids: - missing_refs.append(str(visual_spec_ref)) - - details["visual_spec_cross_refs"] = { - "visual_spec_item_ids": sorted(str(item_id) for item_id in visual_spec_ids if item_id), - "missing_visual_spec_refs": sorted(set(missing_refs)), - } - if missing_refs: - blocker_codes.append(BLOCKERS["ASSERTION_COVERAGE_INCOMPLETE"]) - - -def validate_visual_spec_evidence_packet( - package_dir: Path, - details: dict[str, Any], - blocker_codes: list[str], - warnings: list[str], -) -> None: - evidence_path = package_dir / "visual-spec-evidence-packet.md" - if not evidence_path.exists(): - blocker_codes.append(BLOCKERS["REQUIRED_ARTIFACT_MISSING"]) - return - - details["visual_spec_evidence_packet"] = evidence_path.name - packet_status = parse_evidence_packet_status( - evidence_path.read_text(encoding="utf-8", errors="replace") - ) - details["visual_spec_evidence_packet_metadata"] = packet_status["metadata"] - if packet_status["warnings"]: - warnings.extend(packet_status["warnings"]) - if packet_status["errors"]: - blocker_codes.append(BLOCKERS["READY_WITHOUT_EVIDENCE"]) - return - - packet_blockers = packet_status["metadata"].get("blockers") - has_packet_blockers = isinstance(packet_blockers, list) and bool(packet_blockers) - if packet_status["ready_gate"] != "PASS" or has_packet_blockers: - blocker_codes.append(BLOCKERS["READY_WITHOUT_EVIDENCE"]) - return - - current_blockers = [code for code in blocker_codes if code != BLOCKERS["READY_WITHOUT_EVIDENCE"]] - if current_blockers: - blocker_codes.append(BLOCKERS["READY_WITHOUT_EVIDENCE"]) - - -def parse_count(value: Any, fallback: int) -> int: - try: - return int(value) - except (TypeError, ValueError): - return fallback - - -def valid_source_refs(value: Any) -> bool: - return isinstance(value, list) and bool(value) and all(str(ref).strip() for ref in value) - - -def as_string_set(value: Any) -> set[str]: - if not isinstance(value, list): - return set() - return {str(item).strip() for item in value if str(item).strip()} - - -def locator_is_valid(locator: Any) -> bool: - if not isinstance(locator, dict): - return False - if locator.get("implementation_owned") is not False: - return False - strategy = str(locator.get("strategy") or "") - if strategy in {"css", "xpath", "query-selector", "implementation-selector"}: - return False - value = str(locator.get("value") or "") - if not value: - return False - return not any(pattern.search(value) for pattern in FORBIDDEN_LOCATOR_PATTERNS) - - -def has_downstream_ownership_leak(value: Any) -> bool: - if isinstance(value, dict): - for key, nested in value.items(): - normalized = str(key).strip().lower().replace("-", "_") - if normalized in FORBIDDEN_FIELD_NAMES: - return True - if normalized == "id" and isinstance(nested, str) and looks_like_downstream_id(nested): - return True - if has_downstream_ownership_leak(nested): - return True - elif isinstance(value, list): - return any(has_downstream_ownership_leak(item) for item in value) - elif isinstance(value, str): - return bool(re.search(r"\b(implementation task|code component|css selector|xpath)\b", value, re.IGNORECASE)) - return False - - -def looks_like_downstream_id(value: str) -> bool: - return bool(re.match(r"^(REQ|FR|NFR|TASK|US|AC)-\d+", value.strip(), re.IGNORECASE)) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/extensions/intake/templates/intake-static-html-delivery-contract.md b/extensions/intake/templates/intake-static-html-delivery-contract.md new file mode 100644 index 0000000000..50b139c814 --- /dev/null +++ b/extensions/intake/templates/intake-static-html-delivery-contract.md @@ -0,0 +1,244 @@ +# Static HTML Delivery Contract + +Required final static HTML delivery artifacts and readiness gates for visual intake. This contract replaces the previous preview/mock interpretation: `delivery/index.html` is the final generated static HTML artifact for the submitted design source. + +The delivery bundle must restore the design source as source-backed, offline-runnable HTML. It must render the visual design and replay user operations that are present in the source evidence or confirmed by the user. Missing source evidence must remain explicit through blockers and clarification questions. + +## Artifact Family + +Default directory: + +```text +specs//intake/visual-design/delivery/ +``` + +Required files: + +- `index.html` +- `assets/` +- `screenshots/` +- `render-replay-report.yaml` +- `evidence-packet.md` + +Required adjacent visual IR files: + +- `../visual-ir/asset-inventory.yaml` +- `../visual-ir/layout-tree.yaml` +- `../visual-ir/component-model.yaml` +- `../visual-ir/page-route-model.yaml` +- `../visual-ir/interaction-model.yaml` +- `../visual-ir/motion-anchor-model.yaml` +- `../visual-ir/clarification-log.yaml` + +## Source Boundary + +Static HTML delivery is downstream of visual-design source intake and visual IR: + +1. Source intake preserves original design sources, provider metadata, checksums, source refs, and visual requirements. +2. Visual IR records assets, layout, boxes, components, pages, routes, states, interactions, motion anchors, and clarification decisions. +3. Static HTML delivery renders the IR into offline HTML and validates operation replay, viewport screenshots, and visual diff evidence. + +The delivery bundle must not become a source-of-truth substitute for missing design evidence. When source evidence is incomplete, the delivery must render a visible blocked or unresolved surface and emit a `STATIC_HTML_*` blocker rather than inventing behavior. + +## `index.html` + +`index.html` must be static and offline-runnable. It may include embedded CSS and vanilla JavaScript for replaying source-backed or user-confirmed states, routes, interactions, and motion anchors. + +Required HTML anchors: + +- `data-delivery-root` +- `data-visual-id` +- `data-page-id` +- `data-route-id` +- `data-component-id` +- `data-state-id` +- `data-operation-id` +- `data-motion-id` when motion records exist + +Every page, route state, component state, interaction target, operation result, and motion surface referenced by `render-replay-report.yaml` must resolve to an element in `index.html` with the matching anchor namespace. Page refs resolve through `id`, `data-page-id`, or `data-route-id`; component refs resolve through `id`, `data-component-id`, or `data-state-id`; operation targets resolve through `id`, `data-operation-id`, or `data-component-id`; motion refs resolve through `id`, `data-motion-id`, `data-state-id`, or `data-component-id`; visual asset refs resolve through `id` or `data-visual-id`. + +The HTML must render actual UI surfaces first. Evidence tables, reports, or summaries may appear after rendered UI surfaces, but they must not replace rendered pages, components, states, or interaction surfaces. + +## Visual IR Expectations + +`asset-inventory.yaml` must record: + +- `ready_gate` +- `blockers` +- `assets` +- each asset id, source refs, local path when available, role, media type, hash status, usage refs, and replacement/blocker status + +`layout-tree.yaml` must record: + +- `ready_gate` +- `blockers` +- pages/frames, regions, boxes, coordinates, z-order, scroll/overflow, responsive constraints, and source refs + +`component-model.yaml` must record: + +- `ready_gate` +- `blockers` +- components, instances, variants, props, visual states, state source refs, accessibility expectations, and missing states + +`page-route-model.yaml` must record: + +- `ready_gate` +- `blockers` +- pages, routes, IA regions, route states, overlays, navigation targets, responsive viewports, and missing route behavior + +`interaction-model.yaml` must record: + +- `ready_gate` +- `blockers` +- operations, event targets, user events, preconditions, feedback, state changes, route changes, exception branches, and replay steps + +`motion-anchor-model.yaml` must record: + +- `ready_gate` +- `blockers` +- motion anchors, triggers, affected surfaces, initial/end states, duration, easing, delay, repeat behavior, and source refs + +`clarification-log.yaml` must record: + +- `ready_gate` +- `blockers` +- questions +- each question id, target artifact, blocked delivery surface, blocker code, allowed answer shape, status, answer, source/user confirmation refs, and whether it is required for HTML readiness +- `blocker_code` must be a `STATIC_HTML_*` blocker code, `status` must be `unanswered|answered|out_of_scope`, answered required questions must include `answer` and `confirmed_by_user: true`, and report clarification counts must match this log. + +## `render-replay-report.yaml` + +`render-replay-report.yaml` is the machine-readable readiness record for static HTML delivery. + +Required top-level fields: + +- `ready_gate: PASS|BLOCKED` +- `blockers` +- `html_entry` +- `source_intake_ref` +- `visual_ir_refs` +- `assets` +- `pages` +- `components` +- `operations` +- `motion_anchors` +- `viewports` +- `visual_diffs` +- `clarifications` + +Each asset record must include: + +- `id` +- `inventory_ref` +- `html_refs` +- `local_paths` +- `source_refs` +- `status: covered|blocked|out_of_scope` +- `blockers` + +Each page record must include: + +- `id` +- `route` +- `html_ref` +- `state_refs` +- `layout_refs` +- `source_refs` +- `status` +- `blockers` + +Each component record must include: + +- `id` +- `component_model_ref` +- `html_ref` +- `state_refs` +- `operation_refs` +- `source_refs` +- `status` +- `blockers` + +Each operation record must include: + +- `id` +- `interaction_model_ref` +- `event` +- `target_ref` +- `result_ref` +- `precondition` +- `replay_status: pass|blocked|not_applicable` +- `source_refs` +- `blockers` + +Each motion record must include: + +- `id` +- `motion_model_ref` +- `trigger_ref` +- `affected_ref` +- `end_state_ref` +- `replay_status: pass|blocked|not_applicable` +- `source_refs` +- `blockers` + +Each viewport record must include: + +- `id` +- `width` +- `height` +- `page_refs` +- `screenshot_refs` +- `render_status: pass|blocked` +- `blockers` + +Each visual diff record must include: + +- `id` +- `source_ref` +- `screenshot_ref` +- `status: pass|blocked` +- `thresholds` +- `diff_summary` +- `blockers` + +Clarification summary must include: + +- `required_question_count` +- `answered_required_question_count` +- `unanswered_required_question_ids` + +## Readiness + +Static HTML delivery is ready only when: + +- upstream visual-design intake readiness is `PASS` +- every required visual IR artifact exists +- every visual IR artifact has `ready_gate: PASS` +- `visual_ir_refs` exactly matches the required visual IR file set, `source_intake_ref` resolves to upstream intake evidence, and every `inventory_ref`, `layout_ref`, `state_ref`, `component_model_ref`, `interaction_model_ref`, and `motion_model_ref` resolves to a real visual IR record +- all required clarification questions are answered or explicitly out of scope +- `index.html` exists and contains `data-delivery-root` +- every `html_ref`, `target_ref`, `result_ref`, `trigger_ref`, `affected_ref`, and `page_ref` resolves inside `index.html` with the expected anchor namespace +- all local asset paths resolve under `delivery/assets/` +- every required component, component state, page state, route state, operation, motion anchor, and viewport is covered or blocked explicitly +- every required operation has `replay_status: pass` +- every source-backed motion anchor has `replay_status: pass` or is explicitly out of scope +- every required viewport has an existing screenshot +- visual diff records are `pass`; unavailable source comparison must be recorded as `STATIC_HTML_VISUAL_DIFF_BLOCKED` and keeps readiness blocked +- `evidence-packet.md` front matter reports `ready_gate: PASS` with no blockers + +## Blocker Codes + +- `STATIC_HTML_SOURCE_INTAKE_BLOCKED` +- `STATIC_HTML_REQUIRED_ARTIFACT_MISSING` +- `STATIC_HTML_SCHEMA_INVALID` +- `STATIC_HTML_IR_BLOCKED` +- `STATIC_HTML_CLARIFICATION_REQUIRED` +- `STATIC_HTML_ASSET_INCOMPLETE` +- `STATIC_HTML_LAYOUT_INCOMPLETE` +- `STATIC_HTML_COMPONENT_STATE_INCOMPLETE` +- `STATIC_HTML_PAGE_ROUTE_INCOMPLETE` +- `STATIC_HTML_OPERATION_REPLAY_INCOMPLETE` +- `STATIC_HTML_MOTION_ANCHOR_INCOMPLETE` +- `STATIC_HTML_VIEWPORT_CAPTURE_INCOMPLETE` +- `STATIC_HTML_VISUAL_DIFF_BLOCKED` +- `STATIC_HTML_READY_WITHOUT_EVIDENCE` diff --git a/extensions/intake/templates/intake-visual-design-contract.md b/extensions/intake/templates/intake-visual-design-contract.md index 5e1d3b9bbc..f9e3ac757e 100644 --- a/extensions/intake/templates/intake-visual-design-contract.md +++ b/extensions/intake/templates/intake-visual-design-contract.md @@ -1,14 +1,44 @@ -# Visual Design Intake Contract +# Visual Design Intake Contract -Required visual design intake artifacts and readiness gates. Runtime agents or external intake tools extract high-confidence, traceable, provider-neutral structured UI/visual assets before downstream SDD workflows project evidence into requirements or visual verification criteria. +Required visual design intake artifacts and readiness gates. The visual intake workflow captures design evidence, derives visual IR, asks the user to confirm missing operation semantics, and produces a final static HTML delivery. -Intake does not generate requirements. The structured UI/visual asset is the source of truth for this intake: it preserves all reachable design sources, raw provider evidence, stable source refs, checksums or retrieval metadata, and schema-required visual facts that SDD `specify` can consume accurately. +Intake does not generate downstream requirement IDs, implementation tasks, code component names, or product-owned selectors. It restores design evidence into source-backed artifacts that downstream Spec Kit workflows can consume. -Evidence chain artifacts are supporting artifacts only. They support confidence and human review, but they must not create, override, replace, or act as `source_refs` for structured UI/visual asset records. +## Artifact Families -The machine-readable structures in this contract are enforced by JSON Schemas under `templates/schemas/` before readiness-specific validation runs. Field lists in this document are semantic summaries; the JSON Schemas are canonical for required fields, types, and enums. +Default root: -This contract is intentionally not a downstream workflow schema. It must not require downstream-owned artifact names, item IDs, code component names, or product semantics that are not directly evidenced by the source. Downstream workflows own any projection from these neutral facts into their local schemas. +```text +specs//intake/visual-design/ +``` + +Source intake artifacts: + +- `design-source-manifest.yaml` +- `source-files/` +- `visual-requirements.yaml` +- `visual-evidence-packet.md` +- Figma sources: `figma-metadata.part-*.xml`, `figma-metadata.index.yaml`, `figma-node-inventory.yaml`, `figma-normalized-tree.yaml` + +Visual IR artifacts: + +- `visual-ir/asset-inventory.yaml` +- `visual-ir/layout-tree.yaml` +- `visual-ir/component-model.yaml` +- `visual-ir/page-route-model.yaml` +- `visual-ir/interaction-model.yaml` +- `visual-ir/motion-anchor-model.yaml` +- `visual-ir/clarification-log.yaml` + +Static HTML delivery artifacts: + +- `delivery/index.html` +- `delivery/assets/` +- `delivery/screenshots/` +- `delivery/render-replay-report.yaml` +- `delivery/evidence-packet.md` + +JSON Schemas are canonical for field shapes. This contract defines semantic policy and readiness ownership. ## Supported Sources @@ -16,306 +46,182 @@ This contract is intentionally not a downstream workflow schema. It must not req Required source fields: -- source_type: image|pdf|markdown|figma -- required_fidelity: low|medium|high -- source_files: - - path: - - mime_type: - - byte_size: required for local files or captured snapshots - - sha256: required for local files or captured snapshots - - checksum_status: verified|unavailable|not_applicable - - role: original|rendered_page|screenshot|asset|markdown -- source_integrity_complete: -- captured_at: -- capture_method: -- page_or_frame_count: -- processed_count: -- extraction_scope: +- `source_type: image|pdf|markdown|figma` +- `required_fidelity: low|medium|high` +- `source_files` +- `source_integrity_complete` +- `captured_at` +- `capture_method` +- `page_or_frame_count` +- `processed_count` +- `extraction_scope` Source-specific requirements: -- Image sources must record image dimensions, crop/region coverage, OCR status when text is present, and any exported asset refs. -- PDF sources must record original PDF hash, page count, processed page count, rendered page references, and text extraction status. -- Markdown sources must record original Markdown hash, embedded or linked asset refs, heading structure, and design-note parsing status. -- Figma sources must additionally satisfy the Figma provider contract below. -- Remote source refs may use `http://`, `https://`, or `figma://` as `source_files[].path`; when no local snapshot exists, record retrieval metadata, `snapshot_status`, `integrity_gap_reason`, and `checksum_status: unavailable|not_applicable` instead of marking source integrity complete. +- Image sources record dimensions, region coverage, OCR status, and asset refs. +- PDF sources record original hash, page count, rendered pages, and text extraction status. +- Markdown sources record heading structure, embedded assets, linked assets, and design-note parsing status. +- Figma sources additionally satisfy the Figma provider contract. ## Fidelity Profile -The required fidelity level controls extraction depth and validation strength. - -- Low fidelity: capture screen or page intent, rough layout hierarchy, major content groups, interaction hints, missing information, and source refs. -- Medium fidelity: include low-fidelity requirements plus key spacing, sizing, typography categories, color roles, assets, states, and responsive clues. -- High fidelity: include medium-fidelity requirements plus exact or bounded dimensions, spacing, typography, tokens, asset export contracts, component variants, page/frame coverage, and comparison thresholds. +- Low fidelity: page intent, rough hierarchy, major regions, rough content, interaction hints, and explicit gaps. +- Medium fidelity: low-fidelity facts plus key spacing, sizing, typography categories, color roles, assets, states, and responsive clues. +- High fidelity: medium-fidelity facts plus exact or bounded dimensions, spacing, typography, tokens, asset export contracts, component variants, page coverage, operation replay, and comparison thresholds. -The intake must record `fidelity_rules_applied: true` and explain any accepted gap. +The intake must record `fidelity_rules_applied: true`. ## Visual Requirements -`visual-requirements.yaml` must normalize extracted visual facts into engineering input. The evidence packet may summarize the same records for human review, but readiness validation uses the standalone machine-readable file. - -`source_refs` must point to original design sources, provider metadata, source files, or structured intake records. They must not point to `visual-evidence-packet.md`, `figma-evidence-packet.md`, `visual-spec-evidence-packet.md`, preview HTML, screenshots, visual diffs, or other helper artifacts as source-of-truth records. - -Visual design intake uses a bounded inference model. Intake may preserve direct observations, rule-backed derived claims, and candidate completions, but it must not smooth missing or contradictory evidence into a confirmed requirement. Every non-observed claim must remain auditable through source refs, inference rules, confidence method, score breakdown, downstream use, and blocking conditions. +`visual-requirements.yaml` records source-backed facts that seed the visual IR and delivery. Each requirement must include: -- id: -- category: layout|spacing|sizing|typography|color|asset|component|state|interaction|responsive|accessibility|content -- requirement: -- source_refs: -- evidence_type: observed|inferred|candidate|unsupported|missing|out_of_scope -- confidence: low|medium|high -- confidence_rationale: -- engineering_action: -- acceptance_check: -- fidelity_level: +- `id` +- `category: layout|spacing|sizing|typography|color|asset|component|state|interaction|responsive|accessibility|content` +- `requirement` +- `source_refs` +- `evidence_type: observed|inferred|candidate|unsupported|missing|out_of_scope` +- `confidence` +- `confidence_rationale` +- `engineering_action` +- `acceptance_check` +- `fidelity_level` -Use `id` as a stable visual evidence ID within this intake package. IDs MUST remain unchanged across recaptures unless the source ref or normalized fact changes. Changed IDs MUST be recorded as a traceability gap. IDs must not be named or formatted as downstream-owned requirement IDs or item IDs. +`source_refs` must point to original design sources, provider metadata, source files, or structured source-intake records. They must not point to delivery HTML, screenshots, visual diffs, evidence packets, or generated implementation artifacts as source-of-truth records. -When evidenced by the source, include provider-neutral optional fields: +## Bounded Inference -- state_or_variant_refs: -- asset_refs: -- constraint_refs: -- proof_refs: -- blockers: +Visual intake may preserve direct observations, rule-backed inferred claims, and candidate completions. It must not smooth missing or contradictory evidence into confirmed behavior. -Bounded inference statuses: +- `observed`: direct source-backed fact. +- `inferred`: high-confidence derived claim with `inference_rule`, `confidence_method`, `score_breakdown`, `downstream_use: accepted_claim`, and `blocking_conditions`. +- `candidate`: low/medium confidence completion with `downstream_use: reference_only` and `missing_evidence`. +- `unsupported`: rejected or blocked claim with `downstream_use: blocked`, `blocker_code`, `reason`, `missing_evidence`, and `blockers`. +- `missing` and `out_of_scope`: explicit absence or excluded surface. -- `observed`: source-backed fact from Figma metadata, exported/rendered evidence, preserved design files, OCR/text extraction, or prototype metadata. -- `inferred`: rule-backed derived claim. It is not a source-backed fact. It must use `confidence: high`, `downstream_use: accepted_claim`, and include `inference_rule`, `confidence_method`, `score_breakdown`, and `blocking_conditions`. -- `candidate`: low- or medium-confidence completion. It must use `downstream_use: reference_only` and include `missing_evidence`. Downstream workflows must not promote it into confirmed requirements without additional source evidence. -- `unsupported`: rejected or blocked claim. It must use `downstream_use: blocked`, include `blocker_code`, `reason`, `missing_evidence`, and a matching entry in `blockers`. -- `missing` and `out_of_scope`: explicit gap markers for absent evidence or intentionally excluded surfaces. They must not be rewritten as inferred requirements. +Do not infer business rules, permissions, form validation, error copy, loading/disabled/focus states, data sources, analytics, security, compliance, route results, or animation timing without source evidence or user confirmation. -Evidence priority for Figma-derived claims: +## Visual IR -1. Component, instance, and variant metadata -2. Prototype reactions -3. Variables, styles, and tokens -4. Auto Layout and constraints -5. Rendered visual evidence -6. Layer naming -7. VLM semantic reading -8. LLM product-pattern inference +Visual IR is the deterministic internal structure used to build the final static HTML. It is not a downstream implementation schema. -Priority does not resolve conflicts by overwrite. When rendered evidence, node geometry, metadata, naming, or prototype data disagree, preserve the conflicting claims and emit a blocker rather than merging them into a single false certainty. +Every visual IR file must include: -Suitable automatic completions include visual roles, layout relations, spacing token candidates, color token candidates, typography token candidates, asset usage maps, basic interaction targets, and component grouping candidates. Do not auto-complete business rules, permissions, form validation, error copy, loading or disabled or focus states, complex responsive rules, data sources, backend fields, analytics, security, or compliance logic as confirmed specifications without direct source support. +- `ready_gate: PASS|BLOCKED` +- `blockers` +- source refs or upstream refs for every ready record -Readiness requires: +The visual IR must cover: -- visual_requirements_complete: true -- visual_requirements_count greater than 0 and equal to the number of records in `requirements` -- source_refs_complete: true -- every requirement has the required fields listed above -- missing_or_uncertain items explicitly recorded instead of silently inferred -- inferred items satisfy the bounded inference contract -- candidate items remain `downstream_use: reference_only` -- unsupported items emit blocker codes and do not pass readiness -- no unsupported summary substitution for original source evidence -- no blocker lint errors +- assets/resources: full inventory, local delivery paths, checksums, usage refs, missing resources, substitutions +- layout/boxes: hierarchy, bounding boxes, constraints, z-order, overflow, scroll, responsive rules +- components/states/IA: component sets, instances, variants, props, visual states, accessibility expectations, state evidence, missing states +- pages/routes/IA: pages, route states, overlays, navigation targets, viewport variants, missing route behavior +- interactions: events, targets, preconditions, system feedback, state changes, route changes, exception branches, replay steps +- motion anchors: trigger, affected surface, initial/end state, duration, easing, delay, repeat behavior -## Intake Parity Plan +## Clarification Gate -The extracted requirements must make later downstream comparison possible against the original design source. The intake package records source-side comparison constraints only; downstream workflows own implementation captures, delivery reports, and final approval decisions. +When evidence is missing and affects static HTML visual parity or user-operation replay, the command must ask the user instead of inventing behavior. -Required parity fields in `visual-requirements.yaml`: +`visual-ir/clarification-log.yaml` must record each question: -- visual_parity_plan_complete: -- parity_plan: - - comparison_targets: - - original_refs: - - comparison_method: visual_diff|measurement|token_match|asset_match|manual_review - - thresholds: - - accepted_exceptions: - - blocking_difference_categories: +- `id` +- `target_artifact` +- `blocked_delivery_surface` +- `blocker_code` +- `question` +- `allowed_answer_shape` +- `required_for_html` +- `status: unanswered|answered|out_of_scope` +- `answer` +- `confirmed_by_user` +- `source_refs` -The evidence packet front matter records the `ready_gate` enum only for intake readiness. +Readiness is blocked when any required question is unanswered. -## Figma Provider Contract +## Static HTML Delivery -For `source_type: figma`, `figma-metadata.part-*.xml` must preserve raw `get_metadata` output. +`delivery/index.html` is the final delivery artifact. It must be static and offline-runnable. It may include embedded CSS and vanilla JavaScript to replay source-backed or user-confirmed operations. -- Do not summarize, rewrite, compress into prose, or replace real nodes with natural language. -- Cover the complete descendant subtree for every selected frame or node. -- Treat truncation as failed evidence. +Delivery readiness requires: -Each part must be listed with: +- all required source intake gates pass +- all required visual IR files exist and pass +- all required clarifications are answered or out of scope +- every page, route state, component state, operation, motion anchor, and viewport referenced by `render-replay-report.yaml` resolves to an anchor in `index.html` +- every required local asset resolves under `delivery/assets/` +- every required operation has replay evidence +- screenshots exist for required viewports +- visual diff records pass or are explicitly blocked +- `delivery/evidence-packet.md` front matter reports `ready_gate: PASS` with no blockers -- path: -- byte_size: -- sha256: -- root_node_ids: -- node_count: -- truncated: +## Figma Provider Contract -`figma-metadata.index.yaml` must prove source identity, shard integrity, and selected subtree completeness. +For `source_type: figma`, raw `get_metadata` output must be preserved in `figma-metadata.part-*.xml`. -Required source fields: +Readiness requires: -- file_url: -- file_key: -- page_id: -- selected_node_ids: -- captured_at: -- mcp_tool: get_metadata -- design_version_or_timestamp: - -Required completeness fields: - -- selected_subtree_complete: -- raw_metadata_complete: -- expected_root_node_ids: -- captured_root_node_ids: -- missing_root_node_ids: -- gap_count: -- gaps: - -`figma-node-inventory.yaml` must reconcile inventory with raw metadata. - -Required parity fields: - -- raw_node_count: -- inventory_node_count: -- excluded_node_count: -- missing_node_count: -- duplicate_node_count: -- truncated_raw_evidence: -- node_inventory_coverage: 100% -- parity_passed: true - -Required parity rules: - -- inventory_node_count + excluded_node_count + missing_node_count == raw_node_count -- duplicate_node_count == 0 -- missing_node_count == 0 -- truncated_raw_evidence == false -- parity_passed equals count balance, no duplicates, no missing nodes, and no truncation - -## Figma Layout Normalization - -After raw Figma metadata and node inventory parity pass, Figma sources must derive `figma-normalized-tree.yaml` before extracting `visual-requirements.yaml`. - -The normalized tree is a derived provider-neutral view of Figma layout structure. It exists to make downstream layout extraction deterministic and visually ordered. It must not modify `figma-metadata.part-*.xml`, replace `figma-metadata.index.yaml`, replace `figma-node-inventory.yaml`, or become a source-of-truth substitute for raw provider evidence. - -Allowed normalization operations: - -- `rename`: write `normalized_name` while preserving `source_node_id` and `original_name`. -- `grouper`: write `group_key` and `parent_group_key` to express provider-neutral containment or visual grouping. -- `re-sort`: write `sort_key` and `visual_order` using stable visual ordering rules. - -Required normalization fields: - -- normalization_complete: -- source_metadata_refs: -- source_index_ref: -- source_inventory_ref: -- normalization_rules_applied: -- rename_rule: -- group_rule: -- sort_rule: -- raw_node_count: -- normalized_node_count: -- node_coverage: -- gaps: -- nodes: - -Each normalized node must include: - -- source_node_id: -- original_name: -- normalized_name: -- node_type: -- role_hint: -- group_key: -- parent_group_key: -- sort_key: -- visual_order: -- source_refs: - -Required normalization rules: - -- `source_node_id` must remain unchanged from raw Figma metadata. -- `original_name` must preserve the provider node name as captured. -- `normalized_name` must not overwrite raw node names. -- `source_refs` must point to raw Figma metadata shards or canonical Figma intake artifacts, not previews, screenshots, visual diffs, evidence packets, implementation files, or downstream artifacts. -- `visual_order` must be unique and contiguous across normalized nodes. -- `normalized_node_count` must equal `raw_node_count`. -- `node_coverage` must be `100%`. -- `normalization_complete` must be true only when every raw node appears exactly once in the normalized tree and no normalization gaps remain. - -Do not place rename maps, grouping strategy, sort indexes, layout patches, fixup reasons, dedupe rules, merge rules, flattening decisions, postprocess notes, preview refs, or visual-diff refs in `visual-requirements.yaml`. Keep normalization records in `figma-normalized-tree.yaml`. - -## Evidence Readiness Gate - -Visual design intake is ready only when all conditions pass: - -- source_integrity_complete: true -- source_type is image, pdf, markdown, or figma -- required_fidelity is low, medium, or high -- visual_requirements_complete: true -- source_refs_complete: true -- fidelity_rules_applied: true -- inferred claims satisfy the bounded inference contract -- candidate claims remain `downstream_use: reference_only` -- unsupported claims emit blocker codes and block readiness -- visual_parity_plan_complete: true and required parity plan fields are present -- Figma sources also pass raw_metadata_complete, selected_subtree_complete, node_inventory_coverage, and parity_passed -- Figma sources also pass layout normalization with `figma-normalized-tree.yaml`, `normalization_complete: true`, `node_coverage: 100%`, unique contiguous `visual_order`, and one normalized node per raw node -- No blocker lint errors - -## Blocker Lint Errors - -- VISUAL_SOURCE_MANIFEST_MISSING -- VISUAL_SOURCE_TYPE_UNSUPPORTED -- VISUAL_FIDELITY_LEVEL_UNSUPPORTED -- VISUAL_SOURCE_FILE_MISSING -- VISUAL_SOURCE_HASH_MISMATCH -- VISUAL_SOURCE_INTEGRITY_INCOMPLETE -- VISUAL_REQUIREMENTS_MISSING -- VISUAL_REQUIREMENTS_UNTRACEABLE -- VISUAL_FIDELITY_RULES_MISSING -- VISUAL_PARITY_PLAN_MISSING -- VISUAL_READY_WITHOUT_EVIDENCE -- VISUAL_EVIDENCE_PACKET_MISSING -- VISUAL_BLOCKER_LINT_ERRORS -- VISUAL_INFERENCE_CONTRACT_INVALID -- VISUAL_SCHEMA_INVALID -- FIGMA_RENDER_NODE_MISMATCH -- FIGMA_HIDDEN_LAYER_POLLUTION -- FIGMA_NON_INSTANCE_COMPONENT -- FIGMA_PROTOTYPE_METADATA_MISSING -- FIGMA_UNSUPPORTED_STATE_INFERENCE -- FIGMA_BUSINESS_RULE_UNSUPPORTED -- FIGMA_INTERACTION_CONFLICT -- FIGMA_RESPONSIVE_RULE_MISSING -- FIGMA_LOW_CONFIDENCE_CANDIDATE -- FIGMA_RAW_METADATA_MISSING -- FIGMA_RAW_METADATA_SUMMARY_SUBSTITUTION -- FIGMA_RAW_METADATA_TRUNCATED -- FIGMA_SELECTED_SUBTREE_INCOMPLETE -- FIGMA_METADATA_INDEX_MISSING -- FIGMA_METADATA_PARITY_FAILED -- FIGMA_READY_WITHOUT_COMPLETENESS_PROOF -- FIGMA_NORMALIZED_TREE_MISSING -- FIGMA_NORMALIZED_TREE_INCOMPLETE - -## Gap Rules - -Record a gap instead of passing silently when source evidence is missing, summarized, truncated, incomplete, untraceable, missing fidelity proof, missing visual requirements, missing comparison proof, missing Figma parity proof, missing Figma layout normalization proof, missing nodes, duplicate nodes, duplicate visual order, incomplete normalized coverage, or marked ready without completeness proof. For dirty Figma sources, use blocker codes for hidden-layer pollution, render/node mismatch, non-instance pseudo-components, missing prototype metadata, unsupported state inference, interaction conflicts, and missing responsive evidence. +- complete descendant subtree coverage for selected roots +- no truncated raw evidence +- `figma-metadata.index.yaml` proves source identity and shard integrity +- `figma-node-inventory.yaml` reconciles raw node count, exclusions, missing nodes, duplicates, and truncation +- `figma-normalized-tree.yaml` derives visual order without rewriting raw provider metadata + +`figma-normalized-tree.yaml` may record only provider-neutral layout normalization fields. It must not include delivery refs, selectors, implementation tasks, code component names, route semantics, or product behavior. + +## Blocker Codes + +Source-intake blockers: + +- `VISUAL_SOURCE_MANIFEST_MISSING` +- `VISUAL_SOURCE_TYPE_UNSUPPORTED` +- `VISUAL_FIDELITY_LEVEL_UNSUPPORTED` +- `VISUAL_SOURCE_FILE_MISSING` +- `VISUAL_SOURCE_HASH_MISMATCH` +- `VISUAL_SOURCE_INTEGRITY_INCOMPLETE` +- `VISUAL_REQUIREMENTS_MISSING` +- `VISUAL_REQUIREMENTS_UNTRACEABLE` +- `VISUAL_FIDELITY_RULES_MISSING` +- `VISUAL_PARITY_PLAN_MISSING` +- `VISUAL_READY_WITHOUT_EVIDENCE` +- `VISUAL_EVIDENCE_PACKET_MISSING` +- `VISUAL_BLOCKER_LINT_ERRORS` +- `VISUAL_INFERENCE_CONTRACT_INVALID` +- `VISUAL_SCHEMA_INVALID` + +Figma blockers: + +- `FIGMA_RAW_METADATA_MISSING` +- `FIGMA_RAW_METADATA_SUMMARY_SUBSTITUTION` +- `FIGMA_RAW_METADATA_TRUNCATED` +- `FIGMA_SELECTED_SUBTREE_INCOMPLETE` +- `FIGMA_METADATA_INDEX_MISSING` +- `FIGMA_METADATA_PARITY_FAILED` +- `FIGMA_READY_WITHOUT_COMPLETENESS_PROOF` +- `FIGMA_NORMALIZED_TREE_MISSING` +- `FIGMA_NORMALIZED_TREE_INCOMPLETE` +- `FIGMA_RENDER_NODE_MISMATCH` +- `FIGMA_HIDDEN_LAYER_POLLUTION` +- `FIGMA_NON_INSTANCE_COMPONENT` +- `FIGMA_PROTOTYPE_METADATA_MISSING` +- `FIGMA_UNSUPPORTED_STATE_INFERENCE` +- `FIGMA_BUSINESS_RULE_UNSUPPORTED` +- `FIGMA_INTERACTION_CONFLICT` +- `FIGMA_RESPONSIVE_RULE_MISSING` +- `FIGMA_LOW_CONFIDENCE_CANDIDATE` + +Static delivery blockers are defined in `templates/intake-static-html-delivery-contract.md`. ## Evidence Packet Metadata -`visual-evidence-packet.md` must start with YAML front matter: - -- ready_gate: `PASS|BLOCKED` -- blockers: -- source_ref_count: -- extracted_item_count: -- generated_at: +`visual-evidence-packet.md` and `delivery/evidence-packet.md` must start with YAML front matter: -Human-readable sections may summarize the same records, but readiness metadata is validated from the front matter when present. +- `ready_gate: PASS|BLOCKED` +- `blockers` +- `source_ref_count` +- `extracted_item_count` +- `generated_at` -The evidence packet is not a source-of-truth record. It must summarize structured asset readiness and confidence only; downstream workflows must not derive requirements from the packet when the corresponding structured record is absent or blocked. +Evidence packets summarize validator-backed readiness. They are not source-of-truth records. diff --git a/extensions/intake/templates/intake-visual-previews-contract.md b/extensions/intake/templates/intake-visual-previews-contract.md deleted file mode 100644 index 8142053786..0000000000 --- a/extensions/intake/templates/intake-visual-previews-contract.md +++ /dev/null @@ -1,213 +0,0 @@ -# HTML Mock Delivery Contract - -Required HTML mock equivalent delivery page and coverage readiness gates. `preview.html` is the static HTML/CSS mock equivalent for the visual input design, generated from upstream intake artifacts. It renders intake-backed pages, layout regions, components, visual states, interaction states, content samples, and viewport surfaces before downstream implementation. Coverage artifacts prove whether those rendered mock surfaces are traceable and complete, but they are not the target visual requirements/spec asset package. - -HTML mock delivery does not generate requirements, production implementation HTML, product semantics, downstream-owned selectors, tasks, code component names, or design tokens. It preserves source-backed coverage evidence that points back to design-source refs and forward to `visual-spec-package/` records. - -HTML mock delivery is assembled from the structured UI/visual asset, visual spec package records, coverage YAML, screenshot refs, and source-backed records. It must not create, override, replace, or backfill `visual-requirements.yaml`, `visual-spec.yaml`, or `visual-spec-assertions.yaml`; `preview.html` may implement only existing structured facts and explicit missing or blocked records as the HTML mock equivalent. - -## Artifact Family - -Default directory: - -```text -specs//intake/visual-design/previews/ -``` - -Required files: - -- `preview.html` -- `component-coverage.yaml` -- `viewport-coverage.yaml` -- `known-gaps.md` -- `screenshots/` - -## Source Boundary - -HTML mock delivery is downstream of visual-design intake and adjacent to the visual spec package: - -1. Visual-design intake records source-backed facts, limitations, Figma metadata, node inventory, and visual requirements. -2. Visual Spec Package records the target structured visual requirements/spec facts. -3. HTML mock delivery records the generated visual-equivalent mock page plus machine-readable coverage evidence. - -If Figma or design-source evidence is missing, truncated, contradictory, or blocked, HTML mock delivery must record a `VISUAL_PREVIEW_*` blocker and keep the affected coverage cell missing. Do not silently complete a missing state, variant, resource, viewport, or page behavior in preview HTML. - -## `preview.html` - -The file is the generated static HTML/CSS mock equivalent for UI intake. It implements the visual input design from upstream intake artifacts as rendered mock pages, regions, components, states, interaction surfaces, content samples, and viewport surfaces. Each visualized mock surface that coverage records reference must expose a stable anchor such as `id`, `data-preview-id`, or `data-interaction-id`. - -The IA matrix is the coverage and interaction evidence layer for the HTML mock. Do not let IA matrix tables replace the visualized page and component mock surfaces, and do not create a standalone interaction matrix that is disconnected from the visual states it exercises. - -Required top-level order: - -1. Rendered mock page surfaces, including required page regions, component instances, content samples, and viewport-specific surfaces. -2. IA matrix overview for fused interactions. -3. For each required page: - - page visual state enumeration - - page IA matrix -4. For each required component: - - component visual state enumeration - - component IA matrix with event interaction information -5. Coverage evidence conclusion - -The HTML must expose these stable section anchors so readiness can be checked: - -- `data-preview-section="mock-page"` -- `data-preview-section="ia-matrix-overview"` -- `data-preview-section="page-state-enumeration"` -- `data-preview-section="page-ia-matrix"` -- `data-preview-section="component-state-enumeration"` -- `data-preview-section="component-ia-matrix"` -- `data-preview-section="coverage-evidence-conclusion"` - -Each visual-state enumeration cell must render the state visually or point to source-backed screenshot evidence. A prose-only state row is a missing coverage cell unless the state is explicitly blocked or out of scope. - -Stable anchors must be unambiguous. Values used in `id`, `data-preview-id`, or `data-interaction-id` must not resolve to multiple HTML elements. - -Each page IA matrix row must fuse the current interaction matrix information into the page state that owns it. Required IA fields: - -- `page_region` -- `visual_state` -- `user_event` -- `precondition` -- `system_response` -- `state_change` -- `transition_or_overlay` -- `exception_branch` -- `evidence_ref` -- `coverage_status` - -Each component IA matrix row must include event interaction information for the component state. Required IA fields: - -- `component_state` -- `visible_elements` -- `action_target` -- `user_event` -- `precondition` -- `immediate_feedback` -- `state_change` -- `affected_surface` -- `disabled_or_error_rule` -- `evidence_ref` -- `coverage_status` - -Use stable anchors such as `id`, `data-preview-id`, or `data-interaction-id` for every visualized mock page, visualized component/state node, visual-state cell, and IA matrix row that a coverage record references. Component `preview_ref` values must resolve to visualized component or state nodes, not only explanatory text or IA matrix rows. - -Every visualized component or state node referenced by `component-coverage.yaml` `preview_ref` must expose `data-preview-kind` with one of these values: - -- `component` -- `component-state` -- `component-instance` -- `mock-component` -- `mock-component-state` -- `visual-state` - -The preview page may display: - -- rendered pages and page regions -- component sets and component instances as HTML/CSS mock nodes -- variant props -- states -- page IA rows and component IA rows -- event, precondition, feedback, transition, exception, and return-path evidence -- size, density, and theme dimensions -- content samples, including long copy, empty, overflow, and error-like visual states when source-backed -- viewport-specific snapshots or links -- missing, blocked, and out-of-scope labels - -The preview page must not define product semantics, downstream component names, production implementation selectors, design tokens, or source-backed facts that are absent from upstream intake artifacts. Its equivalence is bounded by the validated intake facts and explicit missing or blocked records. - -## `component-coverage.yaml` - -The file is the machine-readable component coverage evidence. - -`source_ref` fields must point to original design sources, provider metadata, or structured asset records. Preview HTML, screenshots, visual diffs, and evidence packets may be referenced only by preview-specific or screenshot-specific fields. - -Top-level fields: - -- ready_gate: PASS|BLOCKED -- blockers: array of `VISUAL_PREVIEW_*` or allowed upstream `VISUAL_SPEC_*` blocker codes -- components: array - -Each component must include: - -- id -- source_ref -- name -- required_dimensions -- covered -- missing - -Each covered record must include: - -- visual_spec_ref -- preview_ref -- interaction_ref -- optional source_ref -- optional screenshot_refs -- dimension values matching the component's required dimensions when applicable - -`visual_spec_ref` values must point to `../visual-spec-package/visual-spec.yaml#` and resolve to existing `visual-spec-package/visual-spec.yaml` item IDs. `screenshot_refs`, when present, must resolve to existing files under the preview artifact directory. - -Each missing record must include: - -- missing_type: state|variant|viewport|resource|asset|token|screenshot|visual_diff|source_evidence|visual_spec_ref|preview_ref -- reason -- blocker - -## `viewport-coverage.yaml` - -The file is the machine-readable viewport coverage evidence. - -`source_refs` must point to original design sources, provider metadata, or structured asset records. `page_refs`, `screenshot_refs`, and diff outputs are supporting preview evidence and must not replace source refs. - -Each viewport record must include: - -- id -- width -- height -- covered -- source_refs -- visual_spec_refs -- page_refs -- screenshot_refs -- visual_diff_status: pass|blocked|not_applicable - -Missing viewport evidence must stay explicit in `missing` records or top-level blockers. - -## Readiness - -HTML mock delivery is ready only when: - -- upstream visual-design intake readiness is PASS -- adjacent `visual-spec-package/` readiness is PASS -- required preview artifacts exist -- `component-coverage.yaml` validates against `component-coverage.schema.json` -- `viewport-coverage.yaml` validates against `viewport-coverage.schema.json` -- every covered component record has a `visual_spec_ref` -- every covered component record has a `preview_ref` that resolves to a visualized mock component or state node inside `preview.html` -- every covered component record has an `interaction_ref` that resolves inside `preview.html` -- every covered component `visual_spec_ref` points to `visual-spec-package/visual-spec.yaml` and resolves to an existing item -- component screenshot refs resolve to existing files when present -- `preview.html` contains the required mock page section, IA matrix sections, and required IA field markers -- each page and component IA matrix row contains the full required IA field set -- preview anchors are unique across `id`, `data-preview-id`, and `data-interaction-id` -- page refs and visual spec refs in `viewport-coverage.yaml` resolve to mock page surfaces and visual spec items -- no missing record remains for required component states, variants, resources, assets, tokens, screenshots, visual diffs, source evidence, visual spec refs, or preview refs -- every viewport record is covered and has existing screenshot refs -- at least one viewport has page refs -- `known-gaps.md` has no unresolved `BLOCKED`, `UNRESOLVED`, or `TODO` marker - -## Blocker Codes - -- `VISUAL_PREVIEW_SOURCE_INTAKE_BLOCKED` -- `VISUAL_PREVIEW_REQUIRED_ARTIFACT_MISSING` -- `VISUAL_PREVIEW_SCHEMA_INVALID` -- `VISUAL_PREVIEW_FIGMA_NODE_COVERAGE_INCOMPLETE` -- `VISUAL_PREVIEW_COMPONENT_STATE_COVERAGE_INCOMPLETE` -- `VISUAL_PREVIEW_IA_MATRIX_INCOMPLETE` -- `VISUAL_PREVIEW_PAGE_COVERAGE_INCOMPLETE` -- `VISUAL_PREVIEW_ASSET_TRACEABILITY_INCOMPLETE` -- `VISUAL_PREVIEW_VIEWPORT_CAPTURE_INCOMPLETE` -- `VISUAL_PREVIEW_VISUAL_DIFF_BLOCKED` -- `VISUAL_PREVIEW_KNOWN_GAP_UNRESOLVED` diff --git a/extensions/intake/templates/intake-visual-spec-package-contract.md b/extensions/intake/templates/intake-visual-spec-package-contract.md deleted file mode 100644 index 1c77eb1baf..0000000000 --- a/extensions/intake/templates/intake-visual-spec-package-contract.md +++ /dev/null @@ -1,128 +0,0 @@ -# Visual Spec Package Contract - -Required visual requirements/spec structured asset package artifacts and readiness gates. Runtime agents or external intake tools derive high-fidelity UI delivery facts from traceable visual/design evidence before downstream workflows choose their own test runner, selectors, requirement IDs, or implementation ownership. - -Visual Spec Package does not generate requirements, tasks, code component names, implementation-owned selectors, or product semantics. It preserves source-backed DOM, ARIA, token, state, relation, locator-strategy, and assertion facts that can be consumed by low-cost CI checks. - -`visual-spec.yaml` and `visual-spec-assertions.yaml` are the structured asset records for this package. `visual-spec-evidence-packet.md`, preview panels, screenshots, and visual diffs are supporting artifacts only; they must not create, override, replace, or serve as source-of-truth records for spec items or assertions. - -## Artifact Family - -Default directory: - -```text -specs//intake/visual-design/visual-spec-package/ -``` - -Required files: - -- `visual-spec.yaml` -- `visual-spec-assertions.yaml` -- `visual-spec-evidence-packet.md` - -Optional helper refs may point to `previews/preview.html`, `previews/component-coverage.yaml`, `previews/viewport-coverage.yaml`, screenshots, or visual diff reports. In the visual spec package, these remain helper refs; `preview.html` is the equivalent HTML delivery artifact of the preview bundle, not a `source_refs` authority for spec facts. - -## Source Boundary - -Visual Spec Package is downstream of source evidence and upstream of implementation tests: - -1. Visual/design intake records source-backed facts, limitations, Figma metadata, and visual requirements. -2. Preview delivery records the visual-equivalent HTML page, component coverage, viewport coverage, and screenshot comparison evidence when useful. -3. Visual Spec Package records deterministic visual requirements/spec facts suitable for downstream implementation and CI. - -If source evidence is missing, truncated, contradictory, or blocked, visual spec package must record `VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING`. If the source is clear but product behavior is ambiguous, it must record `VISUAL_SPEC_PRODUCT_AMBIGUITY_UNRESOLVED`. Do not collapse these into one generic gap. - -## `visual-spec.yaml` - -The file must normalize UI acceptance facts into provider-neutral records. - -Top-level fields: - -- visual_spec_package_complete: true|false -- visual_spec_item_count: integer -- source_refs_complete: true|false -- provider_evidence_complete: true|false -- resources_traceable_to_design_source: true|false -- product_ambiguities_recorded: true|false -- downstream_ownership_free: true|false -- product_ambiguities: array -- blocker_lint_errors: array -- items: array - -Each item must include: - -- id: stable `VS-*` identifier owned by intake -- source_refs: preserved source evidence refs -- des_refs: optional design evidence source refs -- visual_requirement_refs: optional refs to `visual-requirements.yaml` -- preview_refs: optional refs to `previews/preview.html`, `previews/component-coverage.yaml`, `previews/viewport-coverage.yaml`, screenshots, or diff evidence -- page, region, role, state, viewport -- locator: provider-neutral strategy, value, and `implementation_owned: false` -- expectations: DOM, ARIA, design token, state, content, or relation facts -- acceptance_intent: what low-cost check this fact supports -- evidence_type: observed|inferred|candidate|unsupported|missing|out_of_scope -- confidence: low|medium|high -- status: ready|blocked|reference_only|out_of_scope -- blockers: array - -Locator strategies must not be implementation-owned CSS selectors, XPath, generated class names, or downstream test IDs. Candidate test IDs may be recorded only as `test-id-candidate` and must remain intake-owned guidance, not implementation ownership. - -`source_refs` must point to original design sources, provider metadata, upstream `visual-requirements.yaml` records, or source-backed structured asset records. Preview refs and evidence packet refs belong in helper fields such as `preview_refs`; they must not be used as `source_refs`. - -## `visual-spec-assertions.yaml` - -The file must describe low-cost assertions over visual spec items. - -Top-level fields: - -- assertions_complete: true|false -- assertion_count: integer -- ci_assertions_complete: true|false -- blocker_lint_errors: array -- assertions: array - -Each assertion must include: - -- id: stable `VSA-*` identifier owned by intake -- visual_spec_refs: one or more `VS-*` refs -- assertion_type: visible|hidden|enabled|disabled|contains_text|role|aria|token|relation|state|viewport -- acceptance_intent -- expected -- evidence_refs -- ci_suitability: ci_low_cost|manual_review|blocked -- status: ready|blocked|reference_only|out_of_scope -- blockers - -Ready assertions should use `ci_suitability: ci_low_cost`. Manual review and blocked assertions are allowed as explicit evidence, but they cannot satisfy CI readiness. - -`evidence_refs` for ready assertions must point to source-backed design refs or structured visual spec records. They must not point to preview HTML, screenshots, visual diffs, or evidence packets as the only fact source. - -## Readiness - -Visual Spec Package intake is ready only when: - -- upstream visual-design intake readiness is PASS -- required visual spec package artifacts exist -- both schemas validate -- item and assertion counts match their arrays -- every ready assertion references an existing visual spec item -- source refs and provider evidence are complete -- implementation resources, images, and token refs are traceable to the design source; for Figma sources, they must trace back to Figma refs -- product ambiguities are explicitly recorded and unresolved ambiguity does not masquerade as missing provider evidence -- locators are provider-neutral and not implementation-owned -- no downstream requirement IDs, tasks, code component names, implementation-owned selectors, or product semantics leak into the visual spec package -- at least one ready `ci_low_cost` assertion exists -- `visual-spec-evidence-packet.md` front matter reports `ready_gate: PASS` with no blockers - -## Blocker Codes - -- `VISUAL_SPEC_SOURCE_INTAKE_BLOCKED` -- `VISUAL_SPEC_REQUIRED_ARTIFACT_MISSING` -- `VISUAL_SPEC_SCHEMA_INVALID` -- `VISUAL_SPEC_INTAKE_INCOMPLETE` -- `VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING` -- `VISUAL_SPEC_PRODUCT_AMBIGUITY_UNRESOLVED` -- `VISUAL_SPEC_ASSERTION_COVERAGE_INCOMPLETE` -- `VISUAL_SPEC_LOCATOR_STRATEGY_INVALID` -- `VISUAL_SPEC_DOWNSTREAM_OWNERSHIP_LEAK` -- `VISUAL_SPEC_READY_WITHOUT_EVIDENCE` diff --git a/extensions/intake/templates/intake-visual-spec-package-evidence-packet-template.md b/extensions/intake/templates/intake-visual-spec-package-evidence-packet-template.md deleted file mode 100644 index 406a4fc8b9..0000000000 --- a/extensions/intake/templates/intake-visual-spec-package-evidence-packet-template.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -ready_gate: BLOCKED -blockers: [] -source_ref_count: 0 -extracted_item_count: 0 -generated_at: "" ---- - -# Visual Spec Package Evidence Packet - -Purpose: summarize visual requirements/spec structured asset package readiness while preserving enough traceability for downstream CI workflows to consume deterministic DOM, ARIA, token, state, relation, locator-strategy, and assertion facts. - -This packet is a human-readable readiness summary. Machine-readable UI acceptance facts are recorded in `visual-spec.yaml` and `visual-spec-assertions.yaml` and validated by `templates/schemas/visual-spec-package.schema.json` and `templates/schemas/visual-spec-assertions.schema.json`. This packet does not define downstream requirement IDs, implementation tasks, code component names, implementation-owned selectors, or final product behavior. - -## Source Boundary - -- Visual/design intake directory: -- Visual/design readiness: -- HTML delivery/helper refs, if used: -- Screenshot or visual diff refs, if used: - -## Visual Spec Package Summary - -- visual-spec.yaml: -- visual-spec-assertions.yaml: -- visual spec package item count: -- assertion count: -- CI-low-cost assertion count: - -## Evidence Separation - -- Missing provider evidence: -- Resource refs not traceable to design source: -- Product ambiguities: -- Accepted out-of-scope surfaces: -- Manual-review-only assertions: - -## Readiness - -- ready_gate: -- blockers: -- next corrective action: diff --git a/extensions/intake/templates/schemas/component-coverage.schema.json b/extensions/intake/templates/schemas/component-coverage.schema.json deleted file mode 100644 index 4cb9d44f8d..0000000000 --- a/extensions/intake/templates/schemas/component-coverage.schema.json +++ /dev/null @@ -1,110 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://spec-kit-intake.local/schemas/component-coverage.schema.json", - "title": "Visual Design Component Coverage", - "type": "object", - "required": ["ready_gate", "blockers", "components"], - "properties": { - "ready_gate": { "enum": ["PASS", "BLOCKED"] }, - "blockers": { - "type": "array", - "items": { "$ref": "#/$defs/blocker_code" } - }, - "components": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": ["id", "source_ref", "name", "required_dimensions", "covered", "missing"], - "properties": { - "id": { "type": "string", "minLength": 1 }, - "source_ref": { "$ref": "#/$defs/source_ref" }, - "name": { "type": "string", "minLength": 1 }, - "required_dimensions": { - "type": "object", - "minProperties": 1, - "additionalProperties": { - "type": "array", - "minItems": 1, - "items": { "type": "string", "minLength": 1 } - } - }, - "covered": { - "type": "array", - "items": { - "type": "object", - "required": ["visual_spec_ref", "preview_ref", "interaction_ref"], - "properties": { - "visual_spec_ref": { "type": "string", "minLength": 1 }, - "preview_ref": { "type": "string", "minLength": 1 }, - "interaction_ref": { "type": "string", "minLength": 1 }, - "source_ref": { "$ref": "#/$defs/source_ref" }, - "screenshot_refs": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - } - }, - "additionalProperties": true - } - }, - "missing": { - "type": "array", - "items": { - "type": "object", - "required": ["missing_type", "reason", "blocker"], - "properties": { - "missing_type": { - "enum": [ - "state", - "variant", - "viewport", - "resource", - "asset", - "token", - "screenshot", - "visual_diff", - "source_evidence", - "visual_spec_ref", - "preview_ref", - "interaction_ref" - ] - }, - "reason": { "type": "string", "minLength": 1 }, - "blocker": { "$ref": "#/$defs/blocker_code" } - }, - "additionalProperties": true - } - } - }, - "additionalProperties": true - } - } - }, - "$defs": { - "blocker_code": { - "enum": [ - "VISUAL_PREVIEW_SOURCE_INTAKE_BLOCKED", - "VISUAL_PREVIEW_REQUIRED_ARTIFACT_MISSING", - "VISUAL_PREVIEW_FIGMA_NODE_COVERAGE_INCOMPLETE", - "VISUAL_PREVIEW_COMPONENT_STATE_COVERAGE_INCOMPLETE", - "VISUAL_PREVIEW_IA_MATRIX_INCOMPLETE", - "VISUAL_PREVIEW_PAGE_COVERAGE_INCOMPLETE", - "VISUAL_PREVIEW_ASSET_TRACEABILITY_INCOMPLETE", - "VISUAL_PREVIEW_VIEWPORT_CAPTURE_INCOMPLETE", - "VISUAL_PREVIEW_VISUAL_DIFF_BLOCKED", - "VISUAL_PREVIEW_KNOWN_GAP_UNRESOLVED", - "VISUAL_PREVIEW_SCHEMA_INVALID", - "VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING", - "VISUAL_SPEC_ASSERTION_COVERAGE_INCOMPLETE" - ] - }, - "source_ref": { - "type": "string", - "minLength": 1, - "not": { - "pattern": "(visual-evidence-packet\\.md|figma-evidence-packet\\.md|visual-spec-evidence-packet\\.md|preview\\.html|previews/|screenshots/|visual-diff|diff-output)" - } - } - }, - "additionalProperties": true -} diff --git a/extensions/intake/templates/schemas/figma-normalized-tree.schema.json b/extensions/intake/templates/schemas/figma-normalized-tree.schema.json index dafbc06378..f07629f3ea 100644 --- a/extensions/intake/templates/schemas/figma-normalized-tree.schema.json +++ b/extensions/intake/templates/schemas/figma-normalized-tree.schema.json @@ -118,7 +118,7 @@ "$defs": { "no_downstream_field_name": { "not": { - "pattern": "(preview|screenshot|diff|evidence_packet|requirement|implementation|task|selector|locator|code_component|component_name|visual_spec|acceptance|prd|test_case)" + "pattern": "(preview|delivery|screenshot|diff|evidence_packet|requirement|implementation|task|selector|locator|code_component|component_name|visual_spec|acceptance|prd|test_case)" } } }, diff --git a/extensions/intake/templates/schemas/static-html-delivery.schema.json b/extensions/intake/templates/schemas/static-html-delivery.schema.json new file mode 100644 index 0000000000..8e33074e94 --- /dev/null +++ b/extensions/intake/templates/schemas/static-html-delivery.schema.json @@ -0,0 +1,240 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://spec-kit-intake.local/schemas/static-html-delivery.schema.json", + "title": "Static HTML Delivery Render Replay Report", + "type": "object", + "required": [ + "ready_gate", + "blockers", + "html_entry", + "source_intake_ref", + "visual_ir_refs", + "assets", + "pages", + "components", + "operations", + "motion_anchors", + "viewports", + "visual_diffs", + "clarifications" + ], + "properties": { + "ready_gate": { "enum": ["PASS", "BLOCKED"] }, + "blockers": { + "type": "array", + "items": { "$ref": "#/$defs/blocker_code" } + }, + "html_entry": { "type": "string", "minLength": 1 }, + "source_intake_ref": { "type": "string", "minLength": 1 }, + "visual_ir_refs": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + }, + "assets": { + "type": "array", + "items": { "$ref": "#/$defs/asset_record" } + }, + "pages": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/page_record" } + }, + "components": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/component_record" } + }, + "operations": { + "type": "array", + "items": { "$ref": "#/$defs/operation_record" } + }, + "motion_anchors": { + "type": "array", + "items": { "$ref": "#/$defs/motion_record" } + }, + "viewports": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/viewport_record" } + }, + "visual_diffs": { + "type": "array", + "items": { "$ref": "#/$defs/visual_diff_record" } + }, + "clarifications": { + "type": "object", + "required": [ + "required_question_count", + "answered_required_question_count", + "unanswered_required_question_ids" + ], + "properties": { + "required_question_count": { "type": "integer", "minimum": 0 }, + "answered_required_question_count": { "type": "integer", "minimum": 0 }, + "unanswered_required_question_ids": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + }, + "additionalProperties": true + } + }, + "$defs": { + "blocker_code": { + "enum": [ + "STATIC_HTML_SOURCE_INTAKE_BLOCKED", + "STATIC_HTML_REQUIRED_ARTIFACT_MISSING", + "STATIC_HTML_SCHEMA_INVALID", + "STATIC_HTML_IR_BLOCKED", + "STATIC_HTML_CLARIFICATION_REQUIRED", + "STATIC_HTML_ASSET_INCOMPLETE", + "STATIC_HTML_LAYOUT_INCOMPLETE", + "STATIC_HTML_COMPONENT_STATE_INCOMPLETE", + "STATIC_HTML_PAGE_ROUTE_INCOMPLETE", + "STATIC_HTML_OPERATION_REPLAY_INCOMPLETE", + "STATIC_HTML_MOTION_ANCHOR_INCOMPLETE", + "STATIC_HTML_VIEWPORT_CAPTURE_INCOMPLETE", + "STATIC_HTML_VISUAL_DIFF_BLOCKED", + "STATIC_HTML_READY_WITHOUT_EVIDENCE" + ] + }, + "status": { + "enum": ["covered", "blocked", "out_of_scope"] + }, + "source_refs": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1, + "not": { + "pattern": "(delivery/index\\.html|screenshots/|visual-diff|diff-output|evidence-packet\\.md)" + } + } + }, + "asset_record": { + "type": "object", + "required": ["id", "inventory_ref", "html_refs", "local_paths", "source_refs", "status", "blockers"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "inventory_ref": { "type": "string", "minLength": 1 }, + "html_refs": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "local_paths": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "source_refs": { "$ref": "#/$defs/source_refs" }, + "status": { "$ref": "#/$defs/status" }, + "blockers": { "type": "array", "items": { "$ref": "#/$defs/blocker_code" } } + }, + "additionalProperties": true + }, + "page_record": { + "type": "object", + "required": ["id", "route", "html_ref", "state_refs", "layout_refs", "source_refs", "status", "blockers"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "route": { "type": "string", "minLength": 1 }, + "html_ref": { "type": "string", "minLength": 1 }, + "state_refs": { "type": "array", "minItems": 1 }, + "layout_refs": { "type": "array", "minItems": 1 }, + "source_refs": { "$ref": "#/$defs/source_refs" }, + "status": { "$ref": "#/$defs/status" }, + "blockers": { "type": "array", "items": { "$ref": "#/$defs/blocker_code" } } + }, + "additionalProperties": true + }, + "component_record": { + "type": "object", + "required": ["id", "component_model_ref", "html_ref", "state_refs", "operation_refs", "source_refs", "status", "blockers"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "component_model_ref": { "type": "string", "minLength": 1 }, + "html_ref": { "type": "string", "minLength": 1 }, + "state_refs": { "type": "array", "minItems": 1 }, + "operation_refs": { "type": "array" }, + "source_refs": { "$ref": "#/$defs/source_refs" }, + "status": { "$ref": "#/$defs/status" }, + "blockers": { "type": "array", "items": { "$ref": "#/$defs/blocker_code" } } + }, + "additionalProperties": true + }, + "operation_record": { + "type": "object", + "required": [ + "id", + "interaction_model_ref", + "event", + "target_ref", + "result_ref", + "precondition", + "replay_status", + "source_refs", + "blockers" + ], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "interaction_model_ref": { "type": "string", "minLength": 1 }, + "event": { "type": "string", "minLength": 1 }, + "target_ref": { "type": "string", "minLength": 1 }, + "result_ref": { "type": "string", "minLength": 1 }, + "precondition": { "type": "string", "minLength": 1 }, + "replay_status": { "enum": ["pass", "blocked", "not_applicable"] }, + "source_refs": { "$ref": "#/$defs/source_refs" }, + "blockers": { "type": "array", "items": { "$ref": "#/$defs/blocker_code" } } + }, + "additionalProperties": true + }, + "motion_record": { + "type": "object", + "required": [ + "id", + "motion_model_ref", + "trigger_ref", + "affected_ref", + "end_state_ref", + "replay_status", + "source_refs", + "blockers" + ], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "motion_model_ref": { "type": "string", "minLength": 1 }, + "trigger_ref": { "type": "string", "minLength": 1 }, + "affected_ref": { "type": "string", "minLength": 1 }, + "end_state_ref": { "type": "string", "minLength": 1 }, + "replay_status": { "enum": ["pass", "blocked", "not_applicable"] }, + "source_refs": { "$ref": "#/$defs/source_refs" }, + "blockers": { "type": "array", "items": { "$ref": "#/$defs/blocker_code" } } + }, + "additionalProperties": true + }, + "viewport_record": { + "type": "object", + "required": ["id", "width", "height", "page_refs", "screenshot_refs", "render_status", "blockers"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "width": { "type": "integer", "minimum": 1 }, + "height": { "type": "integer", "minimum": 1 }, + "page_refs": { "type": "array", "minItems": 1 }, + "screenshot_refs": { "type": "array", "minItems": 1 }, + "render_status": { "enum": ["pass", "blocked"] }, + "blockers": { "type": "array", "items": { "$ref": "#/$defs/blocker_code" } } + }, + "additionalProperties": true + }, + "visual_diff_record": { + "type": "object", + "required": ["id", "source_ref", "screenshot_ref", "status", "thresholds", "diff_summary", "blockers"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "source_ref": { "type": "string", "minLength": 1 }, + "screenshot_ref": { "type": "string", "minLength": 1 }, + "status": { "enum": ["pass", "blocked"] }, + "thresholds": { "type": "object" }, + "diff_summary": { "type": "object" }, + "blockers": { "type": "array", "items": { "$ref": "#/$defs/blocker_code" } } + }, + "additionalProperties": true + } + }, + "additionalProperties": true +} diff --git a/extensions/intake/templates/schemas/viewport-coverage.schema.json b/extensions/intake/templates/schemas/viewport-coverage.schema.json deleted file mode 100644 index 7cefc16070..0000000000 --- a/extensions/intake/templates/schemas/viewport-coverage.schema.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://spec-kit-intake.local/schemas/viewport-coverage.schema.json", - "title": "Visual Design Viewport Coverage", - "type": "object", - "required": ["ready_gate", "blockers", "viewports"], - "properties": { - "ready_gate": { "enum": ["PASS", "BLOCKED"] }, - "blockers": { - "type": "array", - "items": { "$ref": "#/$defs/blocker_code" } - }, - "viewports": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "id", - "width", - "height", - "covered", - "source_refs", - "visual_spec_refs", - "page_refs", - "screenshot_refs", - "visual_diff_status" - ], - "properties": { - "id": { "type": "string", "minLength": 1 }, - "width": { "type": "integer", "minimum": 1 }, - "height": { "type": "integer", "minimum": 1 }, - "covered": { "type": "boolean" }, - "source_refs": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/source_ref" } - }, - "visual_spec_refs": { - "type": "array", - "minItems": 1, - "items": { "type": "string", "minLength": 1 } - }, - "page_refs": { - "type": "array", - "minItems": 1, - "items": { "type": "string", "minLength": 1 } - }, - "screenshot_refs": { - "type": "array", - "minItems": 1, - "items": { "type": "string", "minLength": 1 } - }, - "visual_diff_status": { "enum": ["pass", "blocked", "not_applicable"] }, - "missing": { - "type": "array", - "items": { - "type": "object", - "required": ["reason", "blocker"], - "properties": { - "reason": { "type": "string", "minLength": 1 }, - "blocker": { "$ref": "#/$defs/blocker_code" } - }, - "additionalProperties": true - } - } - }, - "additionalProperties": true - } - } - }, - "$defs": { - "blocker_code": { - "enum": [ - "VISUAL_PREVIEW_SOURCE_INTAKE_BLOCKED", - "VISUAL_PREVIEW_REQUIRED_ARTIFACT_MISSING", - "VISUAL_PREVIEW_FIGMA_NODE_COVERAGE_INCOMPLETE", - "VISUAL_PREVIEW_COMPONENT_STATE_COVERAGE_INCOMPLETE", - "VISUAL_PREVIEW_PAGE_COVERAGE_INCOMPLETE", - "VISUAL_PREVIEW_ASSET_TRACEABILITY_INCOMPLETE", - "VISUAL_PREVIEW_VIEWPORT_CAPTURE_INCOMPLETE", - "VISUAL_PREVIEW_VISUAL_DIFF_BLOCKED", - "VISUAL_PREVIEW_KNOWN_GAP_UNRESOLVED", - "VISUAL_PREVIEW_SCHEMA_INVALID", - "VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING", - "VISUAL_SPEC_ASSERTION_COVERAGE_INCOMPLETE" - ] - }, - "source_ref": { - "type": "string", - "minLength": 1, - "not": { - "pattern": "(visual-evidence-packet\\.md|figma-evidence-packet\\.md|visual-spec-evidence-packet\\.md|preview\\.html|previews/|screenshots/|visual-diff|diff-output)" - } - } - }, - "additionalProperties": true -} diff --git a/extensions/intake/templates/schemas/visual-requirements.schema.json b/extensions/intake/templates/schemas/visual-requirements.schema.json index feb43b6b52..19b69430ee 100644 --- a/extensions/intake/templates/schemas/visual-requirements.schema.json +++ b/extensions/intake/templates/schemas/visual-requirements.schema.json @@ -251,12 +251,12 @@ "type": "string", "minLength": 1, "not": { - "pattern": "(visual-evidence-packet\\.md|figma-evidence-packet\\.md|visual-spec-evidence-packet\\.md|preview\\.html|previews/|screenshots/|visual-diff|diff-output)" + "pattern": "(visual-evidence-packet\\.md|figma-evidence-packet\\.md|delivery/index\\.html|delivery/|screenshots/|visual-diff|diff-output)" } }, "no_normalization_field_name": { "not": { - "pattern": "(rename|group|sort|normaliz|layout_patch|fixup|dedupe|merge_rule|flatten|postprocess|preview_ref|visual_diff_ref)" + "pattern": "(rename|group|sort|normaliz|layout_patch|fixup|dedupe|merge_rule|flatten|postprocess|visual_diff_ref)" } } }, diff --git a/extensions/intake/templates/schemas/visual-spec-assertions.schema.json b/extensions/intake/templates/schemas/visual-spec-assertions.schema.json deleted file mode 100644 index 9704efe6c5..0000000000 --- a/extensions/intake/templates/schemas/visual-spec-assertions.schema.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://spec-kit-intake.local/schemas/visual-spec-assertions.schema.json", - "title": "Visual Spec Package Assertions", - "type": "object", - "required": [ - "assertions_complete", - "assertion_count", - "ci_assertions_complete", - "assertions" - ], - "properties": { - "assertions_complete": { "type": "boolean" }, - "assertion_count": { "type": "integer", "minimum": 0 }, - "ci_assertions_complete": { "type": "boolean" }, - "blocker_lint_errors": { "type": "array" }, - "assertions": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "id", - "visual_spec_refs", - "assertion_type", - "acceptance_intent", - "expected", - "evidence_refs", - "ci_suitability", - "status", - "blockers" - ], - "properties": { - "id": { - "type": "string", - "pattern": "^VSA-[A-Za-z0-9._:-]+$" - }, - "visual_spec_refs": { - "type": "array", - "minItems": 1, - "items": { "type": "string", "minLength": 1 } - }, - "assertion_type": { - "enum": [ - "visible", - "hidden", - "enabled", - "disabled", - "contains_text", - "role", - "aria", - "token", - "relation", - "state", - "viewport" - ] - }, - "acceptance_intent": { "type": "string", "minLength": 1 }, - "expected": { - "type": ["string", "object", "array", "boolean", "number"], - "minLength": 1 - }, - "evidence_refs": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/source_ref" } - }, - "ci_suitability": { "enum": ["ci_low_cost", "manual_review", "blocked"] }, - "status": { "enum": ["ready", "blocked", "reference_only", "out_of_scope"] }, - "blockers": { - "type": "array", - "items": { "$ref": "#/$defs/blocker_code" } - } - }, - "additionalProperties": true - } - } - }, - "$defs": { - "blocker_code": { - "enum": [ - "VISUAL_SPEC_SOURCE_INTAKE_BLOCKED", - "VISUAL_SPEC_REQUIRED_ARTIFACT_MISSING", - "VISUAL_SPEC_SCHEMA_INVALID", - "VISUAL_SPEC_INTAKE_INCOMPLETE", - "VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING", - "VISUAL_SPEC_PRODUCT_AMBIGUITY_UNRESOLVED", - "VISUAL_SPEC_ASSERTION_COVERAGE_INCOMPLETE", - "VISUAL_SPEC_LOCATOR_STRATEGY_INVALID", - "VISUAL_SPEC_DOWNSTREAM_OWNERSHIP_LEAK", - "VISUAL_SPEC_READY_WITHOUT_EVIDENCE" - ] - }, - "source_ref": { - "type": "string", - "minLength": 1, - "not": { - "pattern": "(visual-evidence-packet\\.md|figma-evidence-packet\\.md|visual-spec-evidence-packet\\.md|preview\\.html|previews/|screenshots/|visual-diff|diff-output)" - } - } - }, - "additionalProperties": true -} diff --git a/extensions/intake/templates/schemas/visual-spec-package.schema.json b/extensions/intake/templates/schemas/visual-spec-package.schema.json deleted file mode 100644 index cea9fe625d..0000000000 --- a/extensions/intake/templates/schemas/visual-spec-package.schema.json +++ /dev/null @@ -1,155 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://spec-kit-intake.local/schemas/visual-spec-package.schema.json", - "title": "Visual Spec Package", - "type": "object", - "required": [ - "visual_spec_package_complete", - "visual_spec_item_count", - "source_refs_complete", - "provider_evidence_complete", - "resources_traceable_to_design_source", - "product_ambiguities_recorded", - "downstream_ownership_free", - "items" - ], - "properties": { - "visual_spec_package_complete": { "type": "boolean" }, - "visual_spec_item_count": { "type": "integer", "minimum": 0 }, - "source_refs_complete": { "type": "boolean" }, - "provider_evidence_complete": { "type": "boolean" }, - "resources_traceable_to_design_source": { "type": "boolean" }, - "product_ambiguities_recorded": { "type": "boolean" }, - "downstream_ownership_free": { "type": "boolean" }, - "product_ambiguities": { "type": "array" }, - "blocker_lint_errors": { "type": "array" }, - "items": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "id", - "source_refs", - "page", - "region", - "role", - "state", - "viewport", - "locator", - "expectations", - "acceptance_intent", - "evidence_type", - "confidence", - "status", - "blockers" - ], - "properties": { - "id": { - "type": "string", - "pattern": "^VS-[A-Za-z0-9._:-]+$" - }, - "source_refs": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/source_ref" } - }, - "des_refs": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "visual_requirement_refs": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "preview_refs": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "page": { "type": "string", "minLength": 1 }, - "region": { "type": "string", "minLength": 1 }, - "role": { "type": "string", "minLength": 1 }, - "state": { "type": "string", "minLength": 1 }, - "viewport": { "type": "string", "minLength": 1 }, - "locator": { - "type": "object", - "required": ["strategy", "value", "implementation_owned"], - "properties": { - "strategy": { - "enum": [ - "role", - "text", - "label", - "aria-label", - "semantic-region", - "source-ref", - "visual-anchor", - "test-id-candidate" - ] - }, - "value": { "type": "string", "minLength": 1 }, - "implementation_owned": { "const": false } - }, - "additionalProperties": true - }, - "expectations": { - "type": "object", - "minProperties": 1, - "properties": { - "dom": { "type": "array", "items": { "type": "string", "minLength": 1 } }, - "aria": { "type": "array", "items": { "type": "string", "minLength": 1 } }, - "design_tokens": { "type": "array" }, - "relations": { "type": "array" }, - "state": { "type": "array", "items": { "type": "string", "minLength": 1 } }, - "content": { "type": "array", "items": { "type": "string", "minLength": 1 } } - }, - "additionalProperties": true - }, - "acceptance_intent": { "type": "string", "minLength": 1 }, - "evidence_type": { - "enum": ["observed", "inferred", "candidate", "unsupported", "missing", "out_of_scope"] - }, - "confidence": { "enum": ["low", "medium", "high"] }, - "status": { "enum": ["ready", "blocked", "reference_only", "out_of_scope"] }, - "blockers": { - "type": "array", - "items": { "$ref": "#/$defs/blocker_code" } - }, - "missing_evidence": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "ambiguity_refs": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - } - }, - "additionalProperties": true - } - } - }, - "$defs": { - "blocker_code": { - "enum": [ - "VISUAL_SPEC_SOURCE_INTAKE_BLOCKED", - "VISUAL_SPEC_REQUIRED_ARTIFACT_MISSING", - "VISUAL_SPEC_SCHEMA_INVALID", - "VISUAL_SPEC_INTAKE_INCOMPLETE", - "VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING", - "VISUAL_SPEC_PRODUCT_AMBIGUITY_UNRESOLVED", - "VISUAL_SPEC_ASSERTION_COVERAGE_INCOMPLETE", - "VISUAL_SPEC_LOCATOR_STRATEGY_INVALID", - "VISUAL_SPEC_DOWNSTREAM_OWNERSHIP_LEAK", - "VISUAL_SPEC_READY_WITHOUT_EVIDENCE" - ] - }, - "source_ref": { - "type": "string", - "minLength": 1, - "not": { - "pattern": "(visual-evidence-packet\\.md|figma-evidence-packet\\.md|visual-spec-evidence-packet\\.md|preview\\.html|previews/|screenshots/|visual-diff|diff-output)" - } - } - }, - "additionalProperties": true -} diff --git a/extensions/intake/tests/test_extension_contract.py b/extensions/intake/tests/test_extension_contract.py index add46a94ef..cf5d8bf8fc 100644 --- a/extensions/intake/tests/test_extension_contract.py +++ b/extensions/intake/tests/test_extension_contract.py @@ -13,8 +13,7 @@ VALIDATOR = ROOT / "scripts" / "python" / "validate_visual_design_intake.py" PRD_VALIDATOR = ROOT / "scripts" / "python" / "validate_prd_intake.py" TEST_CASE_VALIDATOR = ROOT / "scripts" / "python" / "validate_test_cases_intake.py" -VISUAL_PREVIEWS_VALIDATOR = ROOT / "scripts" / "python" / "validate_visual_previews.py" -VISUAL_SPEC_PACKAGE_VALIDATOR = ROOT / "scripts" / "python" / "validate_visual_spec_package.py" +STATIC_HTML_DELIVERY_VALIDATOR = ROOT / "scripts" / "python" / "validate_static_html_delivery.py" FIGMA_METADATA_CAPTURE = ROOT / "scripts" / "python" / "capture_figma_metadata_shards.py" FIGMA_LAYOUT_NORMALIZE = ROOT / "scripts" / "python" / "normalize_figma_layout.py" @@ -416,210 +415,182 @@ def write_image_visual_intake_fixture(intake: Path): write_visual_intake_fixture(intake, "image", "low", "wireframe.png") -def write_visual_previews_fixture(html_dir: Path): - visual_intake = html_dir.parent - write_visual_spec_package_fixture(visual_intake / "visual-spec-package") - html_dir.mkdir(parents=True, exist_ok=True) - screenshots = html_dir / "screenshots" +def write_static_html_delivery_fixture(delivery_dir: Path): + visual_intake = delivery_dir.parent + write_visual_intake_fixture(visual_intake, "figma", "high", "figma-source.txt") + write_figma_metadata_fixture(visual_intake) + ir_dir = visual_intake / "visual-ir" + ir_dir.mkdir(parents=True, exist_ok=True) + delivery_dir.mkdir(parents=True, exist_ok=True) + assets = delivery_dir / "assets" + screenshots = delivery_dir / "screenshots" + assets.mkdir(exist_ok=True) screenshots.mkdir(exist_ok=True) + (assets / "logo.svg").write_text("\n", encoding="utf-8") (screenshots / "home-desktop.png").write_bytes(b"fake-png") - (html_dir / "preview.html").write_text( - '
' - '
' - '
Home
' - '' - "
" - '
IA matrix overview
' - '
' - '
Home default state
' - "
" - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - "" - "
headerdefaultclick Savebutton enabledsubmit action is requesteddefault to submittednoneblocked state remains missing when unsupportedfigma://node/2covered
" - '
' - '
Save button default state
' - "
" - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - '' - "" - "
defaultSave labelbuttonclickenabledpressed visual feedbackdefault to submittedhome pageunsupported states remain missingfigma://node/2covered
" - '
Coverage evidence conclusion
' - "
", + ir_common = "ready_gate: PASS\nblockers: []\n" + (ir_dir / "asset-inventory.yaml").write_text( + ir_common + + "assets:\n" + + " - id: asset-logo\n" + + " source_refs: [figma://node/asset-logo]\n" + + " local_path: ../delivery/assets/logo.svg\n", encoding="utf-8", ) - (html_dir / "component-coverage.yaml").write_text( - "\n".join( - [ - "ready_gate: PASS", - "blockers: []", - "components:", - " - id: component-button", - " source_ref: figma://node/button-set", - " name: Button", - " required_dimensions:", - " size: [md]", - " tone: [primary]", - " state: [default]", - " icon: [none]", - " covered:", - " - size: md", - " tone: primary", - " state: default", - " icon: none", - " source_ref: figma://node/2", - " visual_spec_ref: ../visual-spec-package/visual-spec.yaml#VS-home-save-default", - " preview_ref: preview.html#button-md-primary-default", - " interaction_ref: preview.html#button-md-primary-default-click", - " screenshot_refs:", - " - screenshots/home-desktop.png", - " missing: []", - "", - ] - ), + (ir_dir / "layout-tree.yaml").write_text( + ir_common + + "boxes:\n" + + " - id: box-home\n" + + " source_refs: [figma://node/1]\n", encoding="utf-8", ) - (html_dir / "viewport-coverage.yaml").write_text( - "ready_gate: PASS\n" - "blockers: []\n" - "viewports:\n" - " - id: desktop\n" - " width: 1440\n" - " height: 900\n" - " covered: true\n" - " source_refs:\n" - " - figma://node/1\n" - " visual_spec_refs:\n" - " - ../visual-spec-package/visual-spec.yaml#VS-home-save-default\n" - " page_refs:\n" - " - preview.html#home-page\n" - " screenshot_refs:\n" - " - screenshots/home-desktop.png\n" - " visual_diff_status: pass\n", + (ir_dir / "component-model.yaml").write_text( + ir_common + + "components:\n" + + " - id: cmp-save\n" + + " states: [default, submitted]\n" + + " source_refs: [figma://node/2]\n", encoding="utf-8", ) - (html_dir / "known-gaps.md").write_text("# Known Gaps\n\nNone.\n", encoding="utf-8") - - -def write_visual_spec_package_fixture(package_dir: Path): - visual_intake = package_dir.parent - write_visual_intake_fixture(visual_intake, "figma", "high", "figma-source.txt") - write_figma_metadata_fixture(visual_intake) - package_dir.mkdir(parents=True, exist_ok=True) - - (package_dir / "visual-spec.yaml").write_text( - "\n".join( - [ - "visual_spec_package_complete: true", - "visual_spec_item_count: 1", - "source_refs_complete: true", - "provider_evidence_complete: true", - "resources_traceable_to_design_source: true", - "product_ambiguities_recorded: true", - "downstream_ownership_free: true", - "product_ambiguities: []", - "blocker_lint_errors: []", - "items:", - " - id: VS-home-save-default", - " source_refs:", - " - figma://node/2", - " visual_requirement_refs:", - " - ../visual-requirements.yaml#VR-001", - " preview_refs:", - " - ../previews/preview.html#button-md-primary-default", - " - ../previews/component-coverage.yaml#component-button", - " page: home", - " region: header", - " role: button", - " state: default", - " viewport: desktop", - " locator:", - " strategy: role", - " value: button[name='Save']", - " implementation_owned: false", - " expectations:", - " dom:", - " - button element is present", - " aria:", - " - accessible name is Save", - " design_tokens:", - " - token: color.primary", - " source_ref: figma://variables/color-primary", - " relations:", - " - type: appears-before", - " target: main content", - " acceptance_intent: Save control is semantically discoverable at desktop viewport", - " evidence_type: observed", - " confidence: high", - " status: ready", - " blockers: []", - "", - ] - ), + (ir_dir / "page-route-model.yaml").write_text( + ir_common + + "pages:\n" + + " - id: page-home\n" + + " route: /\n" + + " source_refs: [figma://node/1]\n", + encoding="utf-8", + ) + (ir_dir / "interaction-model.yaml").write_text( + ir_common + + "operations:\n" + + " - id: op-save\n" + + " event: click\n" + + " target_ref: delivery/index.html#save-button\n" + + " result_ref: delivery/index.html#save-result\n", + encoding="utf-8", + ) + (ir_dir / "motion-anchor-model.yaml").write_text( + ir_common + + "motion_anchors:\n" + + " - id: motion-save-feedback\n" + + " trigger_ref: delivery/index.html#save-button\n" + + " affected_ref: delivery/index.html#save-result\n" + + " end_state_ref: delivery/index.html#save-result\n", + encoding="utf-8", + ) + (ir_dir / "clarification-log.yaml").write_text( + ir_common + "questions: []\n", encoding="utf-8", ) - (package_dir / "visual-spec-assertions.yaml").write_text( + (delivery_dir / "index.html").write_text( + '
' + '
' + 'Logo' + '' + 'Saved' + "
" + "
", + encoding="utf-8", + ) + (delivery_dir / "render-replay-report.yaml").write_text( "\n".join( [ - "assertions_complete: true", - "assertion_count: 1", - "ci_assertions_complete: true", - "blocker_lint_errors: []", - "assertions:", - " - id: VSA-home-save-visible", - " visual_spec_refs:", - " - VS-home-save-default", - " assertion_type: visible", - " acceptance_intent: Save control is visible and discoverable", - " expected: true", - " evidence_refs:", - " - visual-spec.yaml#VS-home-save-default", - " ci_suitability: ci_low_cost", - " status: ready", + "ready_gate: PASS", + "blockers: []", + "html_entry: index.html", + "source_intake_ref: ../visual-requirements.yaml", + "visual_ir_refs:", + " - ../visual-ir/asset-inventory.yaml", + " - ../visual-ir/layout-tree.yaml", + " - ../visual-ir/component-model.yaml", + " - ../visual-ir/page-route-model.yaml", + " - ../visual-ir/interaction-model.yaml", + " - ../visual-ir/motion-anchor-model.yaml", + " - ../visual-ir/clarification-log.yaml", + "assets:", + " - id: asset-logo", + " inventory_ref: ../visual-ir/asset-inventory.yaml#asset-logo", + " html_refs: [index.html#asset-logo]", + " local_paths: [assets/logo.svg]", + " source_refs: [figma://node/asset-logo]", + " status: covered", + " blockers: []", + "pages:", + " - id: page-home", + " route: /", + " html_ref: index.html#page-home", + " state_refs: [../visual-ir/page-route-model.yaml#page-home]", + " layout_refs: [../visual-ir/layout-tree.yaml#box-home]", + " source_refs: [figma://node/1]", + " status: covered", + " blockers: []", + "components:", + " - id: cmp-save", + " component_model_ref: ../visual-ir/component-model.yaml#cmp-save", + " html_ref: index.html#cmp-save", + " state_refs: [../visual-ir/component-model.yaml#cmp-save-default]", + " operation_refs: [op-save]", + " source_refs: [figma://node/2]", + " status: covered", " blockers: []", + "operations:", + " - id: op-save", + " interaction_model_ref: ../visual-ir/interaction-model.yaml#op-save", + " event: click", + " target_ref: index.html#save-button", + " result_ref: index.html#save-result", + " precondition: enabled", + " replay_status: pass", + " source_refs: [figma://node/2]", + " blockers: []", + "motion_anchors:", + " - id: motion-save-feedback", + " motion_model_ref: ../visual-ir/motion-anchor-model.yaml#motion-save-feedback", + " trigger_ref: index.html#save-button", + " affected_ref: index.html#save-result", + " end_state_ref: index.html#save-result", + " replay_status: pass", + " source_refs: [figma://prototype/save-feedback]", + " blockers: []", + "viewports:", + " - id: desktop", + " width: 1440", + " height: 900", + " page_refs: [index.html#page-home]", + " screenshot_refs: [screenshots/home-desktop.png]", + " render_status: pass", + " blockers: []", + "visual_diffs:", + " - id: diff-desktop", + " source_ref: figma://node/1", + " screenshot_ref: screenshots/home-desktop.png", + " status: pass", + " thresholds:", + " max_pixel_diff_percent: 0.5", + " diff_summary:", + " pixel_diff_percent: 0", + " blockers: []", + "clarifications:", + " required_question_count: 0", + " answered_required_question_count: 0", + " unanswered_required_question_ids: []", "", ] ), encoding="utf-8", ) - - (package_dir / "visual-spec-evidence-packet.md").write_text( + (delivery_dir / "evidence-packet.md").write_text( "---\n" "ready_gate: PASS\n" "blockers: []\n" "source_ref_count: 1\n" - "extracted_item_count: 1\n" + "extracted_item_count: 6\n" "generated_at: '2026-07-01T00:00:00Z'\n" "---\n" - "# Visual Spec Package Evidence Packet\n", + "# Static HTML Delivery Evidence Packet\n", encoding="utf-8", ) @@ -700,45 +671,37 @@ def test_readme_release_url_matches_extension_version(): assert f"archive/refs/tags/v{version}.zip" in readme -def test_visual_previews_schema_and_validator_paths_are_declared(): +def test_static_html_delivery_schema_and_validator_paths_are_declared(): extension = ROOT / "extension.yml" config = ROOT / "config-template.yml" for document in (extension.read_text(encoding="utf-8-sig"), config.read_text(encoding="utf-8")): - assert "scripts/python/validate_visual_previews.py" in document - assert "templates/intake-visual-previews-contract.md" in document - assert "templates/schemas/component-coverage.schema.json" in document - assert "templates/schemas/viewport-coverage.schema.json" in document + assert "scripts/python/validate_static_html_delivery.py" in document + assert "templates/intake-static-html-delivery-contract.md" in document + assert "templates/schemas/static-html-delivery.schema.json" in document + assert "scripts/python/validate_visual_previews.py" not in document + assert "templates/intake-visual-previews-contract.md" not in document - assert VISUAL_PREVIEWS_VALIDATOR.exists() - assert (ROOT / "templates" / "intake-visual-previews-contract.md").exists() - assert (ROOT / "templates" / "schemas" / "component-coverage.schema.json").exists() - assert (ROOT / "templates" / "schemas" / "viewport-coverage.schema.json").exists() + assert STATIC_HTML_DELIVERY_VALIDATOR.exists() + assert (ROOT / "templates" / "intake-static-html-delivery-contract.md").exists() + assert (ROOT / "templates" / "schemas" / "static-html-delivery.schema.json").exists() -def test_visual_design_command_uses_html_mock_mode_names(): +def test_visual_design_command_is_single_entrypoint(): command = (ROOT / "commands" / "speckit.intake.visual-design.md").read_text(encoding="utf-8") assert "build-previews" not in command assert "validate-previews" not in command - assert "build-html-mock" in command - assert "validate-html-mock" in command + assert "build-html-mock" not in command + assert "validate-html-mock" not in command + assert "Do not expose or require user-facing subcommands" in command -def test_visual_spec_package_schema_and_validator_paths_are_declared(): +def test_visual_spec_package_paths_are_not_declared_as_static_html_authority(): extension = ROOT / "extension.yml" config = ROOT / "config-template.yml" for document in (extension.read_text(encoding="utf-8-sig"), config.read_text(encoding="utf-8")): - assert "scripts/python/validate_visual_spec_package.py" in document - assert "templates/intake-visual-spec-package-contract.md" in document - assert "templates/intake-visual-spec-package-evidence-packet-template.md" in document - assert "templates/schemas/visual-spec-package.schema.json" in document - assert "templates/schemas/visual-spec-assertions.schema.json" in document - - assert VISUAL_SPEC_PACKAGE_VALIDATOR.exists() - assert (ROOT / "templates" / "intake-visual-spec-package-contract.md").exists() - assert (ROOT / "templates" / "intake-visual-spec-package-evidence-packet-template.md").exists() - assert (ROOT / "templates" / "schemas" / "visual-spec-package.schema.json").exists() - assert (ROOT / "templates" / "schemas" / "visual-spec-assertions.schema.json").exists() + assert "visual-spec-package" not in document + assert "validate_visual_spec_package.py" not in document def test_figma_layout_normalization_schema_and_script_paths_are_declared(): @@ -796,20 +759,6 @@ def test_test_case_validator_blocks_missing_directory(): assert "TEST_EVIDENCE_PACKET_MISSING" in result.stdout -def test_visual_spec_package_validator_blocks_missing_directory(): - result = subprocess.run( - [sys.executable, str(VISUAL_SPEC_PACKAGE_VALIDATOR), "missing-dir"], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert result.returncode == 1 - assert "VISUAL_SPEC_SOURCE_INTAKE_BLOCKED" in result.stdout - assert "VISUAL_SPEC_REQUIRED_ARTIFACT_MISSING" in result.stdout - assert "VISUAL_SPEC_READY_WITHOUT_EVIDENCE" in result.stdout - - @pytest.mark.parametrize( ("source_type", "fidelity", "file_name"), [ @@ -1753,7 +1702,7 @@ def test_visual_validator_blocks_helper_artifacts_as_source_refs(): requirements = yaml.safe_load((intake / "visual-requirements.yaml").read_text(encoding="utf-8")) requirements["requirements"][0]["source_refs"] = [ - "previews/preview.html#home-page", + "delivery/index.html#home-page", "source-files/wireframe.png#full", ] (intake / "visual-requirements.yaml").write_text(yaml.safe_dump(requirements), encoding="utf-8") @@ -1770,7 +1719,7 @@ def test_visual_validator_blocks_helper_artifacts_as_source_refs(): assert "VISUAL_SCHEMA_INVALID" in payload["blockers"] assert "VISUAL_REQUIREMENTS_UNTRACEABLE" in payload["blockers"] helper_refs = payload["details"]["visual_requirements"]["supporting_artifact_source_refs"] - assert helper_refs[0]["refs"] == ["previews/preview.html#home-page"] + assert helper_refs[0]["refs"] == ["delivery/index.html#home-page"] shutil.rmtree(work_dir) @@ -2041,7 +1990,7 @@ def test_visual_validator_blocks_downstream_fields_in_figma_normalized_tree(): write_figma_metadata_fixture(intake) normalized = yaml.safe_load((intake / "figma-normalized-tree.yaml").read_text(encoding="utf-8")) - normalized["nodes"][0]["preview_ref"] = "previews/preview.html#root" + normalized["nodes"][0]["delivery_ref"] = "delivery/index.html#root" normalized["nodes"][0]["code_component"] = "RootView" (intake / "figma-normalized-tree.yaml").write_text(yaml.safe_dump(normalized), encoding="utf-8") @@ -2176,45 +2125,45 @@ def test_validator_blocks_legacy_figma_only_without_manifest(): shutil.rmtree(work_dir) -def test_visual_previews_validator_passes_complete_minimal_bundle(): - work_dir = ROOT / ".tmp" / "test-visual-previews-validator-pass" +def test_static_html_delivery_validator_passes_complete_minimal_bundle(): + work_dir = ROOT / ".tmp" / "test-static-html-delivery-validator-pass" if work_dir.exists(): shutil.rmtree(work_dir) - html_dir = work_dir / "visual-design" / "previews" - write_visual_previews_fixture(html_dir) + delivery_dir = work_dir / "visual-design" / "delivery" + write_static_html_delivery_fixture(delivery_dir) result = subprocess.run( - [sys.executable, str(VISUAL_PREVIEWS_VALIDATOR), str(html_dir)], + [sys.executable, str(STATIC_HTML_DELIVERY_VALIDATOR), str(delivery_dir)], cwd=ROOT, text=True, capture_output=True, ) assert result.returncode == 0, result.stdout + result.stderr - assert "HTML mock readiness: PASS" in result.stdout + assert "Static HTML delivery readiness: PASS" in result.stdout shutil.rmtree(work_dir) -def test_visual_previews_validator_blocks_missing_directory(): +def test_static_html_delivery_validator_blocks_missing_directory(): result = subprocess.run( - [sys.executable, str(VISUAL_PREVIEWS_VALIDATOR), "missing-dir"], + [sys.executable, str(STATIC_HTML_DELIVERY_VALIDATOR), "missing-dir"], cwd=ROOT, text=True, capture_output=True, ) assert result.returncode == 1 - assert "VISUAL_PREVIEW_REQUIRED_ARTIFACT_MISSING" in result.stdout + assert "STATIC_HTML_REQUIRED_ARTIFACT_MISSING" in result.stdout -def test_visual_previews_validator_blocks_source_intake_blocked(): - work_dir = ROOT / ".tmp" / "test-visual-previews-source-blocked" +def test_static_html_delivery_validator_blocks_source_intake_blocked(): + work_dir = ROOT / ".tmp" / "test-static-html-delivery-source-blocked" if work_dir.exists(): shutil.rmtree(work_dir) - html_dir = work_dir / "visual-design" / "previews" - write_visual_previews_fixture(html_dir) - packet = html_dir.parent / "visual-evidence-packet.md" + delivery_dir = work_dir / "visual-design" / "delivery" + write_static_html_delivery_fixture(delivery_dir) + packet = delivery_dir.parent / "visual-evidence-packet.md" packet.write_text( "---\n" "ready_gate: BLOCKED\n" @@ -2227,7 +2176,7 @@ def test_visual_previews_validator_blocks_source_intake_blocked(): ) result = subprocess.run( - [sys.executable, str(VISUAL_PREVIEWS_VALIDATOR), "--json", str(html_dir)], + [sys.executable, str(STATIC_HTML_DELIVERY_VALIDATOR), "--json", str(delivery_dir)], cwd=ROOT, text=True, capture_output=True, @@ -2235,23 +2184,23 @@ def test_visual_previews_validator_blocks_source_intake_blocked(): payload = json.loads(result.stdout) assert result.returncode == 1 - assert "VISUAL_PREVIEW_SOURCE_INTAKE_BLOCKED" in payload["blockers"] + assert "STATIC_HTML_SOURCE_INTAKE_BLOCKED" in payload["blockers"] shutil.rmtree(work_dir) -def test_visual_previews_validator_reports_schema_errors_in_json(): - work_dir = ROOT / ".tmp" / "test-visual-previews-schema-error" +def test_static_html_delivery_validator_reports_schema_errors_in_json(): + work_dir = ROOT / ".tmp" / "test-static-html-delivery-schema-error" if work_dir.exists(): shutil.rmtree(work_dir) - html_dir = work_dir / "visual-design" / "previews" - write_visual_previews_fixture(html_dir) - component_coverage = yaml.safe_load((html_dir / "component-coverage.yaml").read_text(encoding="utf-8")) - component_coverage["components"][0]["covered"][0].pop("preview_ref") - (html_dir / "component-coverage.yaml").write_text(yaml.safe_dump(component_coverage), encoding="utf-8") + delivery_dir = work_dir / "visual-design" / "delivery" + write_static_html_delivery_fixture(delivery_dir) + report = yaml.safe_load((delivery_dir / "render-replay-report.yaml").read_text(encoding="utf-8")) + report["operations"][0].pop("target_ref") + (delivery_dir / "render-replay-report.yaml").write_text(yaml.safe_dump(report), encoding="utf-8") result = subprocess.run( - [sys.executable, str(VISUAL_PREVIEWS_VALIDATOR), "--json", str(html_dir)], + [sys.executable, str(STATIC_HTML_DELIVERY_VALIDATOR), "--json", str(delivery_dir)], cwd=ROOT, text=True, capture_output=True, @@ -2259,44 +2208,8 @@ def test_visual_previews_validator_reports_schema_errors_in_json(): payload = json.loads(result.stdout) assert result.returncode == 1 - assert "VISUAL_PREVIEW_SCHEMA_INVALID" in payload["blockers"] - assert payload["details"]["schema_validation"]["component_coverage"]["valid"] is False - - shutil.rmtree(work_dir) - - -def test_visual_previews_validator_blocks_helper_artifacts_as_source_refs(): - work_dir = ROOT / ".tmp" / "test-visual-previews-helper-source-refs" - if work_dir.exists(): - shutil.rmtree(work_dir) - html_dir = work_dir / "visual-design" / "previews" - write_visual_previews_fixture(html_dir) - - component_coverage = yaml.safe_load((html_dir / "component-coverage.yaml").read_text(encoding="utf-8")) - component_coverage["components"][0]["source_ref"] = "preview.html#button-md-primary-default" - component_coverage["components"][0]["covered"][0]["source_ref"] = "screenshots/home-desktop.png" - (html_dir / "component-coverage.yaml").write_text(yaml.safe_dump(component_coverage), encoding="utf-8") - - viewport_coverage = yaml.safe_load((html_dir / "viewport-coverage.yaml").read_text(encoding="utf-8")) - viewport_coverage["viewports"][0]["source_refs"] = [ - "previews/preview.html#home-page", - "figma://node/1", - ] - (html_dir / "viewport-coverage.yaml").write_text(yaml.safe_dump(viewport_coverage), encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(VISUAL_PREVIEWS_VALIDATOR), "--json", str(html_dir)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "VISUAL_PREVIEW_SCHEMA_INVALID" in payload["blockers"] - assert "VISUAL_PREVIEW_ASSET_TRACEABILITY_INCOMPLETE" in payload["blockers"] - assert payload["details"]["component_coverage"]["supporting_artifact_source_refs"] - assert payload["details"]["viewport_coverage"]["supporting_artifact_source_refs"] + assert "STATIC_HTML_SCHEMA_INVALID" in payload["blockers"] + assert payload["details"]["schema_validation"]["static_html_delivery"]["valid"] is False shutil.rmtree(work_dir) @@ -2304,455 +2217,85 @@ def test_visual_previews_validator_blocks_helper_artifacts_as_source_refs(): @pytest.mark.parametrize( ("edit_kind", "expected_blocker"), [ - ("missing_selector", "VISUAL_PREVIEW_FIGMA_NODE_COVERAGE_INCOMPLETE"), - ("wrong_preview_file_ref", "VISUAL_PREVIEW_FIGMA_NODE_COVERAGE_INCOMPLETE"), - ("missing_mock_page", "VISUAL_PREVIEW_IA_MATRIX_INCOMPLETE"), - ("component_ref_to_matrix", "VISUAL_PREVIEW_FIGMA_NODE_COVERAGE_INCOMPLETE"), - ("ia_matrix", "VISUAL_PREVIEW_IA_MATRIX_INCOMPLETE"), - ("incomplete_ia_row", "VISUAL_PREVIEW_IA_MATRIX_INCOMPLETE"), - ("duplicate_anchor", "VISUAL_PREVIEW_IA_MATRIX_INCOMPLETE"), - ("interaction_ref", "VISUAL_PREVIEW_IA_MATRIX_INCOMPLETE"), - ("missing_visual_spec_ref", "VISUAL_PREVIEW_COMPONENT_STATE_COVERAGE_INCOMPLETE"), - ("wrong_visual_spec_file_ref", "VISUAL_PREVIEW_COMPONENT_STATE_COVERAGE_INCOMPLETE"), - ("visual_spec_package_blocked", "VISUAL_PREVIEW_COMPONENT_STATE_COVERAGE_INCOMPLETE"), - ("component_state", "VISUAL_PREVIEW_COMPONENT_STATE_COVERAGE_INCOMPLETE"), - ("missing_component_screenshot", "VISUAL_PREVIEW_VIEWPORT_CAPTURE_INCOMPLETE"), - ("component_screenshot_directory", "VISUAL_PREVIEW_VIEWPORT_CAPTURE_INCOMPLETE"), - ("component_screenshot_outside_preview_dir", "VISUAL_PREVIEW_VIEWPORT_CAPTURE_INCOMPLETE"), - ("page", "VISUAL_PREVIEW_PAGE_COVERAGE_INCOMPLETE"), - ("wrong_page_file_ref", "VISUAL_PREVIEW_PAGE_COVERAGE_INCOMPLETE"), - ("page_ref_to_matrix", "VISUAL_PREVIEW_PAGE_COVERAGE_INCOMPLETE"), - ("viewport_visual_spec_ref", "VISUAL_PREVIEW_PAGE_COVERAGE_INCOMPLETE"), - ("viewport_wrong_visual_spec_file_ref", "VISUAL_PREVIEW_PAGE_COVERAGE_INCOMPLETE"), - ("asset", "VISUAL_PREVIEW_ASSET_TRACEABILITY_INCOMPLETE"), - ("viewport", "VISUAL_PREVIEW_VIEWPORT_CAPTURE_INCOMPLETE"), - ("visual_diff", "VISUAL_PREVIEW_VISUAL_DIFF_BLOCKED"), - ("known_gap", "VISUAL_PREVIEW_KNOWN_GAP_UNRESOLVED"), + ("missing_html_ref", "STATIC_HTML_REQUIRED_ARTIFACT_MISSING"), + ("typed_anchor", "STATIC_HTML_REQUIRED_ARTIFACT_MISSING"), + ("source_intake_ref", "STATIC_HTML_SOURCE_INTAKE_BLOCKED"), + ("visual_ir_refs", "STATIC_HTML_IR_BLOCKED"), + ("ir_ref", "STATIC_HTML_IR_BLOCKED"), + ("missing_asset", "STATIC_HTML_ASSET_INCOMPLETE"), + ("blocked_ir", "STATIC_HTML_IR_BLOCKED"), + ("clarification", "STATIC_HTML_CLARIFICATION_REQUIRED"), + ("operation", "STATIC_HTML_OPERATION_REPLAY_INCOMPLETE"), + ("motion", "STATIC_HTML_MOTION_ANCHOR_INCOMPLETE"), + ("viewport", "STATIC_HTML_VIEWPORT_CAPTURE_INCOMPLETE"), + ("visual_diff", "STATIC_HTML_VISUAL_DIFF_BLOCKED"), + ("evidence_packet", "STATIC_HTML_READY_WITHOUT_EVIDENCE"), ], ) -def test_visual_previews_validator_blocks_incomplete_coverage(edit_kind, expected_blocker): - work_dir = ROOT / ".tmp" / f"test-visual-previews-{edit_kind}" +def test_static_html_delivery_validator_blocks_incomplete_delivery(edit_kind, expected_blocker): + work_dir = ROOT / ".tmp" / f"test-static-html-delivery-{edit_kind}" if work_dir.exists(): shutil.rmtree(work_dir) - html_dir = work_dir / "visual-design" / "previews" - write_visual_previews_fixture(html_dir) - - if edit_kind == "missing_selector": - html = (html_dir / "preview.html").read_text(encoding="utf-8") - (html_dir / "preview.html").write_text( - html.replace('id="button-md-primary-default"', "").replace( - 'data-preview-id="button-md-primary-default"', "" - ), + delivery_dir = work_dir / "visual-design" / "delivery" + write_static_html_delivery_fixture(delivery_dir) + report_path = delivery_dir / "render-replay-report.yaml" + report = yaml.safe_load(report_path.read_text(encoding="utf-8")) + + if edit_kind == "missing_html_ref": + report["components"][0]["html_ref"] = "index.html#missing-component" + elif edit_kind == "typed_anchor": + report["components"][0]["html_ref"] = "index.html#page-home" + elif edit_kind == "source_intake_ref": + report["source_intake_ref"] = "../missing-visual-requirements.yaml" + elif edit_kind == "visual_ir_refs": + report["visual_ir_refs"] = report["visual_ir_refs"][:-1] + elif edit_kind == "ir_ref": + report["pages"][0]["layout_refs"] = ["../visual-ir/layout-tree.yaml#missing-box"] + elif edit_kind == "missing_asset": + report["assets"][0]["local_paths"] = ["assets/missing.svg"] + elif edit_kind == "blocked_ir": + ir_file = delivery_dir.parent / "visual-ir" / "component-model.yaml" + ir_file.write_text("ready_gate: BLOCKED\nblockers: [STATIC_HTML_COMPONENT_STATE_INCOMPLETE]\n", encoding="utf-8") + elif edit_kind == "clarification": + clarification = delivery_dir.parent / "visual-ir" / "clarification-log.yaml" + clarification.write_text( + "ready_gate: PASS\n" + "blockers: []\n" + "questions:\n" + " - id: CQ-001\n" + " required_for_html: true\n" + " status: unanswered\n", encoding="utf-8", ) - elif edit_kind == "wrong_preview_file_ref": - component_coverage = yaml.safe_load((html_dir / "component-coverage.yaml").read_text(encoding="utf-8")) - component_coverage["components"][0]["covered"][0]["preview_ref"] = ( - "wrong-preview.html#button-md-primary-default" - ) - (html_dir / "component-coverage.yaml").write_text(yaml.safe_dump(component_coverage), encoding="utf-8") - elif edit_kind == "missing_mock_page": - html = (html_dir / "preview.html").read_text(encoding="utf-8") - (html_dir / "preview.html").write_text( - html.replace('data-preview-section="mock-page"', ""), - encoding="utf-8", - ) - elif edit_kind == "component_ref_to_matrix": - component_coverage = yaml.safe_load((html_dir / "component-coverage.yaml").read_text(encoding="utf-8")) - component_coverage["components"][0]["covered"][0]["preview_ref"] = "preview.html#button-ia" - (html_dir / "component-coverage.yaml").write_text(yaml.safe_dump(component_coverage), encoding="utf-8") - elif edit_kind == "ia_matrix": - html = (html_dir / "preview.html").read_text(encoding="utf-8") - (html_dir / "preview.html").write_text( - html.replace('data-preview-section="component-ia-matrix"', ""), - encoding="utf-8", - ) - elif edit_kind == "incomplete_ia_row": - html = (html_dir / "preview.html").read_text(encoding="utf-8") - (html_dir / "preview.html").write_text( - html.replace( - "" - '
footer' - '
", '
Duplicate
'), - encoding="utf-8", - ) - elif edit_kind == "interaction_ref": - html = (html_dir / "preview.html").read_text(encoding="utf-8") - (html_dir / "preview.html").write_text( - html.replace('data-interaction-id="button-md-primary-default-click"', ""), - encoding="utf-8", - ) - elif edit_kind == "missing_visual_spec_ref": - component_coverage = yaml.safe_load((html_dir / "component-coverage.yaml").read_text(encoding="utf-8")) - component_coverage["components"][0]["covered"][0]["visual_spec_ref"] = ( - "../visual-spec-package/visual-spec.yaml#VS-missing" - ) - (html_dir / "component-coverage.yaml").write_text(yaml.safe_dump(component_coverage), encoding="utf-8") - elif edit_kind == "wrong_visual_spec_file_ref": - component_coverage = yaml.safe_load((html_dir / "component-coverage.yaml").read_text(encoding="utf-8")) - component_coverage["components"][0]["covered"][0]["visual_spec_ref"] = ( - "../visual-spec-package/other.yaml#VS-home-save-default" - ) - (html_dir / "component-coverage.yaml").write_text(yaml.safe_dump(component_coverage), encoding="utf-8") - elif edit_kind == "visual_spec_package_blocked": - (html_dir.parent / "visual-spec-package" / "visual-spec-assertions.yaml").unlink() - elif edit_kind == "component_state": - component_coverage = yaml.safe_load((html_dir / "component-coverage.yaml").read_text(encoding="utf-8")) - component_coverage["components"][0]["missing"].append( - { - "missing_type": "state", - "state": "loading", - "reason": "Missing Figma source state", - "blocker": "VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING", - } - ) - (html_dir / "component-coverage.yaml").write_text(yaml.safe_dump(component_coverage), encoding="utf-8") - elif edit_kind == "missing_component_screenshot": - component_coverage = yaml.safe_load((html_dir / "component-coverage.yaml").read_text(encoding="utf-8")) - component_coverage["components"][0]["covered"][0]["screenshot_refs"] = ["screenshots/missing.png"] - (html_dir / "component-coverage.yaml").write_text(yaml.safe_dump(component_coverage), encoding="utf-8") - elif edit_kind == "component_screenshot_directory": - component_coverage = yaml.safe_load((html_dir / "component-coverage.yaml").read_text(encoding="utf-8")) - component_coverage["components"][0]["covered"][0]["screenshot_refs"] = ["screenshots"] - (html_dir / "component-coverage.yaml").write_text(yaml.safe_dump(component_coverage), encoding="utf-8") - elif edit_kind == "component_screenshot_outside_preview_dir": - outside = html_dir.parent / "outside.png" - outside.write_bytes(b"fake-png") - component_coverage = yaml.safe_load((html_dir / "component-coverage.yaml").read_text(encoding="utf-8")) - component_coverage["components"][0]["covered"][0]["screenshot_refs"] = ["../outside.png"] - (html_dir / "component-coverage.yaml").write_text(yaml.safe_dump(component_coverage), encoding="utf-8") - elif edit_kind == "page": - viewport_coverage = yaml.safe_load((html_dir / "viewport-coverage.yaml").read_text(encoding="utf-8")) - viewport_coverage["viewports"][0]["page_refs"] = [] - (html_dir / "viewport-coverage.yaml").write_text(yaml.safe_dump(viewport_coverage), encoding="utf-8") - elif edit_kind == "wrong_page_file_ref": - viewport_coverage = yaml.safe_load((html_dir / "viewport-coverage.yaml").read_text(encoding="utf-8")) - viewport_coverage["viewports"][0]["page_refs"] = ["wrong-preview.html#home-page"] - (html_dir / "viewport-coverage.yaml").write_text(yaml.safe_dump(viewport_coverage), encoding="utf-8") - elif edit_kind == "page_ref_to_matrix": - viewport_coverage = yaml.safe_load((html_dir / "viewport-coverage.yaml").read_text(encoding="utf-8")) - viewport_coverage["viewports"][0]["page_refs"] = ["preview.html#home-page-ia"] - (html_dir / "viewport-coverage.yaml").write_text(yaml.safe_dump(viewport_coverage), encoding="utf-8") - elif edit_kind == "viewport_visual_spec_ref": - viewport_coverage = yaml.safe_load((html_dir / "viewport-coverage.yaml").read_text(encoding="utf-8")) - viewport_coverage["viewports"][0]["visual_spec_refs"] = [ - "../visual-spec-package/visual-spec.yaml#VS-missing" - ] - (html_dir / "viewport-coverage.yaml").write_text(yaml.safe_dump(viewport_coverage), encoding="utf-8") - elif edit_kind == "viewport_wrong_visual_spec_file_ref": - viewport_coverage = yaml.safe_load((html_dir / "viewport-coverage.yaml").read_text(encoding="utf-8")) - viewport_coverage["viewports"][0]["visual_spec_refs"] = [ - "../visual-spec-package/other.yaml#VS-home-save-default" - ] - (html_dir / "viewport-coverage.yaml").write_text(yaml.safe_dump(viewport_coverage), encoding="utf-8") - elif edit_kind == "asset": - component_coverage = yaml.safe_load((html_dir / "component-coverage.yaml").read_text(encoding="utf-8")) - component_coverage["components"][0]["missing"].append( - { - "missing_type": "resource", - "resource": "logo", - "reason": "Missing Figma image resource", - "blocker": "VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING", - } - ) - (html_dir / "component-coverage.yaml").write_text(yaml.safe_dump(component_coverage), encoding="utf-8") + report["clarifications"]["required_question_count"] = 1 + report["clarifications"]["unanswered_required_question_ids"] = ["CQ-001"] + elif edit_kind == "operation": + report["operations"][0]["replay_status"] = "blocked" + report["operations"][0]["blockers"] = ["STATIC_HTML_OPERATION_REPLAY_INCOMPLETE"] + elif edit_kind == "motion": + report["motion_anchors"][0]["replay_status"] = "blocked" + report["motion_anchors"][0]["blockers"] = ["STATIC_HTML_MOTION_ANCHOR_INCOMPLETE"] elif edit_kind == "viewport": - shutil.rmtree(html_dir / "screenshots") - (html_dir / "screenshots").mkdir() + report["viewports"][0]["screenshot_refs"] = ["screenshots/missing.png"] elif edit_kind == "visual_diff": - viewport_coverage = yaml.safe_load((html_dir / "viewport-coverage.yaml").read_text(encoding="utf-8")) - viewport_coverage["viewports"][0]["visual_diff_status"] = "blocked" - (html_dir / "viewport-coverage.yaml").write_text(yaml.safe_dump(viewport_coverage), encoding="utf-8") - elif edit_kind == "known_gap": - (html_dir / "known-gaps.md").write_text("# Known Gaps\n\nBLOCKED: missing mobile state.\n", encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(VISUAL_PREVIEWS_VALIDATOR), "--json", str(html_dir)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert expected_blocker in payload["blockers"] - - shutil.rmtree(work_dir) - - -def test_visual_spec_package_validator_passes_complete_minimal_bundle(): - work_dir = ROOT / ".tmp" / "test-visual-spec-package-validator-pass" - if work_dir.exists(): - shutil.rmtree(work_dir) - package_dir = work_dir / "visual-design" / "visual-spec-package" - write_visual_spec_package_fixture(package_dir) - - result = subprocess.run( - [sys.executable, str(VISUAL_SPEC_PACKAGE_VALIDATOR), str(package_dir)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert result.returncode == 0, result.stdout + result.stderr - assert "Visual Spec Package intake readiness: PASS" in result.stdout - - shutil.rmtree(work_dir) - - -def test_visual_spec_package_validator_ignores_preview_gate_status(): - work_dir = ROOT / ".tmp" / "test-visual-spec-package-ignores-preview-gate" - if work_dir.exists(): - shutil.rmtree(work_dir) - package_dir = work_dir / "visual-design" / "visual-spec-package" - write_visual_spec_package_fixture(package_dir) - previews_dir = package_dir.parent / "previews" - previews_dir.mkdir(parents=True) - (previews_dir / "component-coverage.yaml").write_text( - "ready_gate: BLOCKED\n" - "blockers: [VISUAL_PREVIEW_KNOWN_GAP_UNRESOLVED]\n" - "components: []\n", - encoding="utf-8", - ) - - result = subprocess.run( - [sys.executable, str(VISUAL_SPEC_PACKAGE_VALIDATOR), "--json", str(package_dir)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 0, result.stdout + result.stderr - assert payload["status"] == "PASS" - assert not any(code.startswith("VISUAL_PREVIEW_") for code in payload["blockers"]) - - shutil.rmtree(work_dir) - - -def test_visual_spec_package_validator_blocks_source_intake_blocked(): - work_dir = ROOT / ".tmp" / "test-visual-spec-package-source-blocked" - if work_dir.exists(): - shutil.rmtree(work_dir) - package_dir = work_dir / "visual-design" / "visual-spec-package" - write_visual_spec_package_fixture(package_dir) - packet = package_dir.parent / "visual-evidence-packet.md" - packet.write_text( - "---\n" - "ready_gate: BLOCKED\n" - "blockers: [VISUAL_REQUIREMENTS_MISSING]\n" - "source_ref_count: 1\n" - "extracted_item_count: 0\n" - "generated_at: '2026-07-01T00:00:00Z'\n" - "---\n", - encoding="utf-8", - ) - - result = subprocess.run( - [sys.executable, str(VISUAL_SPEC_PACKAGE_VALIDATOR), "--json", str(package_dir)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "VISUAL_SPEC_SOURCE_INTAKE_BLOCKED" in payload["blockers"] - assert "VISUAL_SPEC_READY_WITHOUT_EVIDENCE" in payload["blockers"] - - shutil.rmtree(work_dir) - - -def test_visual_spec_package_validator_blocks_helper_artifacts_as_fact_refs(): - work_dir = ROOT / ".tmp" / "test-visual-spec-package-helper-fact-refs" - if work_dir.exists(): - shutil.rmtree(work_dir) - package_dir = work_dir / "visual-design" / "visual-spec-package" - write_visual_spec_package_fixture(package_dir) - - package_doc = yaml.safe_load((package_dir / "visual-spec.yaml").read_text(encoding="utf-8")) - package_doc["items"][0]["source_refs"] = [ - "../previews/preview.html#button-md-primary-default", - "figma://node/2", - ] - (package_dir / "visual-spec.yaml").write_text(yaml.safe_dump(package_doc), encoding="utf-8") - - assertions_doc = yaml.safe_load((package_dir / "visual-spec-assertions.yaml").read_text(encoding="utf-8")) - assertions_doc["assertions"][0]["evidence_refs"] = [ - "visual-spec-evidence-packet.md#summary", - "figma://node/2", - ] - (package_dir / "visual-spec-assertions.yaml").write_text(yaml.safe_dump(assertions_doc), encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(VISUAL_SPEC_PACKAGE_VALIDATOR), "--json", str(package_dir)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "VISUAL_SPEC_SCHEMA_INVALID" in payload["blockers"] - assert "VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING" in payload["blockers"] - assert "VS-home-save-default" in payload["details"]["visual_spec_package"]["provider_evidence_gaps"] - assert "VSA-home-save-visible" in payload["details"]["visual_spec_assertions"]["provider_evidence_gaps"] - - shutil.rmtree(work_dir) - - -def test_visual_spec_package_validator_reports_schema_errors_in_json(): - work_dir = ROOT / ".tmp" / "test-visual-spec-package-schema-error" - if work_dir.exists(): - shutil.rmtree(work_dir) - package_dir = work_dir / "visual-design" / "visual-spec-package" - write_visual_spec_package_fixture(package_dir) - text = (package_dir / "visual-spec.yaml").read_text(encoding="utf-8") - (package_dir / "visual-spec.yaml").write_text( - text.replace(" role: button\n", ""), - encoding="utf-8", - ) - - result = subprocess.run( - [sys.executable, str(VISUAL_SPEC_PACKAGE_VALIDATOR), "--json", str(package_dir)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "VISUAL_SPEC_SCHEMA_INVALID" in payload["blockers"] - assert payload["details"]["schema_validation"]["visual_spec_package"]["valid"] is False - - shutil.rmtree(work_dir) - - -@pytest.mark.parametrize( - ("artifact", "detail_key"), - [ - ("visual-spec.yaml", "visual_spec_package"), - ("visual-spec-assertions.yaml", "visual_spec_assertions"), - ], -) -def test_visual_spec_package_validator_rejects_unknown_blocker_codes(artifact, detail_key): - work_dir = ROOT / ".tmp" / f"test-visual-spec-package-unknown-blocker-{detail_key}" - if work_dir.exists(): - shutil.rmtree(work_dir) - package_dir = work_dir / "visual-design" / "visual-spec-package" - write_visual_spec_package_fixture(package_dir) - - path = package_dir / artifact - text = path.read_text(encoding="utf-8") - path.write_text(text.replace(" blockers: []", " blockers: [NOT_A_BLOCKER]", 1), encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(VISUAL_SPEC_PACKAGE_VALIDATOR), "--json", str(package_dir)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "VISUAL_SPEC_SCHEMA_INVALID" in payload["blockers"] - assert payload["details"]["schema_validation"][detail_key]["valid"] is False - - shutil.rmtree(work_dir) - - -@pytest.mark.parametrize( - ("edit_kind", "expected_blocker"), - [ - ("provider_evidence", "VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING"), - ("provider_evidence_blocker", "VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING"), - ("product_ambiguity", "VISUAL_SPEC_PRODUCT_AMBIGUITY_UNRESOLVED"), - ("locator", "VISUAL_SPEC_LOCATOR_STRATEGY_INVALID"), - ("ownership", "VISUAL_SPEC_DOWNSTREAM_OWNERSHIP_LEAK"), - ("assertion_coverage", "VISUAL_SPEC_ASSERTION_COVERAGE_INCOMPLETE"), - ("assertion_blocker", "VISUAL_SPEC_ASSERTION_COVERAGE_INCOMPLETE"), - ("cross_ref", "VISUAL_SPEC_ASSERTION_COVERAGE_INCOMPLETE"), - ("evidence_packet", "VISUAL_SPEC_READY_WITHOUT_EVIDENCE"), - ], -) -def test_visual_spec_package_validator_blocks_readiness_failures(edit_kind, expected_blocker): - work_dir = ROOT / ".tmp" / f"test-visual-spec-package-{edit_kind}" - if work_dir.exists(): - shutil.rmtree(work_dir) - package_dir = work_dir / "visual-design" / "visual-spec-package" - write_visual_spec_package_fixture(package_dir) - - if edit_kind == "provider_evidence": - text = (package_dir / "visual-spec.yaml").read_text(encoding="utf-8") - (package_dir / "visual-spec.yaml").write_text( - text.replace("provider_evidence_complete: true", "provider_evidence_complete: false"), - encoding="utf-8", - ) - elif edit_kind == "provider_evidence_blocker": - text = (package_dir / "visual-spec.yaml").read_text(encoding="utf-8") - (package_dir / "visual-spec.yaml").write_text( - text.replace(" blockers: []", " blockers: [VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING]", 1), - encoding="utf-8", - ) - elif edit_kind == "product_ambiguity": - text = (package_dir / "visual-spec.yaml").read_text(encoding="utf-8") - (package_dir / "visual-spec.yaml").write_text( - text.replace("product_ambiguities: []", "product_ambiguities:\n - Save disabled conditions are not specified."), - encoding="utf-8", - ) - elif edit_kind == "locator": - text = (package_dir / "visual-spec.yaml").read_text(encoding="utf-8") - (package_dir / "visual-spec.yaml").write_text( - text.replace(" value: button[name='Save']", " value: '#save-button'"), - encoding="utf-8", - ) - elif edit_kind == "ownership": - text = (package_dir / "visual-spec.yaml").read_text(encoding="utf-8") - (package_dir / "visual-spec.yaml").write_text( - text.replace(" blockers: []", " blockers: []\n code_component: SaveButton", 1), - encoding="utf-8", - ) - elif edit_kind == "assertion_coverage": - text = (package_dir / "visual-spec-assertions.yaml").read_text(encoding="utf-8") - (package_dir / "visual-spec-assertions.yaml").write_text( - text.replace(" ci_suitability: ci_low_cost", " ci_suitability: manual_review"), - encoding="utf-8", - ) - elif edit_kind == "assertion_blocker": - text = (package_dir / "visual-spec-assertions.yaml").read_text(encoding="utf-8") - (package_dir / "visual-spec-assertions.yaml").write_text( - text.replace(" blockers: []", " blockers: [VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING]", 1), - encoding="utf-8", - ) - elif edit_kind == "cross_ref": - text = (package_dir / "visual-spec-assertions.yaml").read_text(encoding="utf-8") - (package_dir / "visual-spec-assertions.yaml").write_text( - text.replace(" - VS-home-save-default", " - VS-missing"), - encoding="utf-8", - ) + report["visual_diffs"][0]["status"] = "blocked" + report["visual_diffs"][0]["blockers"] = ["STATIC_HTML_VISUAL_DIFF_BLOCKED"] elif edit_kind == "evidence_packet": - (package_dir / "visual-spec-evidence-packet.md").write_text( + (delivery_dir / "evidence-packet.md").write_text( "---\n" "ready_gate: BLOCKED\n" - "blockers: [VISUAL_SPEC_PROVIDER_EVIDENCE_MISSING]\n" + "blockers: [STATIC_HTML_READY_WITHOUT_EVIDENCE]\n" "source_ref_count: 1\n" - "extracted_item_count: 1\n" + "extracted_item_count: 0\n" "generated_at: '2026-07-01T00:00:00Z'\n" "---\n", encoding="utf-8", ) + report_path.write_text(yaml.safe_dump(report, sort_keys=False), encoding="utf-8") + result = subprocess.run( - [sys.executable, str(VISUAL_SPEC_PACKAGE_VALIDATOR), "--json", str(package_dir)], + [sys.executable, str(STATIC_HTML_DELIVERY_VALIDATOR), "--json", str(delivery_dir)], cwd=ROOT, text=True, capture_output=True,