From 91ccc042ddcace878e325e8aee7878dec72ccbbe Mon Sep 17 00:00:00 2001 From: bigsmartben <30429295+bigsmartben@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:08:05 +0800 Subject: [PATCH] feat: retire obsolete bundled extensions Remove inception, intake, and repository-governance from bundled distribution, make discovery and preview opt-in, and preserve existing project files during upgrades. Closes #85 Assisted-by: OpenAI Codex (model: GPT-5, autonomous) --- .github/workflows/community-smoke.yml | 61 +- README.md | 134 +- docs/community/extensions.md | 2 - docs/preset-extension-coding-standard.md | 2 +- docs/quickstart.md | 33 +- ...kflow-preset-plan-responsibility-matrix.md | 2 +- extensions/catalog.community.json | 121 +- extensions/catalog.json | 76 +- extensions/inception/CHANGELOG.md | 17 - extensions/inception/LICENSE | 21 - extensions/inception/README.md | 62 - .../commands/speckit.inception.arch.md | 26 - .../commands/speckit.inception.product.md | 151 -- extensions/inception/extension.yml | 32 - .../templates/product/uc-template.md | 69 - .../product/wireflow-high-template.html | 75 - .../product/wireflow-medium-template.html | 61 - .../tests/test_extension_contract.py | 16 - extensions/intake/.extensionignore | 9 - .../workflows/community-source-contract.yml | 172 -- .../.github/workflows/extension-artifact.yml | 159 -- extensions/intake/.gitignore | 23 - extensions/intake/AGENTS.md | 33 - extensions/intake/CHANGELOG.md | 22 - extensions/intake/LICENSE | 21 - extensions/intake/README.md | 180 -- .../intake/commands/speckit.intake.prd.md | 97 - .../commands/speckit.intake.test-cases.md | 97 - .../commands/speckit.intake.visual-design.md | 194 -- extensions/intake/config-template.yml | 115 - extensions/intake/extension.yml | 165 -- extensions/intake/requirements-dev.txt | 11 - .../python/capture_figma_metadata_shards.py | 394 --- .../scripts/python/intake_validator_common.py | 408 --- .../scripts/python/normalize_figma_layout.py | 350 --- .../scripts/python/validate_prd_intake.py | 255 -- .../python/validate_static_html_delivery.py | 675 ----- .../python/validate_test_cases_intake.py | 271 -- .../python/validate_visual_design_intake.py | 883 ------- .../intake/templates/intake-prd-contract.md | 125 - .../intake-prd-evidence-packet-template.md | 108 - .../intake-static-html-delivery-contract.md | 244 -- .../templates/intake-test-cases-contract.md | 138 - ...ake-test-cases-evidence-packet-template.md | 108 - .../intake-visual-design-contract.md | 227 -- ...-visual-design-evidence-packet-template.md | 168 -- .../schemas/figma-metadata-index.schema.json | 57 - .../schemas/figma-node-inventory.schema.json | 27 - .../schemas/figma-normalized-tree.schema.json | 126 - .../templates/schemas/prd-intake.schema.json | 103 - .../schemas/prd-source-manifest.schema.json | 158 -- .../schemas/static-html-delivery.schema.json | 240 -- .../schemas/test-case-intake.schema.json | 116 - .../test-case-source-manifest.schema.json | 176 -- .../schemas/visual-requirements.schema.json | 264 -- .../visual-source-manifest.schema.json | 186 -- .../intake/tests/test_extension_contract.py | 2308 ----------------- .../repository-governance/.extensionignore | 15 - extensions/repository-governance/.gitignore | 23 - extensions/repository-governance/CHANGELOG.md | 124 - extensions/repository-governance/LICENSE | 21 - extensions/repository-governance/README.md | 113 - .../speckit.repository-governance.generate.md | 35 - .../repository-governance/extension.yml | 46 - .../scripts/generate_repository_governance.py | 1178 --------- .../repository-governance-template.md | 119 - pyproject.toml | 3 - src/specify_cli/commands/init.py | 5 - src/specify_cli/component_standard.py | 2 +- templates/commands/checklist.md | 6 +- templates/commands/clarify.md | 6 +- templates/commands/plan.md | 2 +- tests/extensions/inception/__init__.py | 1 - .../inception/test_inception_extension.py | 94 - tests/integrations/community_defaults.py | 13 - tests/integrations/test_cli.py | 24 +- .../integrations/test_integration_catalog.py | 27 + tests/test_extensions.py | 42 +- tests/test_presets.py | 51 +- ...test_requirement_gate_workflow_contract.py | 2 +- 80 files changed, 109 insertions(+), 12217 deletions(-) delete mode 100644 extensions/inception/CHANGELOG.md delete mode 100644 extensions/inception/LICENSE delete mode 100644 extensions/inception/README.md delete mode 100644 extensions/inception/commands/speckit.inception.arch.md delete mode 100644 extensions/inception/commands/speckit.inception.product.md delete mode 100644 extensions/inception/extension.yml delete mode 100644 extensions/inception/templates/product/uc-template.md delete mode 100644 extensions/inception/templates/product/wireflow-high-template.html delete mode 100644 extensions/inception/templates/product/wireflow-medium-template.html delete mode 100644 extensions/inception/tests/test_extension_contract.py delete mode 100644 extensions/intake/.extensionignore delete mode 100644 extensions/intake/.github/workflows/community-source-contract.yml delete mode 100644 extensions/intake/.github/workflows/extension-artifact.yml delete mode 100644 extensions/intake/.gitignore delete mode 100644 extensions/intake/AGENTS.md delete mode 100644 extensions/intake/CHANGELOG.md delete mode 100644 extensions/intake/LICENSE delete mode 100644 extensions/intake/README.md delete mode 100644 extensions/intake/commands/speckit.intake.prd.md delete mode 100644 extensions/intake/commands/speckit.intake.test-cases.md delete mode 100644 extensions/intake/commands/speckit.intake.visual-design.md delete mode 100644 extensions/intake/config-template.yml delete mode 100644 extensions/intake/extension.yml delete mode 100644 extensions/intake/requirements-dev.txt delete mode 100644 extensions/intake/scripts/python/capture_figma_metadata_shards.py delete mode 100644 extensions/intake/scripts/python/intake_validator_common.py delete mode 100644 extensions/intake/scripts/python/normalize_figma_layout.py delete mode 100644 extensions/intake/scripts/python/validate_prd_intake.py delete mode 100644 extensions/intake/scripts/python/validate_static_html_delivery.py delete mode 100644 extensions/intake/scripts/python/validate_test_cases_intake.py delete mode 100644 extensions/intake/scripts/python/validate_visual_design_intake.py delete mode 100644 extensions/intake/templates/intake-prd-contract.md delete mode 100644 extensions/intake/templates/intake-prd-evidence-packet-template.md delete mode 100644 extensions/intake/templates/intake-static-html-delivery-contract.md delete mode 100644 extensions/intake/templates/intake-test-cases-contract.md delete mode 100644 extensions/intake/templates/intake-test-cases-evidence-packet-template.md delete mode 100644 extensions/intake/templates/intake-visual-design-contract.md delete mode 100644 extensions/intake/templates/intake-visual-design-evidence-packet-template.md delete mode 100644 extensions/intake/templates/schemas/figma-metadata-index.schema.json delete mode 100644 extensions/intake/templates/schemas/figma-node-inventory.schema.json delete mode 100644 extensions/intake/templates/schemas/figma-normalized-tree.schema.json delete mode 100644 extensions/intake/templates/schemas/prd-intake.schema.json delete mode 100644 extensions/intake/templates/schemas/prd-source-manifest.schema.json delete mode 100644 extensions/intake/templates/schemas/static-html-delivery.schema.json delete mode 100644 extensions/intake/templates/schemas/test-case-intake.schema.json delete mode 100644 extensions/intake/templates/schemas/test-case-source-manifest.schema.json delete mode 100644 extensions/intake/templates/schemas/visual-requirements.schema.json delete mode 100644 extensions/intake/templates/schemas/visual-source-manifest.schema.json delete mode 100644 extensions/intake/tests/test_extension_contract.py delete mode 100644 extensions/repository-governance/.extensionignore delete mode 100644 extensions/repository-governance/.gitignore delete mode 100644 extensions/repository-governance/CHANGELOG.md delete mode 100644 extensions/repository-governance/LICENSE delete mode 100644 extensions/repository-governance/README.md delete mode 100644 extensions/repository-governance/commands/speckit.repository-governance.generate.md delete mode 100644 extensions/repository-governance/extension.yml delete mode 100755 extensions/repository-governance/scripts/generate_repository_governance.py delete mode 100644 extensions/repository-governance/templates/repository-governance-template.md delete mode 100644 tests/extensions/inception/__init__.py delete mode 100644 tests/extensions/inception/test_inception_extension.py diff --git a/.github/workflows/community-smoke.yml b/.github/workflows/community-smoke.yml index 1b0dc74758..c340dc20fc 100644 --- a/.github/workflows/community-smoke.yml +++ b/.github/workflows/community-smoke.yml @@ -45,11 +45,12 @@ jobs: cd "$project_dir" /tmp/specify-community-smoke-venv/bin/specify init --here --integration claude --script sh --ignore-agent-tools - test -f .specify/extensions/preview/extension.yml - test -f .specify/extensions/discovery/extension.yml - test -f .specify/extensions/inception/extension.yml - test -f .specify/extensions/intake/extension.yml - test -f .specify/extensions/repository-governance/extension.yml + test -f .specify/extensions/git/extension.yml + test ! -e .specify/extensions/discovery + test ! -e .specify/extensions/inception + test ! -e .specify/extensions/intake + test ! -e .specify/extensions/preview + test ! -e .specify/extensions/repository-governance test -f .specify/presets/workflow-preset/preset.yml /tmp/specify-community-smoke-venv/bin/python - <<'PY' import json @@ -103,14 +104,8 @@ jobs: test ! -e .specify/presets/workflow-preset/commands/speckit.implement.md test ! -e .specify/presets/workflow-preset/.composed/speckit.implement.md - test -f .claude/skills/speckit-preview-wireflow/SKILL.md - test -f .claude/skills/speckit-discovery-contract/SKILL.md - test -f .claude/skills/speckit-inception-product/SKILL.md - test -f .claude/skills/speckit-inception-arch/SKILL.md - test -f .claude/skills/speckit-intake-visual-design/SKILL.md - test -f .claude/skills/speckit-intake-prd/SKILL.md - test -f .claude/skills/speckit-intake-test-cases/SKILL.md - test -f .claude/skills/speckit-repository-governance-generate/SKILL.md + test ! -e .claude/skills/speckit-preview-wireflow + test ! -e .claude/skills/speckit-discovery-contract test -f .claude/skills/speckit-specify/SKILL.md test -f .claude/skills/speckit-clarify/SKILL.md test -f .claude/skills/speckit-checklist/SKILL.md @@ -129,29 +124,7 @@ jobs: grep -q "## Pre-Execution Checks" .claude/skills/speckit-implement/SKILL.md grep -q "mark the task off as \[X\] in the tasks file" .claude/skills/speckit-implement/SKILL.md - for extension_id in discovery inception intake preview repository-governance; do - /tmp/specify-community-smoke-venv/bin/specify extension remove "$extension_id" --force - test ! -d ".specify/extensions/$extension_id" - case "$extension_id" in - preview) - test ! -d .claude/skills/speckit-preview-wireflow - ;; - discovery) - test "$(find .claude/skills -maxdepth 1 -type d -name 'speckit-discovery-*' | wc -l)" -eq 0 - ;; - inception) - test "$(find .claude/skills -maxdepth 1 -type d -name 'speckit-inception-*' | wc -l)" -eq 0 - ;; - intake) - test "$(find .claude/skills -maxdepth 1 -type d -name 'speckit-intake-*' | wc -l)" -eq 0 - ;; - repository-governance) - test ! -d .claude/skills/speckit-repository-governance-generate - ;; - esac - done - - for extension_id in arch discovery inception intake preview repository-governance; do + for extension_id in arch discovery preview; do /tmp/specify-community-smoke-venv/bin/specify extension add "$GITHUB_WORKSPACE/extensions/$extension_id" --dev case "$extension_id" in arch) @@ -167,22 +140,6 @@ jobs: test -f .claude/skills/speckit-discovery-contract/SKILL.md grep -q "interface-contract discovery facilitator" .claude/skills/speckit-discovery-contract/SKILL.md ;; - inception) - test -f .claude/skills/speckit-inception-product/SKILL.md - test -f .claude/skills/speckit-inception-arch/SKILL.md - grep -q "Conversation Workflow" .claude/skills/speckit-inception-product/SKILL.md - grep -q "INCEPTION_ARCH_COMMAND_RETIRED" .claude/skills/speckit-inception-arch/SKILL.md - ;; - intake) - test -f .claude/skills/speckit-intake-visual-design/SKILL.md - test -f .claude/skills/speckit-intake-prd/SKILL.md - test -f .claude/skills/speckit-intake-test-cases/SKILL.md - grep -q "visual design intake" .claude/skills/speckit-intake-visual-design/SKILL.md - ;; - repository-governance) - test -f .claude/skills/speckit-repository-governance-generate/SKILL.md - grep -q "Project Governance Projection Generation" .claude/skills/speckit-repository-governance-generate/SKILL.md - ;; esac done diff --git a/README.md b/README.md index 4b6d5f40f7..6a6cf95b3d 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,14 @@

- 这个仓库的重点不是复述基础用法,而是打包一组本地扩展和预设,让规格、架构、证据、预览、治理和多 agent 实现流程连成一条可审查的链路。 + 这个仓库的重点不是复述基础用法,而是提供一套小而清晰的默认流程,以及按需启用的扩展和预设。

--- ## 本地定位 -这个 checkout 是一个带本地增强能力的 Spec Kit 仓库。它保留核心 `specify` 工作流,同时默认安装一组本地扩展和一个默认预设。 +这个 checkout 是一个带本地增强能力的 Spec Kit 仓库。它保留核心 `specify` 工作流,默认只安装核心 `git` 扩展和 `workflow-preset` 预设;其他扩展均按需启用。 本 README 只介绍仓库中实际存在的本地内容: @@ -24,9 +24,9 @@ 如果你只是想知道这个仓库相比基础流程多了什么,可以先看这三件事: -- 默认扩展:`discovery`、`inception`、`intake`、`preview`、`repository-governance`。 +- 默认扩展:`git`。 - 默认预设:`workflow-preset`。 -- 自动上下文扩展:`agent-context`。 +- 可选扩展:`discovery`、`preview`、`agent-context`、`arch`、`bug`。 ## 快速开始 @@ -55,7 +55,7 @@ specify init . --integration codex --force specify init my-project --integration codex --ignore-agent-tools ``` -初始化完成后,本地默认能力会被复制到项目的 `.specify/` 目录,并注册到所选 agent 的命令或 skill 目录中。 +初始化完成后,`git` 扩展和默认预设会被复制到项目的 `.specify/` 目录,并注册到所选 agent 的命令或 skill 目录中。 ## 默认安装内容 @@ -63,42 +63,25 @@ specify init my-project --integration codex --ignore-agent-tools | 类型 | ID | 来源目录 | 作用 | | --- | --- | --- | --- | -| 自动扩展 | `agent-context` | `extensions/agent-context` | 维护 AGENTS、CLAUDE、Copilot 等 agent context 文件里的 Spec Kit 受管段。 | -| 默认扩展 | `discovery` | `extensions/discovery` | 在正式计划前做可行性、技术选型、旧代码评估、接口理解、PoC 和场景化技术决策。 | -| 默认扩展 | `inception` | `extensions/inception` | 在正式 SDD 前通过对话收敛可选的产品 UC 和 wireflow;旧架构入口仅提供迁移提示。 | -| 默认扩展 | `intake` | `extensions/intake` | 把 PRD、设计稿、Figma、最终静态 HTML 交付、测试用例等来源归一化为 SDD 可消费的证据包。 | -| 默认扩展 | `preview` | `extensions/preview` | 从规格和计划生成低、中、高保真 Markdown 或自包含 HTML 预览。 | -| 默认扩展 | `repository-governance` | `extensions/repository-governance` | 生成仓库治理 SSOT,帮助 agent 明确目录责任、读取顺序和事实证据。 | +| 默认扩展 | `git` | `extensions/git` | 提供特性分支、远端检查和提交辅助命令。 | | 默认预设 | `workflow-preset` | `presets/workflow-preset` | 从 Specify 引入 UI/UX 需求,从 Plan 引入 BDD、集成与 E2E 测试设计,由 Tasks 映射为执行清单,最终交给标准 Core Implement。 | 默认扩展列表在 `src/specify_cli/commands/init.py` 的 `DEFAULT_BUNDLED_EXTENSIONS` 中维护。默认预设列表在同文件的 `DEFAULT_BUNDLED_PRESETS` 中维护。 -## 默认扩展 +## 扩展审计与迁移 -### `inception` - -`inception` 是正式规格化之前的可选产品启动阶段。它通过对话确认产品材料,并用模板生成 `inception/product/` 下的产物。 - -产品命令: - -```text -/speckit.inception.product -``` - -主要产物: +| 扩展 | 状态 | 原因与迁移 | +| --- | --- | --- | +| `git` | 默认保留 | 核心分支工作流,体积小且普遍适用。 | +| `discovery` | 内置、按需安装 | 技术调研仍有价值,但不是所有项目的必经阶段;使用 `specify extension add discovery`。 | +| `preview` | 内置、按需安装 | 实现前评审仍有价值,但不是默认流程;使用 `specify extension add preview`。 | +| `inception` | 已移除 | 旧产品启动和迁移命令不再作为权威阶段;把仍有价值的材料直接作为 `/speckit.specify` 的输入。 | +| `intake` | 已移除 | 工作流不再依赖其 schema、artifact 或 blocker route;由用户选择的来源工具提供证据,并在规格中记录来源。 | +| `repository-governance` | 已移除 | 其上下文投影与 `agent-context` 的独占职责冲突;需要受管上下文时显式安装 `agent-context`。 | -```text -inception/product/uc.md -inception/product/wireflow-medium.html -inception/product/wireflow-high.html -``` +升级 CLI 不会扫描或删除既有项目的 `.specify/extensions/` 内容。因此,旧项目中已经安装的退役扩展文件会原样保留;如需清理,请由项目维护者显式运行 `specify extension remove `。 -使用建议: - -- 仅在需要产品启动材料时运行 `/speckit.inception.product`。 -- `uc.md` 只有在用户后续明确选择时才成为 Constitution 或 Architecture 输入。 -- `/speckit.inception.arch` 已退役,只返回迁移提示,不读取 UC、生成架构或运行 PoC。 -- `inception` 不生成 `spec.md`、`plan.md`、`tasks.md`、OpenAPI、数据库 schema、生产代码或测试套件变更。 +## 可选扩展 ### `arch` @@ -143,37 +126,6 @@ inception/product/wireflow-high.html 典型产物会写在当前 feature 的 discovery 相关文件中,例如 feasibility、tech-selection、legacy codebase risk、PoC plan/result 或 API implementation overview。 -### `intake` - -`intake` 负责把外部输入变成可追踪证据,而不是直接替你生成需求。它的重点是保留来源、标记不确定性、做结构化归一化,让后续 `/speckit.specify`、`/speckit.plan` 能带着证据继续工作。 - -常用命令: - -```text -/speckit.intake.prd -/speckit.intake.visual-design -/speckit.intake.test-cases -``` - -支持来源: - -- PRD、产品说明、Markdown、PDF、导出的文档。 -- 图片、线框图、设计 PDF、Figma 文件、Figma 页面或节点。 -- 设计稿派生的视觉 IR、操作回放、动效锚点、视口截图和最终静态 HTML 交付证据。 -- 既有测试、Gherkin、手工测试用例、QA 导出、测试管理表格。 - -主要产物: - -```text -specs//intake/prd/ -specs//intake/visual-design/ -specs//intake/visual-design/visual-ir/ -specs//intake/visual-design/delivery/ -specs//intake/test-cases/ -``` - -这些目录中会包含 source manifest、source files、归一化 YAML、evidence packet 和 schema 校验所需材料。 - ### `preview` `preview` 在实现前生成评审产物。它不改应用源码,不替代实现;它用当前 feature 的规格、计划和契约生成可以讨论的自包含 HTML wireflow。 @@ -196,35 +148,9 @@ specs//preview/wireflow.html - 产品、设计和工程需要一起评审:用 `mid`。 - 交互、状态、权限、响应式和错误反馈要确认:用 `high`。 -### `repository-governance` - -`repository-governance` 生成 agent 可读的仓库治理说明。它把目录职责、SSOT 读取顺序、工具链证据、agent 平台适配和仓库事实投影到当前 agent 的上下文文件中。 - -常用命令: - -```text -/speckit.repository-governance.generate -``` - -它也注册了 hook,可在 constitution、plan、tasks 之后提示生成或更新治理内容。 - -主要产物: - -```text -.specify/memory/repository-governance.md -``` - -以及当前集成对应的 agent context 文件中的受管治理段。 - -使用建议: - -- 多 agent 协作时使用。 -- 新人或新 agent 接手仓库时使用。 -- 仓库目录结构、构建工具、SSOT 或平台适配规则变化后使用。 - ### `agent-context` -`agent-context` 是上下文维护扩展。它读取集成元数据,并更新当前 agent 的说明文件,例如 `AGENTS.md`、`CLAUDE.md` 或 `.github/copilot-instructions.md`。 +`agent-context` 是完全 opt-in(显式启用)的上下文维护扩展。只有用户显式安装并启用它后,它才会更新 `AGENTS.md`、`CLAUDE.md` 或 `.github/copilot-instructions.md` 等文件。 常用命令: @@ -412,25 +338,22 @@ specify bundle build --path ./my-bundle ### 新项目 ```text -/speckit.inception.product # 可选 /speckit.constitution # greenfield;用户指定输入和更新范围 /speckit.specify /speckit.clarify /speckit.checklist -/speckit.discovery.feasibility /speckit.plan -/speckit.preview.wireflow mid /speckit.tasks /speckit.analyze /speckit.implement ``` +需要技术调研或 UI 评审时,先显式安装 `discovery` 或 `preview`,再把对应命令插入上述链路。 + ### 旧仓库接入 ```text -/speckit.discovery.codebase /speckit.constitution # brownfield;显式授权仓库证据范围 -/speckit.repository-governance.generate /speckit.specify /speckit.checklist /speckit.plan @@ -440,19 +363,11 @@ specify bundle build --path ./my-bundle ### 已有 PRD、设计或测试用例 -```text -/speckit.intake.prd -/speckit.intake.visual-design -/speckit.intake.test-cases -/speckit.specify -/speckit.clarify -/speckit.plan -``` +把材料路径和来源说明直接传给 `/speckit.specify`;缺失或矛盾的证据保持为 blocker(阻塞项),由用户选择的来源工具补齐。 ### 前端和交互功能 ```text -/speckit.intake.visual-design /speckit.specify /speckit.preview.wireflow low /speckit.plan @@ -495,9 +410,6 @@ specify extension add bug | 目录或文件 | 来源 | 含义 | | --- | --- | --- | | `.specify/memory/architecture.md` | `workflow-preset` 的 `/speckit.constitution` | 项目级边界、概念、技术证据、规划约束和缺口。 | -| `.specify/memory/repository-governance.md` | `repository-governance` | 内部仓库治理 SSOT。 | -| `inception/product/` | `inception` | 产品 UC 和 medium/high wireflow 启动设计产物。 | -| `specs//intake/` | `intake` | PRD、视觉设计、测试用例的结构化证据包。 | | `specs//preview/` | `preview` | 单一自包含 HTML wireflow 预览。 | | `specs//contracts/bdd/` | `workflow-preset` | BDD 行为契约。 | | `specs//contracts/uif/` | `workflow-preset` | UI flow / interface fidelity 契约。 | @@ -524,7 +436,7 @@ specify extension add git ```bash specify extension add --dev extensions/preview -specify extension add --dev extensions/intake +specify extension add --dev extensions/discovery ``` 查看已安装预设: @@ -576,7 +488,7 @@ uv run pytest tests/integrations -v ```bash specify extension add --dev extensions/preview -specify extension add --dev extensions/intake +specify extension add --dev extensions/preview specify preset add --dev presets/workflow-preset ``` diff --git a/docs/community/extensions.md b/docs/community/extensions.md index 0b247d15e4..8fd2c924b5 100644 --- a/docs/community/extensions.md +++ b/docs/community/extensions.md @@ -57,7 +57,6 @@ 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, 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) | @@ -96,7 +95,6 @@ The following community-contributed extensions are available in [`catalog.commun | Ralph Loop | Autonomous implementation loop using AI agent CLI | `code` | Read+Write | [spec-kit-ralph](https://github.com/Rubiss-Projects/spec-kit-ralph) | | Reconcile Extension | Reconcile implementation drift by surgically updating feature artifacts. | `docs` | Read+Write | [spec-kit-reconcile](https://github.com/stn1slv/spec-kit-reconcile) | | Red Team | Adversarial review of specs before /speckit.plan — parallel lens agents surface risks that clarify/analyze structurally can't (prompt injection, integrity gaps, cross-spec drift, silent failures). Produces a structured findings report; no auto-edits to specs. | `docs` | Read+Write | [spec-kit-red-team](https://github.com/ashbrener/spec-kit-red-team) | -| Repository Governance | Generate or update the active Repository Governance Framework file with vertical SSOT routing and evidence | `process` | Read+Write | [spec-kit-agent-governance](https://github.com/bigsmartben/spec-kit-agent-governance) | | Research Harness | State-externalizing research harness: budgeted exploration, evidence curation, and claim verification for spec-driven development | `process` | Read+Write | [spec-kit-harness](https://github.com/formin/spec-kit-harness) | | Repository Index | Generate index for existing repo for overview, architecture and module level. | `docs` | Read-only | [spec-kit-repoindex](https://github.com/liuyiyu/spec-kit-repoindex) | | Reqnroll BDD | Adds Reqnroll BDD planning, Gherkin generation, traceability, safe task injection, handoff, and verification to Spec Kit | `process` | Read+Write | [spec-kit-reqnroll-bdd](https://github.com/LoogacyStudio/spec-kit-reqnroll-bdd) | diff --git a/docs/preset-extension-coding-standard.md b/docs/preset-extension-coding-standard.md index d4b803e034..e20c26935c 100644 --- a/docs/preset-extension-coding-standard.md +++ b/docs/preset-extension-coding-standard.md @@ -577,7 +577,7 @@ Preset/Extension issue 只有满足以下条件才可视为完成: ## 16. 推荐参考实现 - `extensions/arch/`:Command、Template、Schema 和 Validator 分离。 -- `extensions/intake/`:Schema、语义 Contract、Readiness Validator 权威清晰。 +- `extensions/discovery/`:命令、语义 Contract、验证器责任边界清晰。 - `presets/workflow-preset/`:阶段所有权、结构化行为契约,以及 Tasks 与标准 Core Implement 的边界。 - `src/specify_cli/integrations/`:平台投影与功能提示词分离。 - `src/specify_cli/agents.py`:Extension/Preset 共用的 command registrar。 diff --git a/docs/quickstart.md b/docs/quickstart.md index 88048b1dcc..632aab0375 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,6 +1,6 @@ # 快速开始 -这份指南面向本仓库分发版的 Spec Kit。它保留核心 Spec-Driven Development 流程,同时默认带上 Constitution 托管的单文件项目架构、仓库治理、BDD/UIF 行为契约、HTML 预览、实现期 handoff 和最终 code review receipt。 +这份指南面向本仓库分发版的 Spec Kit。它保留核心 Spec-Driven Development 流程,默认只安装 `git` 扩展和 `workflow-preset`;技术调研、HTML 预览和上下文维护均按需启用。 > [!NOTE] > 自动化脚本同时提供 Bash (`.sh`) 和 PowerShell (`.ps1`) 版本。`specify` CLI 会按系统自动选择,也可以通过 `--script sh|ps` 显式指定。 @@ -13,14 +13,11 @@ 生产功能建议使用默认增强链路: ```text -/speckit.inception.product # 可选;仅在用户选择其产物作为输入时 /speckit.constitution # 同时托管 constitution.md 与 architecture.md -/speckit.repository-governance.generate /speckit.specify /speckit.checklist /speckit.clarify /speckit.plan -/speckit.preview.wireflow mid /speckit.tasks /speckit.analyze /speckit.implement @@ -101,7 +98,7 @@ specify init my-project --integration codex --integration-options="--skills" specify integration list ``` -## 3. 建立项目原则与治理 +## 3. 建立项目原则与架构 在编码助手中先建立项目原则和独立的项目架构: @@ -119,19 +116,7 @@ specify integration list 其推理顺序是 System Boundary(系统边界)→ Conceptual Model(概念模型)→ Technical Decisions & Evidence(技术决策与证据)→ Planning Guardrails & Gaps(规划守则与缺口),不再生成 4+1、多视图或 PoC 目录。 -生成或更新仓库治理规范: - -```text -/speckit.repository-governance.generate -``` - -治理命令会生成或更新当前 integration 对应的 agent 上下文文件中的受管段,并维护内部治理记忆: - -```text -.specify/memory/repository-governance.md -``` - -它用于约束 SSOT 读取顺序、目录责任、agent 平台适配和仓库事实证据。 +需要维护 agent 上下文文件时,显式运行 `specify extension add agent-context`;CLI 本身不会写入或迁移这些文件。 ## 4. 确认项目架构 @@ -176,12 +161,16 @@ specs//checklists/nfr.md specs//checklists/visual.md ``` -如果清单指出产品决策缺失,使用 `/speckit.clarify` 修复;provider 证据缺口返回对应 intake。所有适用门禁通过后,`/speckit.plan` 才会写入规划产物。 +如果清单指出产品决策缺失,使用 `/speckit.clarify` 修复;provider 证据缺口返回来源证据流程。所有适用门禁通过后,`/speckit.plan` 才会写入规划产物。 ## 6. 预览 UI/UX 规格 对 UI、流程或交互有不确定性时,先生成保真度合适的预览产物: +```bash +specify extension add preview +``` + ```text /speckit.preview.wireflow mid mobile album browsing and reorder flow ``` @@ -276,13 +265,13 @@ specify preset search specify workflow search ``` -重新安装默认增强能力: +安装可选增强能力: ```bash specify extension add arch -specify extension add inception +specify extension add discovery specify extension add preview -specify extension add repository-governance +specify extension add agent-context specify preset add workflow-preset ``` diff --git a/docs/workflow-preset-plan-responsibility-matrix.md b/docs/workflow-preset-plan-responsibility-matrix.md index 337489c7a4..73fca84a39 100644 --- a/docs/workflow-preset-plan-responsibility-matrix.md +++ b/docs/workflow-preset-plan-responsibility-matrix.md @@ -119,7 +119,7 @@ Y1 在 Test 主轴内的一个动作,不是完整的 Phase 0。 | 职责 | 判断 Plan 是否有权开始,不产生任何设计 | | 输出 | 仅内存中的 PASS/BLOCKED 和阻塞报告 | | 门禁 | Architecture 完整;Checklist Revision 与 Spec 一致;Applicable Gate 全部 PASS | -| 失败路由 | 产品问题回 Clarify;Provider Evidence 回 Intake;架构冲突回 Constitution | +| 失败路由 | 产品问题回 Clarify;Provider Evidence 回来源证据流程;架构冲突回 Constitution | | 禁止 | 创建目录、写 BDD Draft、更新 `plan.md` | ## 8. Y1:Design Basis Projection diff --git a/extensions/catalog.community.json b/extensions/catalog.community.json index 37beed08d9..69ba3fca78 100644 --- a/extensions/catalog.community.json +++ b/extensions/catalog.community.json @@ -1,6 +1,6 @@ { "schema_version": "1.0", - "updated_at": "2026-07-06T00:00:00Z", + "updated_at": "2026-07-31T00:00:00Z", "catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json", "extensions": { "agent-assign": { @@ -37,45 +37,6 @@ "created_at": "2026-03-31T00:00:00Z", "updated_at": "2026-03-31T00:00:00Z" }, - "agent-governance": { - "name": "Agent Governance", - "id": "agent-governance", - "description": "Generate agent-platform repository governance files from Spec Kit metadata.", - "author": "bigben", - "version": "1.2.0", - "download_url": "https://github.com/bigsmartben/spec-kit-agent-governance/archive/refs/tags/v1.2.0.zip", - "repository": "https://github.com/bigsmartben/spec-kit-agent-governance", - "homepage": "https://github.com/bigsmartben/spec-kit-agent-governance", - "documentation": "https://github.com/bigsmartben/spec-kit-agent-governance/blob/main/README.md", - "changelog": "https://github.com/bigsmartben/spec-kit-agent-governance/blob/main/CHANGELOG.md", - "license": "MIT", - "category": "process", - "effect": "read-write", - "requires": { - "speckit_version": ">=0.8.0", - "tools": [ - { - "name": "uv", - "required": true - } - ] - }, - "provides": { - "commands": 1, - "hooks": 3 - }, - "tags": [ - "governance", - "agents", - "memory", - "context" - ], - "verified": false, - "downloads": 0, - "stars": 0, - "created_at": "2026-05-14T00:00:00Z", - "updated_at": "2026-05-21T00:00:00Z" - }, "agent-orchestrator": { "name": "Intelligent Agent Orchestrator", "id": "agent-orchestrator", @@ -1041,7 +1002,7 @@ "discovery": { "name": "Spec Kit Discovery Extension", "id": "discovery", - "description": "Validate pre-development interface feasibility by finding the key interface design from uc.md, spec.md, or arch.md and producing one verified interface contract artifact.", + "description": "Validate pre-development interface feasibility by finding the key interface design from uc.md, spec.md, or arch.md, validating that design with non-persistent evidence, and producing one verified interface contract artifact.", "author": "bigsmartben", "version": "0.3.0", "download_url": "https://github.com/bigsmartben/spec-kit-discovery/archive/38f660c815f3ef95d80f143dac6c6411b12a9f04.zip", @@ -1477,46 +1438,6 @@ "created_at": "2026-06-16T00:00:00Z", "updated_at": "2026-06-16T00:00:00Z" }, - "intake": { - "name": "Intake", - "id": "intake", - "description": "Normalize PRD, visual design/spec packages, preview evidence, and test-case evidence into SDD-ready intake artifacts", - "author": "bigsmartben", - "version": "0.1.5", - "download_url": "https://github.com/bigsmartben/spec-kit-intake/archive/cc3c90731b1d4366d1fa34e47d8713e5478d0599.zip", - "repository": "https://github.com/bigsmartben/spec-kit-intake", - "homepage": "https://github.com/bigsmartben/spec-kit-intake", - "documentation": "https://github.com/bigsmartben/spec-kit-intake/blob/main/README.md", - "changelog": "https://github.com/bigsmartben/spec-kit-intake/blob/main/CHANGELOG.md", - "license": "MIT", - "category": "docs", - "effect": "read-write", - "requires": { - "speckit_version": ">=0.8.10.dev0", - "tools": [ - { - "name": "figma-mcp", - "required": false - } - ] - }, - "provides": { - "commands": 3, - "hooks": 1 - }, - "tags": [ - "intake", - "sdd", - "requirements", - "validation", - "figma" - ], - "verified": false, - "downloads": 0, - "stars": 0, - "created_at": "2026-06-23T00:00:00Z", - "updated_at": "2026-07-06T00:00:00Z" - }, "issue": { "name": "GitHub Issues Integration 2", "id": "issue", @@ -2887,44 +2808,6 @@ "created_at": "2026-03-23T13:30:00Z", "updated_at": "2026-03-23T13:30:00Z" }, - "repository-governance": { - "name": "Repository Governance", - "id": "repository-governance", - "description": "Generate project-governance projections from Spec Kit metadata", - "author": "bigben", - "version": "3.0.2", - "download_url": "https://github.com/bigsmartben/spec-kit-agent-governance/archive/refs/tags/v3.0.2.zip", - "repository": "https://github.com/bigsmartben/spec-kit-agent-governance", - "homepage": "https://github.com/bigsmartben/spec-kit-agent-governance", - "documentation": "https://github.com/bigsmartben/spec-kit-agent-governance/blob/main/README.md", - "changelog": "https://github.com/bigsmartben/spec-kit-agent-governance/blob/main/CHANGELOG.md", - "license": "MIT", - "requires": { - "speckit_version": ">=0.8.0", - "tools": [ - { - "name": "uv", - "required": true - } - ] - }, - "provides": { - "commands": 1, - "hooks": 3 - }, - "tags": [ - "governance", - "repository", - "ssot", - "agents", - "context" - ], - "verified": false, - "downloads": 0, - "stars": 0, - "created_at": "2026-05-14T00:00:00Z", - "updated_at": "2026-07-03T00:00:00Z" - }, "reqnroll-bdd": { "name": "Reqnroll BDD", "id": "reqnroll-bdd", diff --git a/extensions/catalog.json b/extensions/catalog.json index 94db3e143f..bfd8408b09 100644 --- a/extensions/catalog.json +++ b/extensions/catalog.json @@ -1,6 +1,6 @@ { "schema_version": "1.0", - "updated_at": "2026-07-07T00:00:00Z", + "updated_at": "2026-07-31T00:00:00Z", "catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json", "extensions": { "agent-context": { @@ -69,54 +69,6 @@ "core" ] }, - "inception": { - "name": "SDD Inception", - "id": "inception", - "version": "2.0.0", - "description": "Guide optional product inception and redirect retired architecture inception to the Constitution stage", - "author": "bigsmartben", - "repository": "https://github.com/bigsmartben/spec-kit", - "license": "MIT", - "bundled": true, - "requires": { - "speckit_version": ">=0.8.10.dev0" - }, - "provides": { - "commands": 2 - }, - "tags": [ - "inception", - "sdd", - "product", - "migration", - "workflow" - ] - }, - "intake": { - "name": "Intake", - "id": "intake", - "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", - "bundled": true, - "requires": { - "speckit_version": ">=0.8.10.dev0" - }, - "provides": { - "commands": 3, - "hooks": 1 - }, - "tags": [ - "intake", - "sdd", - "requirements", - "validation", - "figma", - "static-html" - ] - }, "preview": { "name": "Spec Kit Preview", "id": "preview", @@ -144,7 +96,7 @@ "name": "Spec Kit Discovery Extension", "id": "discovery", "version": "0.3.0", - "description": "Validate pre-development interface feasibility by finding the key interface design from uc.md, spec.md, or arch.md and producing one verified interface contract artifact", + "description": "Validate pre-development interface feasibility by finding the key interface design from uc.md, spec.md, or arch.md, validating that design with non-persistent evidence, and producing one verified interface contract artifact.", "author": "bigsmartben", "repository": "https://github.com/bigsmartben/spec-kit-discovery", "license": "MIT", @@ -165,30 +117,6 @@ "pre-development", "validation" ] - }, - "repository-governance": { - "name": "Repository Governance", - "id": "repository-governance", - "version": "3.0.2", - "description": "Generate project-governance projections from Spec Kit metadata", - "author": "bigben", - "repository": "https://github.com/bigsmartben/spec-kit-agent-governance", - "license": "MIT", - "bundled": true, - "requires": { - "speckit_version": ">=0.8.0" - }, - "provides": { - "commands": 1, - "hooks": 3 - }, - "tags": [ - "governance", - "repository", - "ssot", - "agents", - "context" - ] } } } diff --git a/extensions/inception/CHANGELOG.md b/extensions/inception/CHANGELOG.md deleted file mode 100644 index e52fa1525b..0000000000 --- a/extensions/inception/CHANGELOG.md +++ /dev/null @@ -1,17 +0,0 @@ -# Changelog - -## 2.0.0 - -- Move Architecture inception into the Constitution-managed single-file - project Architecture lifecycle. -- Retain `/speckit.inception.arch` only as a write-free compatibility - entrypoint returning `INCEPTION_ARCH_COMMAND_RETIRED`. -- Remove the five `inception/arch/` templates and automatic PoC responsibility. -- Make product inception optional and require users to select its outputs - explicitly when using them as Constitution-stage inputs. - -## 1.0.0 - -- Add product and architecture inception workflow commands. -- Add template-backed inception artifacts under `inception/product/` and `inception/arch/`. -- Require user-confirmed preparation before running real API POC code. diff --git a/extensions/inception/LICENSE b/extensions/inception/LICENSE deleted file mode 100644 index 7a62eb18f5..0000000000 --- a/extensions/inception/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 bigsmartben - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/extensions/inception/README.md b/extensions/inception/README.md deleted file mode 100644 index 4e962e4255..0000000000 --- a/extensions/inception/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# SDD Inception Extension - -Run optional product inception before formal Spec Kit specification. - -## Product Inception - -`/speckit.inception.product` converges user-confirmed product ideas into: - -```text -inception/product/uc.md -inception/product/wireflow-medium.html -inception/product/wireflow-high.html -``` - -`wireflow-medium.html` and `wireflow-high.html` are derived from `uc.md`. They -must not create new product facts or overwrite the product boundary established -in `uc.md`. - -Product inception is optional. Its outputs become downstream inputs only when -the user selects them for a later command. - -## Retired Architecture Inception - -`/speckit.inception.arch` is retained only as a write-free compatibility -entrypoint. It returns `INCEPTION_ARCH_COMMAND_RETIRED` and directs the user to: - -```text -/speckit.constitution -``` - -The compatibility command does not read `inception/product/uc.md`, create -`inception/arch/`, run PoC code, or write project Architecture. - -For example: - -```text -/speckit.constitution Greenfield. Use inception/product/uc.md as the selected -product-intent source, exclude wireflow HTML, and update Constitution and Architecture. -``` - -The user may select `inception/product/uc.md`, another product document, -conversation input, existing Architecture, authorized repository evidence, or -external constraints. No conventional path is mandatory. - -## Boundaries - -The inception extension does not create formal Spec Kit artifacts: - -```text -.specify/memory/constitution.md -.specify/memory/architecture.md -spec.md -plan.md -tasks.md -OpenAPI -database schema -application implementation -test suite changes -``` - -Use `/speckit.constitution` for project governance and Architecture, then -`/speckit.specify` for feature requirements. diff --git a/extensions/inception/commands/speckit.inception.arch.md b/extensions/inception/commands/speckit.inception.arch.md deleted file mode 100644 index ce13043d39..0000000000 --- a/extensions/inception/commands/speckit.inception.arch.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -description: Retired compatibility entrypoint for Constitution-managed project Architecture. ---- - -## Retired Responsibility - -`__SPECKIT_COMMAND_INCEPTION_ARCH__` no longer generates Architecture inception artifacts or PoC assets. - -Project Architecture is maintained by the Constitution stage: - -```text -__SPECKIT_COMMAND_CONSTITUTION__ -``` - -Do not read `inception/product/uc.md` by default. Do not inspect product, repository, or existing Architecture files from this compatibility command. Do not create `inception/arch/`, PoC code, or `.specify/memory/architecture.md`. - -Report: - -```text -INCEPTION_ARCH_COMMAND_RETIRED -Use __SPECKIT_COMMAND_CONSTITUTION__. Specify greenfield, brownfield, or amendment mode; -selected and excluded sources; repository-inspection scope; and whether -Constitution, Architecture, or both may be updated. -``` - -`inception/product/uc.md` may be selected by the user as one Architecture input, but it is not a prerequisite or automatic authority. diff --git a/extensions/inception/commands/speckit.inception.product.md b/extensions/inception/commands/speckit.inception.product.md deleted file mode 100644 index d38e7635db..0000000000 --- a/extensions/inception/commands/speckit.inception.product.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -description: Converge product intent into inception product UC and wireflow artifacts. ---- - -## User Input - -```text -$ARGUMENTS -``` - -You **MUST** consider the user input before proceeding when it is not empty. - -## Goal - -Guide a conversational product inception workflow and write only these artifacts: - -```text -inception/product/uc.md -inception/product/wireflow-medium.html -inception/product/wireflow-high.html -``` - -This command answers: - -- Who is the user? -- What goal are they trying to complete? -- What is the product boundary for this inception round? -- How does the user journey flow at medium and high fidelity? - -It does not create formal Spec Kit feature artifacts. - -## Templates - -Template structure is authoritative. Load these templates before writing output: - -```text -.specify/extensions/inception/templates/product/uc-template.md -.specify/extensions/inception/templates/product/wireflow-medium-template.html -.specify/extensions/inception/templates/product/wireflow-high-template.html -``` - -When running from the extension repository, use: - -```text -templates/product/uc-template.md -templates/product/wireflow-medium-template.html -templates/product/wireflow-high-template.html -``` - -Do not invent output sections, table columns, HTML structure, or quality gates outside the templates. If a template is missing, stop with blocker `TEMPLATE_BYPASS`. - -## Conversation Workflow - -Proceed through these confirmation points before writing: - -1. Confirm the product idea, target users, and primary job-to-be-done. -2. Confirm business assumptions and success criteria. -3. Confirm core user scenarios, alternate paths, exception paths, and permission boundaries. -4. Confirm out-of-scope items for this inception round. -5. Confirm unresolved questions and mark them as `[NEEDS CLARIFICATION]`. - -If any confirmation is missing and the artifact would otherwise turn an assumption into fact, stop with blocker `UNSUPPORTED_INFERENCE`. - -## Context Loading - -1. Verify the current directory is a Spec Kit project by checking for `.specify/`. -2. Read user-provided product input from `$ARGUMENTS`. -3. Read existing `inception/product/uc.md` only when refreshing prior product inception work. -4. Read existing `inception/product/wireflow-medium.html` and `inception/product/wireflow-high.html` only to preserve still-valid design notes and open questions. - -Forbidden sources: - -```text -spec.md -plan.md -tasks.md -inception/arch/ -docs/technical/ -contracts/ -OpenAPI files -database schemas -source code -tests -``` - -If a forbidden source is used as evidence, stop with blocker `SOURCE_PRIORITY_VIOLATION`. - -## Product Artifact Rules - -- `uc.md` is the primary product fact artifact. -- `wireflow-medium.html` and `wireflow-high.html` are derived from `uc.md`. -- Wireflow artifacts must not add new product facts, new business rules, new permissions, or new validation rules that are absent from `uc.md`. -- Unsupported requested items must be recorded as open questions, not silently invented. -- Use stable evidence labels: `confirmed`, `needs-clarification`, `derived-from-uc`, `unsupported`. - -`wireflow-high.html` must cover these UI states when relevant to the confirmed UC: - -```text -loading -success -empty -error -disabled -permission denied -submitting -submitted -``` - -## Operating Boundaries - -Write only the three product inception paths listed in Goal. - -Do not create or modify: - -```text -spec.md -arch.md -plan.md -tasks.md -api-contract -openapi.yaml -database schema -domain model -source code -tests -build configuration -production assets -``` - -If any output path falls outside `inception/product/`, stop with blocker `OUTPUT_PATH_MISMATCH`. - -## Procedure - -1. Load the product templates. -2. Run the conversation workflow and resolve missing confirmations. -3. Render `inception/product/uc.md` first. -4. Derive `inception/product/wireflow-medium.html` from `uc.md`. -5. Derive `inception/product/wireflow-high.html` from `uc.md`. -6. Re-check every rendered artifact against the quality gates. -7. Report written paths, confirmed product facts, derived wireflow coverage, unsupported requests, and open questions. - -## Quality Gates - -- `OUTPUT_PATH_MISMATCH`: a write target is outside `inception/product/`. -- `SOURCE_PRIORITY_VIOLATION`: a forbidden source was used as evidence. -- `SCOPE_LEAK`: output includes formal spec, plan, task, API contract, database schema, domain model, code implementation, or test-suite work. -- `UNSUPPORTED_INFERENCE`: an unconfirmed item is written as fact. -- `EMPTY_PRIMARY_ARTIFACT`: `uc.md` lacks roles, goals, main flow, success criteria, or out-of-scope sections. -- `TEMPLATE_BYPASS`: templates were not loaded or the output structure bypasses them. - -If any blocker is present, report the blocker code and do not claim the inception product artifacts are ready. diff --git a/extensions/inception/extension.yml b/extensions/inception/extension.yml deleted file mode 100644 index a51036683a..0000000000 --- a/extensions/inception/extension.yml +++ /dev/null @@ -1,32 +0,0 @@ -schema_version: "1.0" - -extension: - id: inception - name: "SDD Inception" - version: "2.0.0" - description: "Guide optional product inception and redirect retired architecture inception to the Constitution stage" - author: "bigsmartben" - repository: "https://github.com/bigsmartben/spec-kit" - homepage: "https://github.com/bigsmartben/spec-kit" - license: "MIT" - category: "process" - effect: "read-write" - -requires: - speckit_version: ">=0.8.10.dev0" - -provides: - commands: - - name: "speckit.inception.product" - file: "commands/speckit.inception.product.md" - description: "Converge product intent into inception/product UC and wireflow artifacts" - - name: "speckit.inception.arch" - file: "commands/speckit.inception.arch.md" - description: "Redirect the retired architecture inception command to /speckit.constitution" - -tags: - - "inception" - - "sdd" - - "product" - - "migration" - - "workflow" diff --git a/extensions/inception/templates/product/uc-template.md b/extensions/inception/templates/product/uc-template.md deleted file mode 100644 index 81d180eeb1..0000000000 --- a/extensions/inception/templates/product/uc-template.md +++ /dev/null @@ -1,69 +0,0 @@ -# Product Inception UC - -## Metadata - -- **Artifact**: `inception/product/uc.md` -- **Command**: `/speckit.inception.product` -- **Status**: draft | ready | blocked -- **Last Updated**: - -## Product Idea - -Describe the confirmed product idea in one paragraph. - -## Target Users - -| Role | Goal | Context | Evidence | -| --- | --- | --- | --- | -| | | | confirmed | needs-clarification | - -## User Goals - -| Goal ID | User Goal | Success Signal | Evidence | -| --- | --- | --- | --- | -| UG-001 | | | confirmed | - -## Main Flow - -1. TBD - -## Alternate Flows - -| Flow ID | Trigger | User Path | Outcome | Evidence | -| --- | --- | --- | --- | --- | -| AF-001 | | | | confirmed | - -## Exception Flows - -| Flow ID | Condition | User Feedback | Recovery | Evidence | -| --- | --- | --- | --- | --- | -| EF-001 | | | | confirmed | - -## Success Criteria - -| Criterion | Measurement | Evidence | -| --- | --- | --- | -| | | confirmed | - -## Business Boundaries - -| Boundary | In Scope | Out of Scope | Evidence | -| --- | --- | --- | --- | -| | | | confirmed | - -## This Round Will Not Do - -- TBD - -## Open Questions - -- [NEEDS CLARIFICATION: ] - -## Quality Gate - -- `OUTPUT_PATH_MISMATCH`: not evaluated | pass | blocked -- `SOURCE_PRIORITY_VIOLATION`: not evaluated | pass | blocked -- `SCOPE_LEAK`: not evaluated | pass | blocked -- `UNSUPPORTED_INFERENCE`: not evaluated | pass | blocked -- `EMPTY_PRIMARY_ARTIFACT`: not evaluated | pass | blocked -- `TEMPLATE_BYPASS`: not evaluated | pass | blocked diff --git a/extensions/inception/templates/product/wireflow-high-template.html b/extensions/inception/templates/product/wireflow-high-template.html deleted file mode 100644 index b3edaffcf8..0000000000 --- a/extensions/inception/templates/product/wireflow-high-template.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - Product Inception Wireflow High - - - -
-

Product Inception Wireflow High

-

Source: inception/product/uc.md

- -
-

High-Fidelity Screen Flow

-
-

Screen derived from UC

-

Layout, fields, controls, and feedback must trace to UC evidence.

-
-
- -
-

Required State Coverage

- loading - success - empty - error - disabled - permission denied - submitting - submitted -
- -
-

Interaction Matrix

- - - - - - - -
ControlUser ActionSystem ResponseState ChangeUC Evidence
derived-from-uc
-
- -
-

Visual Acceptance Standards

- - - - - - - -
SurfaceAcceptance RuleEvidence
derived-from-uc
-
- -
-

Open Questions

-
    -
  • [NEEDS CLARIFICATION: ]
  • -
-
-
- - diff --git a/extensions/inception/templates/product/wireflow-medium-template.html b/extensions/inception/templates/product/wireflow-medium-template.html deleted file mode 100644 index ba788d1bf2..0000000000 --- a/extensions/inception/templates/product/wireflow-medium-template.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - Product Inception Wireflow Medium - - - -
-

Product Inception Wireflow Medium

-

Source: inception/product/uc.md

- -
-

Page Flow

-
Start node derived from UC
-
Decision or interaction node derived from UC
-
Outcome node derived from UC
-
- -
-

Page Responsibilities

- - - - - - - -
Page or NodeResponsibilityUser ActionSystem FeedbackUC Evidence
derived-from-uc
-
- -
-

Branch and Exception Paths

- - - - - - - -
ConditionPathFeedbackRecoveryUC Evidence
derived-from-uc
-
- -
-

Open Questions

-
    -
  • [NEEDS CLARIFICATION: ]
  • -
-
-
- - diff --git a/extensions/inception/tests/test_extension_contract.py b/extensions/inception/tests/test_extension_contract.py deleted file mode 100644 index 805fc7e671..0000000000 --- a/extensions/inception/tests/test_extension_contract.py +++ /dev/null @@ -1,16 +0,0 @@ -from pathlib import Path - - -EXTENSION_DIR = Path(__file__).resolve().parents[1] - - -def test_retired_architecture_entrypoint_is_write_free_and_portable(): - command = ( - EXTENSION_DIR / "commands" / "speckit.inception.arch.md" - ).read_text(encoding="utf-8") - - assert "INCEPTION_ARCH_COMMAND_RETIRED" in command - assert "__SPECKIT_COMMAND_CONSTITUTION__" in command - assert "Do not create `inception/arch/`" in command - assert "Do not read `inception/product/uc.md` by default" in command - assert "/speckit." not in command diff --git a/extensions/intake/.extensionignore b/extensions/intake/.extensionignore deleted file mode 100644 index 24052f3cc5..0000000000 --- a/extensions/intake/.extensionignore +++ /dev/null @@ -1,9 +0,0 @@ -.git/ -.github/ -.gitignore -.pytest_cache/ -.tmp/ -__pycache__/ -*.pyc -tests/ -docs/ diff --git a/extensions/intake/.github/workflows/community-source-contract.yml b/extensions/intake/.github/workflows/community-source-contract.yml deleted file mode 100644 index f762462422..0000000000 --- a/extensions/intake/.github/workflows/community-source-contract.yml +++ /dev/null @@ -1,172 +0,0 @@ -name: Community Source Contract - -permissions: - contents: read - -on: - pull_request: - push: - branches: ["main"] - workflow_dispatch: - -concurrency: - group: community-source-contract-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - source-contract: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 1 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.11" - - - name: Install manifest validation dependencies - run: python3 -m pip install --upgrade PyYAML jsonschema - - - name: Validate source manifest contract - run: | - python3 - <<'PY' - import re - from pathlib import Path - - import yaml - - root = Path.cwd() - manifest_path = root / "extension.yml" - manifest_kind = "extension" - if not manifest_path.is_file(): - manifest_path = root / "preset.yml" - manifest_kind = "preset" - if not manifest_path.is_file(): - raise SystemExit("expected extension.yml or preset.yml") - - data = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) - if not isinstance(data, dict): - raise SystemExit(f"{manifest_path.name} must contain a mapping") - if data.get("schema_version") != "1.0": - raise SystemExit("schema_version must be 1.0") - - meta = data.get(manifest_kind) - if not isinstance(meta, dict): - raise SystemExit(f"missing {manifest_kind} metadata") - - item_id = meta.get("id") - version = meta.get("version") - repository = meta.get("repository") - if not isinstance(item_id, str) or not re.fullmatch(r"[a-z][a-z0-9-]*", item_id): - raise SystemExit(f"invalid {manifest_kind} id: {item_id!r}") - if not isinstance(version, str) or not re.fullmatch(r"\d+\.\d+\.\d+", version): - raise SystemExit(f"invalid semver version: {version!r}") - if not isinstance(repository, str) or not repository.startswith("https://github.com/"): - raise SystemExit("repository must be an https GitHub URL") - - for required_file in ("README.md", "LICENSE", "CHANGELOG.md"): - if not (root / required_file).is_file(): - raise SystemExit(f"missing {required_file}") - - requires = data.get("requires") - if not isinstance(requires, dict) or not requires.get("speckit_version"): - raise SystemExit("requires.speckit_version is required") - - provides = data.get("provides") - if not isinstance(provides, dict): - raise SystemExit("provides must be a mapping") - - referenced_files = [] - if manifest_kind == "extension": - commands = provides.get("commands") - if not isinstance(commands, list) or not commands: - raise SystemExit("extension provides.commands must be a non-empty list") - for command in commands: - if not isinstance(command, dict): - raise SystemExit("command entries must be mappings") - name = command.get("name") - file_name = command.get("file") - if not isinstance(name, str) or not name.startswith("speckit."): - raise SystemExit(f"invalid command name: {name!r}") - if not isinstance(file_name, str): - raise SystemExit(f"command {name!r} missing file") - referenced_files.append(file_name) - for config in provides.get("config", []) or []: - if isinstance(config, dict) and isinstance(config.get("template"), str): - referenced_files.append(config["template"]) - else: - templates = provides.get("templates") - if not isinstance(templates, list) or not templates: - raise SystemExit("preset provides.templates must be a non-empty list") - for template in templates: - if not isinstance(template, dict): - raise SystemExit("template entries must be mappings") - file_name = template.get("file") - if not isinstance(file_name, str): - raise SystemExit(f"template {template.get('name')!r} missing file") - referenced_files.append(file_name) - - missing = [path for path in referenced_files if not (root / path).is_file()] - if missing: - raise SystemExit(f"manifest references missing files: {missing}") - - tags = data.get("tags") - if not isinstance(tags, list) or not tags: - raise SystemExit("tags must be a non-empty list") - PY - - - name: Validate integration repository boundary - run: | - python3 - <<'PY' - from pathlib import Path - - blocked_patterns = ( - "gh pr create --repo " + "github/spec-kit", - "repos/" + "github/spec-kit" + "/dispatches", - "repos/" + "github/spec-kit" + "/pulls", - "github.com/" + "github/spec-kit" + "/compare", - ) - offenders = [] - for workflow in Path(".github/workflows").glob("*.yml"): - text = workflow.read_text(encoding="utf-8") - for pattern in blocked_patterns: - if pattern in text: - offenders.append(f"{workflow}: {pattern}") - if offenders: - raise SystemExit( - "source repositories must not automate direct submissions to github/spec-kit:\n" - + "\n".join(offenders) - ) - PY - - - name: Install project test dependencies - if: hashFiles('requirements-dev.txt') != '' - run: python3 -m pip install -r requirements-dev.txt - - - name: Run shell contract tests - if: hashFiles('tests/repository-first-contract.sh') != '' - run: bash tests/repository-first-contract.sh - - - name: Run extension validator - if: hashFiles('tests/validate-extension.py') != '' - run: python3 tests/validate-extension.py - - - name: Run Python tests from requirements projects - if: hashFiles('requirements-dev.txt') != '' && hashFiles('tests/test_*.py') != '' - run: | - python3 - <<'PY' - import importlib.util - import subprocess - import sys - - if importlib.util.find_spec("pytest") is not None: - subprocess.run([sys.executable, "-m", "pytest", "-q"], check=True) - else: - subprocess.run( - [sys.executable, "-m", "unittest", "discover", "-s", "tests", "-p", "test_*.py"], - check=True, - ) - PY diff --git a/extensions/intake/.github/workflows/extension-artifact.yml b/extensions/intake/.github/workflows/extension-artifact.yml deleted file mode 100644 index 9d3dde8f3e..0000000000 --- a/extensions/intake/.github/workflows/extension-artifact.yml +++ /dev/null @@ -1,159 +0,0 @@ -name: Extension Release Artifact - -permissions: - contents: read - -on: - push: - tags: ["v*"] - workflow_dispatch: - inputs: - version: - description: "Release version. Defaults to extension.yml or tag version." - required: false - type: string - -concurrency: - group: extension-release-artifact-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - -jobs: - build-and-verify: - runs-on: ubuntu-latest - env: - INPUT_VERSION: ${{ inputs.version || '' }} - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 1 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.11" - - - name: Install packaging dependencies - run: python3 -m pip install --upgrade PyYAML - - - name: Build and verify extension artifact - id: artifact - run: | - python3 - <<'PY' - import os - import json - import re - import zipfile - from pathlib import Path - from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo - - import yaml - - root = Path.cwd() - manifest = yaml.safe_load(Path("extension.yml").read_text(encoding="utf-8")) - extension = manifest["extension"] - extension_id = extension["id"] - manifest_version = extension["version"] - tag_version = os.environ.get("GITHUB_REF_NAME", "") - if tag_version.startswith("v"): - tag_version = tag_version[1:] - input_version = os.environ.get("INPUT_VERSION", "") - version = input_version or tag_version or manifest_version - - if not re.fullmatch(r"\d+\.\d+\.\d+", version): - raise SystemExit(f"version must use X.Y.Z format: {version}") - if tag_version and version != tag_version: - raise SystemExit(f"version {version} does not match tag v{tag_version}") - if version != manifest_version: - raise SystemExit(f"version {version} does not match extension.yml {manifest_version}") - - include_roots = [ - "extension.yml", - "README.md", - "CHANGELOG.md", - "LICENSE", - "commands", - "scripts", - "templates", - "schemas", - "config-template.yml", - ] - forbidden_prefixes = (".git/", ".github/", "tests/", "docs/superpowers/") - files = [] - for item in include_roots: - path = root / item - if path.is_file(): - files.append(path) - elif path.is_dir(): - files.extend(file for file in path.rglob("*") if file.is_file()) - - zip_name = f"{extension_id}-v{version}.zip" - with ZipFile(zip_name, "w", compression=ZIP_DEFLATED) as archive: - for file_path in sorted(files, key=lambda path: path.relative_to(root).as_posix()): - name = file_path.relative_to(root).as_posix() - if name.startswith(forbidden_prefixes): - continue - info = ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0)) - info.compress_type = ZIP_DEFLATED - info.external_attr = 0o644 << 16 - archive.writestr(info, file_path.read_bytes()) - - required = {"extension.yml", "README.md", "LICENSE"} - for command in manifest["provides"]["commands"]: - required.add(command["file"]) - for config in manifest["provides"].get("config", []) or []: - template = config.get("template") - if template: - required.add(template) - - with zipfile.ZipFile(zip_name) as archive: - names = set(archive.namelist()) - missing = sorted(required - names) - if missing: - raise SystemExit(f"artifact missing required entries: {missing}") - forbidden = sorted(name for name in names if name.startswith(forbidden_prefixes)) - if forbidden: - raise SystemExit(f"artifact contains forbidden entries: {forbidden[:10]}") - - repository = os.environ["GITHUB_REPOSITORY"] - source_commit = os.environ["GITHUB_SHA"] - if tag_version: - download_url = f"https://github.com/{repository}/archive/refs/tags/v{version}.zip" - else: - download_url = f"https://github.com/{repository}/archive/{source_commit}.zip" - provenance = { - "repository_url": f"https://github.com/{repository}", - "release_version": version, - "source_commit_sha": source_commit, - "download_url": download_url, - "validation_evidence": { - "workflow": os.environ["GITHUB_WORKFLOW"], - "run_id": os.environ["GITHUB_RUN_ID"], - "artifact_name": zip_name, - "manifest_version": manifest_version, - "required_entries_verified": sorted(required), - "forbidden_prefixes_checked": list(forbidden_prefixes), - }, - } - provenance_name = "release-provenance.json" - Path(provenance_name).write_text( - json.dumps(provenance, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - - with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: - output.write(f"zip_name={zip_name}\n") - output.write(f"provenance_name={provenance_name}\n") - PY - - - name: Upload verified artifact - uses: actions/upload-artifact@v4 - with: - name: ${{ steps.artifact.outputs.zip_name }} - path: ${{ steps.artifact.outputs.zip_name }} - - - name: Upload release provenance - uses: actions/upload-artifact@v4 - with: - name: release-provenance - path: ${{ steps.artifact.outputs.provenance_name }} diff --git a/extensions/intake/.gitignore b/extensions/intake/.gitignore deleted file mode 100644 index 1e94d05bf7..0000000000 --- a/extensions/intake/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -__pycache__/ -*.py[cod] -.pytest_cache/ -.mypy_cache/ -.ruff_cache/ -.coverage -htmlcov/ -.tmp/ -tmp/ -temp/ -.venv/ -venv/ -dist/ -build/ -*.egg-info/ -*.zip -*.log -.env -.env.* -!.env.example -.DS_Store -Thumbs.db -.worktrees/ diff --git a/extensions/intake/AGENTS.md b/extensions/intake/AGENTS.md deleted file mode 100644 index 1d9c955434..0000000000 --- a/extensions/intake/AGENTS.md +++ /dev/null @@ -1,33 +0,0 @@ -# Intake Extension - -This repository is a Spec Kit community extension source project for `intake`. - -## Project Shape - -- `extension.yml` is the extension manifest and must stay aligned with declared files. -- `commands/` contains Spec Kit command templates. -- `templates/` contains intake contracts, evidence packet templates, and schemas. -- `scripts/python/` contains intake validators. -- `tests/test_extension_contract.py` is the main contract test suite. - -## Source Workflow - -- Develop intake command, validator, schema, and template changes in this repository. -- Release versioned source artifacts from this repository. -- Keep `README.md`, `CHANGELOG.md`, `extension.yml`, validators, templates, and tests aligned. -- Run `python -m pytest -q` after behavior, validator, schema, template, or manifest changes. - -## Integration Boundary - -- This repository owns source development and release artifacts only. -- Do not open pull requests from this repository directly to `github/spec-kit`. -- Do not push branches to `github/spec-kit` or add workflow automation that targets `github/spec-kit` for pull requests, repository dispatches, or direct writes. -- If a Spec Kit catalog or bundled snapshot update is needed, target the `bigsmartben/spec-kit` integration fork first. The integration fork owns any downstream pull request to `github/spec-kit`. -- Source releases must provide source-backed metadata for the integration fork: repository URL, release version, source commit SHA, download URL, and validation evidence. - -## Handoff - -- changed files -- commands run -- validation result -- unresolved risks diff --git a/extensions/intake/CHANGELOG.md b/extensions/intake/CHANGELOG.md deleted file mode 100644 index cb2b9972d3..0000000000 --- a/extensions/intake/CHANGELOG.md +++ /dev/null @@ -1,22 +0,0 @@ -# Changelog - -## [0.2.0] - 2026-07-07 - -### Breaking Reset - -- 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 - -- 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 - -- 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/LICENSE b/extensions/intake/LICENSE deleted file mode 100644 index 14fac913cc..0000000000 --- a/extensions/intake/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/extensions/intake/README.md b/extensions/intake/README.md deleted file mode 100644 index 0491f1a6ed..0000000000 --- a/extensions/intake/README.md +++ /dev/null @@ -1,180 +0,0 @@ -# Spec Kit Intake Extension - -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 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 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. - -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 - -- PRDs, product briefs, Markdown documents, PDFs, and exported docs -- Low-fidelity, medium-fidelity, and high-fidelity design drafts -- 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 -- 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 - -| 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`, `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 must not convert source evidence directly into downstream-owned requirement IDs, implementation tasks, or code component names. - -## Commands - -- `/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. - -## Artifact Layout - -```text -specs//intake/ -├── prd/ -│ ├── source-manifest.yaml -│ ├── source-files/ -│ ├── prd-intake.yaml -│ └── evidence-packet.md -├── visual-design/ -│ ├── design-source-manifest.yaml -│ ├── source-files/ -│ ├── visual-requirements.yaml -│ ├── figma-metadata.part-001.xml -│ ├── figma-metadata.index.yaml -│ ├── figma-node-inventory.yaml -│ ├── figma-normalized-tree.yaml -│ ├── visual-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/ - ├── test-case-intake.yaml - └── 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, visual requirements, visual IR, and delivery parity evidence. - -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 - -- Spec Kit `>=0.8.10.dev0` -- Python validator dependencies: `PyYAML` and `jsonschema` -- Optional: `figma-mcp` for Figma metadata capture - -## Install for Local Development - -From a Spec Kit project: - -```bash -specify extension add --dev C:/Users/24598/Documents/github/spec-kit-intake -``` - -## Install from Release - -From a Spec Kit project: - -```bash -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: - -- `repository_url` -- `release_version` -- `source_commit_sha` -- `download_url` -- `validation_evidence` - -Then run: - -```text -/speckit.intake.visual-design -/speckit.intake.prd capture -/speckit.intake.prd validate -/speckit.intake.test-cases capture -/speckit.intake.test-cases validate -``` - -## Visual Design Readiness Gate - -Visual design intake passes only when: - -- source identity, fidelity level, and source-file integrity are proven -- low, medium, or high-fidelity extraction rules are applied consistently -- 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 -- 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 - -## Static HTML Delivery Readiness Gate - -Static HTML delivery passes only when: - -- upstream visual-design intake is ready -- 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 - -Validate the manifest from the local `spec-kit` checkout: - -```bash -python -c "from pathlib import Path; from specify_cli.extensions import ExtensionManifest; ExtensionManifest(Path('extension.yml')); print('manifest ok')" -``` - -Validate visual-design source artifacts: - -```bash -python scripts/python/validate_visual_design_intake.py specs//intake/visual-design -``` - -Validate static HTML delivery artifacts: - -```bash -python scripts/python/validate_static_html_delivery.py specs//intake/visual-design/delivery -``` - -Validate PRD artifacts: - -```bash -python scripts/python/validate_prd_intake.py specs//intake/prd -``` - -Validate test-case artifacts: - -```bash -python scripts/python/validate_test_cases_intake.py specs//intake/test-cases -``` diff --git a/extensions/intake/commands/speckit.intake.prd.md b/extensions/intake/commands/speckit.intake.prd.md deleted file mode 100644 index 5246ec4e03..0000000000 --- a/extensions/intake/commands/speckit.intake.prd.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -description: Capture PRD intake for the active Spec Kit feature. ---- - -## User Input - -```text -$ARGUMENTS -``` - -Classify the input before proceeding: - -- `source`: PRD file, Markdown document, PDF, URL, exported doc, issue, epic, or section scope -- `intake_dir`: existing PRD intake artifact directory -- `validation_request`: validate, check, gate, or readiness request -- `review_guidance`: source precedence, extraction scope, or reviewer instructions - -## Goal - -Create, update, or validate PRD intake artifacts for the active Spec Kit feature. Intake preserves reachable source files, stable source refs, checksums or retrieval metadata, and schema-required source-backed product facts so downstream SDD workflows can project requirements, scope, acceptance criteria, and clarification items with traceability. - -Default output directory: - -```text -specs//intake/prd/ -``` - -Normative authority: - -- `templates/schemas/*.json` defines machine-readable structure, required fields, types, and enums. -- `scripts/python/validate_prd_intake.py` defines readiness evaluation and blocker emission. -- `templates/intake-prd-contract.md` defines semantic extraction policy and source-domain terminology. -- This command only performs input routing, context loading, capture orchestration, validation invocation, and reporting. - -## Operating Boundaries - -- Preserve original PRD sources and record checksums or stable retrieval metadata before extraction. -- Extract source-backed product facts, not downstream-owned requirement IDs, implementation tasks, code component names, or unsupported product semantics. -- Keep ambiguous, missing, stale, or conflicting product facts explicit as `[NEEDS CLARIFICATION]`. -- Use stable provider-neutral evidence IDs and source refs that downstream workflows can map later. -- Do not mark intake ready unless source integrity, traceability, extraction completeness, and blocker review pass. -- Do not modify application source, tests, package manifests, feature implementation files, or existing Spec Kit core templates. - -## Context Loading - -1. Verify the current directory is a Spec Kit project by checking for `.specify/`. -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 recent feature directory only if there is a single clear candidate. - - If the feature cannot be identified, 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-prd-contract.md` and the referenced JSON Schemas from this extension before creating or validating artifacts. -5. Read any existing PRD intake artifacts and preserve valid evidence unless the user explicitly asks to recapture it. - -## Mode Routing - -- Capture mode: use when `$ARGUMENTS` names a PRD file, Markdown document, PDF, URL, issue, exported doc, section scope, or asks to capture, ingest, update, or recapture product evidence. -- Validate mode: use when `$ARGUMENTS` includes `validate`, `check`, `gate`, `readiness`, or only names an existing PRD 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 PRD source, document version, relevant sections, and target feature. -2. Preserve source identity and checksums in `source-manifest.yaml`. -3. Classify source-domain scenario coverage using `templates/intake-prd-contract.md`; do not define additional scenario categories in this command. -4. Extract source-backed facts into `prd-intake.yaml` according to `templates/schemas/prd-intake.schema.json` and the semantic policies in `templates/intake-prd-contract.md`. -5. For unavailable required evidence, record a structured gap or blocker instead of omitting the field. -6. Record conflicts, stale context, missing acceptance evidence, and unresolved decisions as explicit gaps instead of smoothing them into inferred requirements. -7. Create `evidence-packet.md` from `templates/intake-prd-evidence-packet-template.md` with readiness front matter and human-readable evidence notes; keep structured records in `prd-intake.yaml`. - -## Validation Procedure - -1. Resolve the PRD intake directory from `$ARGUMENTS` or the active feature. -2. Run: - -```bash -python .specify/extensions/intake/scripts/python/validate_prd_intake.py -``` - -3. Prefer `--json` when a machine-readable result is needed. Report the validator result exactly: - - `PASS` means the PRD artifacts passed JSON Schema structure checks and are ready for downstream projection as traceable product input. - - `BLOCKED` means downstream workflows must keep PRD-derived facts blocked, unresolved, or marked `[NEEDS CLARIFICATION]` instead of promoting unsupported product intent. - -## Readiness Authority - -Use this precedence when sources disagree: - -1. JSON Schemas are canonical for structural validity. -2. `validate_prd_intake.py` is canonical for readiness status and blocker codes. -3. `templates/intake-prd-contract.md` is canonical for semantic extraction policy. - -Do not restate, reinterpret, or override blocker codes in this command. - -## Report - -Return the mode executed, output or validated directory, source refs captured, extracted item counts, readiness result, blocker errors, next corrective action when blocked, and open clarification items. diff --git a/extensions/intake/commands/speckit.intake.test-cases.md b/extensions/intake/commands/speckit.intake.test-cases.md deleted file mode 100644 index cbaa50bde3..0000000000 --- a/extensions/intake/commands/speckit.intake.test-cases.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -description: Capture test-case intake for the active Spec Kit feature. ---- - -## User Input - -```text -$ARGUMENTS -``` - -Classify the input before proceeding: - -- `source`: test files, test management exports, QA spreadsheets, Gherkin feature files, issue links, or bug repros -- `intake_dir`: existing test-case intake artifact directory -- `validation_request`: validate, check, gate, or readiness request -- `review_guidance`: execution scope, framework/export format, source precedence, or reviewer instructions - -## Goal - -Create, update, or validate test-case intake artifacts for the active Spec Kit feature. Intake preserves reachable test sources, stable source refs, checksums or retrieval metadata, and schema-required behavioral evidence so downstream SDD workflows can project scenarios, assertions, fixtures, and coverage gaps without treating tests as the only source of truth. - -Default output directory: - -```text -specs//intake/test-cases/ -``` - -Normative authority: - -- `templates/schemas/*.json` defines machine-readable structure, required fields, types, and enums. -- `scripts/python/validate_test_cases_intake.py` defines readiness evaluation and blocker emission. -- `templates/intake-test-cases-contract.md` defines semantic extraction policy and source-domain terminology. -- This command only performs input routing, context loading, capture orchestration, validation invocation, and reporting. - -## Operating Boundaries - -- Preserve original test sources and record checksums, stable links, or export metadata before extraction. -- Treat tests as behavioral evidence, not as the sole product source of truth. -- Extract scenarios, assertions, fixtures, and coverage signals without inventing downstream-owned requirement IDs or implementation tasks. -- Keep skipped, flaky, obsolete, contradictory, or missing behavior explicit. -- Do not mark intake ready unless source integrity, traceability, scenario extraction, assertion capture, and gap reporting pass. -- Do not modify application source, tests, package manifests, feature implementation files, or existing Spec Kit core templates. - -## Context Loading - -1. Verify the current directory is a Spec Kit project by checking for `.specify/`. -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 recent feature directory only if there is a single clear candidate. - - If the feature cannot be identified, 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-test-cases-contract.md` and the referenced JSON Schemas from this extension before creating or validating artifacts. -5. Read any existing test-case intake artifacts and preserve valid evidence unless the user explicitly asks to recapture it. - -## Mode Routing - -- Capture mode: use when `$ARGUMENTS` names test files, a Gherkin feature, QA spreadsheet, test management export, issue link, framework, suite scope, or asks to capture, ingest, update, or recapture test evidence. -- Validate mode: use when `$ARGUMENTS` includes `validate`, `check`, `gate`, `readiness`, or only names an existing test-case 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 test sources, framework or export format, feature scope, and execution context. -2. Preserve source identity and checksums in `source-manifest.yaml`. -3. Classify source-domain scenario coverage using `templates/intake-test-cases-contract.md`; do not define additional scenario categories in this command. -4. Extract source-backed facts into `test-case-intake.yaml` according to `templates/schemas/test-case-intake.schema.json` and the semantic policies in `templates/intake-test-cases-contract.md`. -5. For unavailable required evidence, record a structured gap or blocker instead of omitting the field. -6. Record skipped tests, flaky cases, obsolete coverage, missing assertions, and product-intent inference as explicit gaps. -7. Create `evidence-packet.md` from `templates/intake-test-cases-evidence-packet-template.md` with readiness front matter and human-readable evidence notes; keep structured records in `test-case-intake.yaml`. - -## Validation Procedure - -1. Resolve the test-case intake directory from `$ARGUMENTS` or the active feature. -2. Run: - -```bash -python .specify/extensions/intake/scripts/python/validate_test_cases_intake.py -``` - -3. Prefer `--json` when a machine-readable result is needed. Report the validator result exactly: - - `PASS` means the test-case artifacts passed JSON Schema structure checks and are ready for downstream projection as traceable behavioral input. - - `BLOCKED` means downstream workflows must keep test-derived scenarios blocked, unresolved, or marked `[NEEDS CLARIFICATION]` instead of promoting unsupported behavior. - -## Readiness Authority - -Use this precedence when sources disagree: - -1. JSON Schemas are canonical for structural validity. -2. `validate_test_cases_intake.py` is canonical for readiness status and blocker codes. -3. `templates/intake-test-cases-contract.md` is canonical for semantic extraction policy. - -Do not restate, reinterpret, or override blocker codes in this command. - -## Report - -Return the mode executed, output or validated directory, source refs captured, scenario counts, readiness result, blocker errors, next corrective action when blocked, and coverage gaps. diff --git a/extensions/intake/commands/speckit.intake.visual-design.md b/extensions/intake/commands/speckit.intake.visual-design.md deleted file mode 100644 index c098e17788..0000000000 --- a/extensions/intake/commands/speckit.intake.visual-design.md +++ /dev/null @@ -1,194 +0,0 @@ ---- -description: Orchestrate visual design intake into a source-backed static HTML delivery for the active Spec Kit feature. ---- - -## User Input - -```text -$ARGUMENTS -``` - -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 - -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: - -- 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 - -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/ -``` - -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: - -- `delivery/index.html` -- `delivery/assets/` -- `delivery/screenshots/` -- `delivery/render-replay-report.yaml` -- `delivery/evidence-packet.md` - -## Operating Boundaries - -- 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. - -## Internal Workflow - -Run these phases internally. Do not ask the user to invoke a phase name. - -### 1. Resolve Context - -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. - -### 2. Capture Source Evidence - -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`. - -If source intake is blocked, stop and report validator blocker codes plus the next concrete recovery action. - -### 3. Build Visual IR - -Create or update the required files under `visual-ir/`: - -- 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 - -### 4. Clarification Gate - -Before building `delivery/index.html`, scan visual IR for missing information that affects visual parity or operation replay. - -Emit `ready_gate: BLOCKED` with `next_action: USER_CONFIRMATION_REQUIRED` when any required confirmation is unanswered. Ask concise, concrete questions. Each question must name: - -- `question_id` -- `target_artifact` -- `blocked_delivery_surface` -- `blocker_code` -- `allowed_answer_shape` - -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. - -The HTML must: - -- 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 - -### 6. Validate Delivery - -Run: - -```bash -python .specify/extensions/intake/scripts/python/validate_static_html_delivery.py /delivery -``` - -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: - -- 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 codes -- next corrective action when blocked - -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 deleted file mode 100644 index af1e4c2c9d..0000000000 --- a/extensions/intake/config-template.yml +++ /dev/null @@ -1,115 +0,0 @@ -# Intake Configuration - -artifacts: - base_dir: "specs/{feature}/intake" - visual_design_dir: "visual-design" - visual_ir_dir: "visual-ir" - static_html_delivery_dir: "delivery" - prd_dir: "prd" - test_cases_dir: "test-cases" - prd_source_manifest: "source-manifest.yaml" - visual_design_source_manifest: "design-source-manifest.yaml" - test_cases_source_manifest: "source-manifest.yaml" - source_files_dir: "source-files" - prd_intake: "prd-intake.yaml" - visual_requirements: "visual-requirements.yaml" - test_case_intake: "test-case-intake.yaml" - 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" - 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" - 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" - prd_source_manifest_schema: "templates/schemas/prd-source-manifest.schema.json" - prd_intake_schema: "templates/schemas/prd-intake.schema.json" - visual_design_source_manifest_schema: "templates/schemas/visual-source-manifest.schema.json" - visual_requirements_schema: "templates/schemas/visual-requirements.schema.json" - test_cases_source_manifest_schema: "templates/schemas/test-case-source-manifest.schema.json" - test_case_intake_schema: "templates/schemas/test-case-intake.schema.json" - 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" - 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" - figma_normalized_tree: "figma-normalized-tree.yaml" - prd_evidence_packet: "evidence-packet.md" - visual_design_evidence_packet: "visual-evidence-packet.md" - test_cases_evidence_packet: "evidence-packet.md" - comparison_report: "visual-comparison-report.md" - -readiness: - require_prd_intake: true - require_prd_acceptance_evidence: true - require_prd_clarification_marking: true - require_test_case_intake: true - require_test_assertions: true - require_test_fixture_evidence: true - require_test_coverage_gaps: true - allowed_source_types: - - "image" - - "pdf" - - "markdown" - - "figma" - allowed_fidelity_levels: - - "low" - - "medium" - - "high" - require_source_integrity: true - require_visual_requirements: true - require_visual_source_refs: true - require_fidelity_rules_applied: true - require_visual_parity_plan: true - require_raw_metadata_complete_for_figma: true - require_selected_subtree_complete_for_figma: true - require_node_inventory_coverage_for_figma: "100%" - require_parity_passed_for_figma: true - 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 - -capture: - preserve_original_sources: true - source_file_dir: "source-files" - prd_intake_file: "prd-intake.yaml" - visual_requirements_file: "visual-requirements.yaml" - test_case_intake_file: "test-case-intake.yaml" - shard_prefix: "figma-metadata.part-" - metadata_capture_mode: "sharded_file_staging" - figma_layout_normalization_mode: "derived_from_verified_metadata" - preserve_raw_metadata: true - reject_truncated_metadata_shards: true - 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 deleted file mode 100644 index a329c81ab4..0000000000 --- a/extensions/intake/extension.yml +++ /dev/null @@ -1,165 +0,0 @@ -schema_version: "1.0" - -extension: - id: intake - name: "Intake" - 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" - license: "MIT" - category: "docs" - effect: "read-write" - -requires: - speckit_version: ">=0.8.10.dev0" - tools: - - name: "figma-mcp" - required: false - -provides: - commands: - - name: "speckit.intake.visual-design" - file: "commands/speckit.intake.visual-design.md" - 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" - - name: "speckit.intake.test-cases" - file: "commands/speckit.intake.test-cases.md" - description: "Capture or validate existing test cases and normalize scenarios, assertions, fixtures, and coverage evidence for SDD workflows" - - config: - - name: "intake-config.yml" - template: "config-template.yml" - description: "Intake artifact paths and readiness defaults" - required: false - -hooks: - before_specify: - command: "speckit.intake.visual-design" - optional: true - 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" - - "sdd" - - "requirements" - - "validation" - - "figma" - - "static-html" - -defaults: - artifacts: - base_dir: "specs/{feature}/intake" - visual_design_dir: "visual-design" - visual_ir_dir: "visual-ir" - static_html_delivery_dir: "delivery" - prd_dir: "prd" - test_cases_dir: "test-cases" - prd_source_manifest: "source-manifest.yaml" - visual_design_source_manifest: "design-source-manifest.yaml" - test_cases_source_manifest: "source-manifest.yaml" - source_files_dir: "source-files" - prd_intake: "prd-intake.yaml" - visual_requirements: "visual-requirements.yaml" - test_case_intake: "test-case-intake.yaml" - 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" - 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" - 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" - prd_source_manifest_schema: "templates/schemas/prd-source-manifest.schema.json" - prd_intake_schema: "templates/schemas/prd-intake.schema.json" - visual_design_source_manifest_schema: "templates/schemas/visual-source-manifest.schema.json" - visual_requirements_schema: "templates/schemas/visual-requirements.schema.json" - test_cases_source_manifest_schema: "templates/schemas/test-case-source-manifest.schema.json" - test_case_intake_schema: "templates/schemas/test-case-intake.schema.json" - 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" - 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" - figma_normalized_tree: "figma-normalized-tree.yaml" - prd_evidence_packet: "evidence-packet.md" - visual_design_evidence_packet: "visual-evidence-packet.md" - test_cases_evidence_packet: "evidence-packet.md" - comparison_report: "visual-comparison-report.md" - readiness: - require_prd_intake: true - require_prd_acceptance_evidence: true - require_prd_clarification_marking: true - require_test_case_intake: true - require_test_assertions: true - require_test_fixture_evidence: true - require_test_coverage_gaps: true - allowed_source_types: - - "image" - - "pdf" - - "markdown" - - "figma" - allowed_fidelity_levels: - - "low" - - "medium" - - "high" - require_source_integrity: true - require_visual_requirements: true - require_visual_source_refs: true - require_fidelity_rules_applied: true - require_visual_parity_plan: true - require_raw_metadata_complete_for_figma: true - require_selected_subtree_complete_for_figma: true - require_node_inventory_coverage_for_figma: "100%" - require_parity_passed_for_figma: true - 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 - capture: - preserve_original_sources: true - source_file_dir: "source-files" - prd_intake_file: "prd-intake.yaml" - visual_requirements_file: "visual-requirements.yaml" - test_case_intake_file: "test-case-intake.yaml" - shard_prefix: "figma-metadata.part-" - metadata_capture_mode: "sharded_file_staging" - figma_layout_normalization_mode: "derived_from_verified_metadata" - preserve_raw_metadata: true - reject_truncated_metadata_shards: true - allow_screenshot_only_intake: false - default_screenshot_level: "source_blocked_until_delivery_validation" diff --git a/extensions/intake/requirements-dev.txt b/extensions/intake/requirements-dev.txt deleted file mode 100644 index 75cdb5c50d..0000000000 --- a/extensions/intake/requirements-dev.txt +++ /dev/null @@ -1,11 +0,0 @@ -pytest>=7.0 -pyyaml>=6.0 -jsonschema>=4.0 -typer>=0.24.0 -click>=8.2.1 -rich -platformdirs -readchar -packaging>=23.0 -pathspec>=0.12.0 -json5>=0.13.0 diff --git a/extensions/intake/scripts/python/capture_figma_metadata_shards.py b/extensions/intake/scripts/python/capture_figma_metadata_shards.py deleted file mode 100644 index 0a36589c77..0000000000 --- a/extensions/intake/scripts/python/capture_figma_metadata_shards.py +++ /dev/null @@ -1,394 +0,0 @@ -"""Stage sharded Figma metadata captures into visual-design intake artifacts.""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import shutil -import sys -from datetime import datetime, timezone -from pathlib import Path -from typing import Any -from xml.etree import ElementTree - - -SHARD_PREFIX = "figma-metadata.part-" -INDEX_NAME = "figma-metadata.index.yaml" -INVENTORY_NAME = "figma-node-inventory.yaml" - - -def sha256_bytes(data: bytes) -> str: - return hashlib.sha256(data).hexdigest() - - -def utc_now() -> str: - return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") - - -def yaml_scalar(value: Any) -> str: - if isinstance(value, bool): - return "true" if value else "false" - if isinstance(value, int): - return str(value) - if value is None: - return "null" - return json.dumps(str(value), ensure_ascii=False) - - -def write_mapping(path: Path, rows: list[tuple[str, Any]]) -> None: - lines: list[str] = [] - for key, value in rows: - if isinstance(value, list): - lines.append(f"{key}:") - if not value: - lines[-1] = f"{key}: []" - else: - for item in value: - if isinstance(item, dict): - lines.append(" - " + next(iter(item.keys())) + f": {yaml_scalar(next(iter(item.values())))}") - for sub_key, sub_value in list(item.items())[1:]: - write_nested(lines, sub_key, sub_value, indent=" ") - else: - lines.append(f" - {yaml_scalar(item)}") - else: - lines.append(f"{key}: {yaml_scalar(value)}") - path.write_text("\n".join(lines) + "\n", encoding="utf-8") - - -def write_nested(lines: list[str], key: str, value: Any, *, indent: str) -> None: - if isinstance(value, list): - if not value: - lines.append(f"{indent}{key}: []") - return - lines.append(f"{indent}{key}:") - for item in value: - if isinstance(item, dict): - lines.append(f"{indent} - " + next(iter(item.keys())) + f": {yaml_scalar(next(iter(item.values())))}") - for sub_key, sub_value in list(item.items())[1:]: - write_nested(lines, sub_key, sub_value, indent=indent + " ") - else: - lines.append(f"{indent} - {yaml_scalar(item)}") - return - lines.append(f"{indent}{key}: {yaml_scalar(value)}") - - -def is_metadata_source(path: Path) -> bool: - return path.suffix.lower() in {".xml", ".json", ".txt"} - - -def is_canonical_artifact(path: Path, intake_dir: Path) -> bool: - try: - path.relative_to(intake_dir.resolve()) - except ValueError: - return False - return path.name in {INDEX_NAME, INVENTORY_NAME} or ( - path.name.startswith(SHARD_PREFIX) and path.suffix.lower() == ".xml" - ) - - -def expand_sources(sources: list[Path], intake_dir: Path) -> list[Path]: - expanded: list[Path] = [] - for source in sources: - if source.is_dir(): - for path in sorted(source.rglob("*")): - if path.is_file() and is_metadata_source(path) and not is_canonical_artifact(path.resolve(), intake_dir): - expanded.append(path) - elif source.is_file(): - if not is_metadata_source(source): - raise SystemExit(f"metadata source must be .xml, .json, or .txt: {source}") - if is_canonical_artifact(source.resolve(), intake_dir): - raise SystemExit(f"metadata source must not be an existing canonical intake artifact: {source}") - expanded.append(source) - else: - raise SystemExit(f"metadata source does not exist: {source}") - if not expanded: - raise SystemExit("at least one metadata source file is required") - return expanded - - -def looks_truncated(text: str) -> bool: - lower = text.lower() - if "truncated" not in lower: - return False - allowed = [ - 'truncated="false"', - "truncated='false'", - '"truncated": false', - "'truncated': false", - "truncated: false", - ] - return not any(marker in lower for marker in allowed) - - -def extract_xml_node_ids(text: str) -> tuple[list[str], list[str], str | None]: - try: - root = ElementTree.fromstring(text) - except ElementTree.ParseError as exc: - return [], [], f"XML_PARSE_ERROR: {exc}" - - all_ids: list[str] = [] - for element in root.iter(): - node_id = element.attrib.get("id") - if node_id: - all_ids.append(str(node_id)) - - root_ids: list[str] = [] - root_id = root.attrib.get("id") - if root_id: - root_ids.append(str(root_id)) - else: - for child in list(root): - child_id = child.attrib.get("id") - if child_id: - root_ids.append(str(child_id)) - if not root_ids and all_ids: - root_ids.append(all_ids[0]) - return root_ids, all_ids, None - - -def extract_json_node_ids(text: str) -> tuple[list[str], list[str], str | None]: - try: - data = json.loads(text) - except json.JSONDecodeError as exc: - return [], [], f"JSON_PARSE_ERROR: {exc}" - - all_ids: list[str] = [] - - def walk(value: Any) -> None: - if isinstance(value, dict): - for key in ("id", "nodeId", "node_id"): - node_id = value.get(key) - if isinstance(node_id, (str, int)): - all_ids.append(str(node_id)) - break - for child in value.values(): - walk(child) - elif isinstance(value, list): - for item in value: - walk(item) - - walk(data) - root_ids: list[str] = [] - if isinstance(data, dict): - for key in ("id", "nodeId", "node_id"): - node_id = data.get(key) - if isinstance(node_id, (str, int)): - root_ids.append(str(node_id)) - break - if not root_ids: - document = data.get("document") - if isinstance(document, dict): - node_id = document.get("id") - if isinstance(node_id, (str, int)): - root_ids.append(str(node_id)) - if not root_ids and all_ids: - root_ids.append(all_ids[0]) - return root_ids, all_ids, None - - -def extract_node_ids(raw: bytes, source: Path) -> tuple[list[str], list[str], str | None]: - text = raw.decode("utf-8-sig", errors="replace") - suffix = source.suffix.lower() - if suffix == ".json": - return extract_json_node_ids(text) - if suffix in {".xml", ".txt", ""}: - root_ids, all_ids, error = extract_xml_node_ids(text) - if error and suffix == ".txt": - return extract_json_node_ids(text) - return root_ids, all_ids, error - return extract_xml_node_ids(text) - - -def sorted_unique(values: list[str]) -> list[str]: - return sorted(set(values)) - - -def build_artifacts(args: argparse.Namespace) -> dict[str, Any]: - intake_dir = args.intake_dir - intake_dir.mkdir(parents=True, exist_ok=True) - sources = expand_sources(args.metadata_source, intake_dir) - if args.node_id and len(args.node_id) not in {1, len(sources)}: - raise SystemExit("--node-id must be supplied once for all shards or once per metadata source") - - if args.overwrite: - for old in intake_dir.glob(f"{SHARD_PREFIX}*.xml"): - old.unlink() - for old_name in [INDEX_NAME, INVENTORY_NAME]: - old = intake_dir / old_name - if old.exists(): - old.unlink() - - captured_at = args.captured_at or utc_now() - design_version = args.design_version or captured_at - shard_rows: list[dict[str, Any]] = [] - all_node_ids: list[str] = [] - all_root_ids: list[str] = [] - gaps: list[dict[str, Any]] = [] - any_truncated = False - - for index, source in enumerate(sources, start=1): - raw = source.read_bytes() - text = raw.decode("utf-8-sig", errors="replace") - root_ids, node_ids, parse_error = extract_node_ids(raw, source) - if args.node_id: - root_ids = [args.node_id[index - 1] if len(args.node_id) == len(sources) else args.node_id[0]] - - truncated = looks_truncated(text) - any_truncated = any_truncated or truncated - if truncated: - gaps.append( - { - "code": "FIGMA_RAW_METADATA_TRUNCATED", - "source": str(source), - "reason": "metadata source contains a truncation marker", - } - ) - if parse_error: - gaps.append( - { - "code": "FIGMA_METADATA_PARITY_FAILED", - "source": str(source), - "reason": parse_error, - } - ) - if not node_ids: - gaps.append( - { - "code": "FIGMA_METADATA_PARITY_FAILED", - "source": str(source), - "reason": "no node ids were found in the metadata source", - } - ) - - shard_name = f"{SHARD_PREFIX}{index:03d}.xml" - shard_path = intake_dir / shard_name - if shard_path.exists() and not args.overwrite: - raise SystemExit(f"refusing to overwrite existing shard without --overwrite: {shard_path}") - shutil.copyfile(source, shard_path) - all_node_ids.extend(node_ids) - all_root_ids.extend(root_ids) - shard_rows.append( - { - "path": shard_name, - "byte_size": len(raw), - "sha256": sha256_bytes(raw), - "root_node_ids": root_ids, - "node_count": len(sorted_unique(node_ids)), - "truncated": truncated, - "source_path": str(source), - } - ) - - expected_roots = args.node_id or sorted_unique(all_root_ids) - captured_roots = sorted_unique(all_root_ids) - missing_roots = sorted(set(expected_roots) - set(captured_roots)) - duplicate_node_count = len(all_node_ids) - len(set(all_node_ids)) - raw_node_count = len(set(all_node_ids)) - metadata_complete = not any_truncated and not missing_roots and raw_node_count > 0 and not any( - gap["code"] == "FIGMA_METADATA_PARITY_FAILED" for gap in gaps - ) - selected_subtree_complete = metadata_complete - parity_passed = metadata_complete and duplicate_node_count == 0 - - index_rows: list[tuple[str, Any]] = [ - ("file_url", args.file_url), - ("file_key", args.file_key), - ("page_id", args.page_id), - ("selected_node_ids", expected_roots), - ("captured_at", captured_at), - ("mcp_tool", "get_metadata"), - ("design_version_or_timestamp", design_version), - ("selected_subtree_complete", selected_subtree_complete), - ("raw_metadata_complete", metadata_complete), - ("expected_root_node_ids", expected_roots), - ("captured_root_node_ids", captured_roots), - ("missing_root_node_ids", missing_roots), - ("gap_count", len(gaps)), - ("gaps", gaps), - ("shards", shard_rows), - ] - write_mapping(intake_dir / INDEX_NAME, index_rows) - - inventory_rows: list[tuple[str, Any]] = [ - ("raw_node_count", raw_node_count), - ("inventory_node_count", raw_node_count), - ("excluded_node_count", 0), - ("missing_node_count", len(missing_roots)), - ("duplicate_node_count", duplicate_node_count), - ("truncated_raw_evidence", any_truncated), - ("node_inventory_coverage", "100%" if parity_passed else "incomplete"), - ("parity_passed", parity_passed), - ("captured_root_node_ids", captured_roots), - ("missing_root_node_ids", missing_roots), - ] - write_mapping(intake_dir / INVENTORY_NAME, inventory_rows) - - return { - "status": "PASS" if metadata_complete and parity_passed else "BLOCKED", - "intake_dir": str(intake_dir), - "metadata_shards": [row["path"] for row in shard_rows], - "index": INDEX_NAME, - "inventory": INVENTORY_NAME, - "raw_node_count": raw_node_count, - "captured_root_node_ids": captured_roots, - "missing_root_node_ids": missing_roots, - "gaps": gaps, - "blockers": sorted( - { - gap["code"] - for gap in gaps - } - | ({"FIGMA_METADATA_PARITY_FAILED"} if not parity_passed else set()) - ), - } - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser( - description=( - "Copy already-sharded Figma get_metadata outputs into an intake directory, " - "then write figma-metadata.index.yaml and figma-node-inventory.yaml." - ) - ) - parser.add_argument("intake_dir", type=Path, help="visual-design intake directory") - parser.add_argument( - "--metadata-source", - type=Path, - action="append", - required=True, - help="Raw get_metadata response file or directory. Repeat for each shard.", - ) - parser.add_argument("--file-url", required=True, help="Stable Figma file URL") - parser.add_argument("--file-key", required=True, help="Figma file key") - parser.add_argument("--page-id", required=True, help="Figma page or source page id") - parser.add_argument( - "--node-id", - action="append", - help="Expected selected root node id. Repeat once per metadata source for exact root mapping.", - ) - parser.add_argument("--captured-at", help="Capture timestamp. Defaults to current UTC time.") - parser.add_argument("--design-version", help="Design version or timestamp. Defaults to captured-at.") - parser.add_argument("--overwrite", action="store_true", help="Replace existing canonical metadata artifacts.") - parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON result.") - return parser.parse_args(argv) - - -def main(argv: list[str] | None = None) -> int: - args = parse_args(argv or sys.argv[1:]) - result = build_artifacts(args) - if args.json: - print(json.dumps(result, indent=2, ensure_ascii=False, sort_keys=True)) - else: - print(f"Figma metadata shard capture: {result['status']}") - print(f"Output directory: {result['intake_dir']}") - print(f"Shards: {', '.join(result['metadata_shards'])}") - if result["blockers"]: - print("Blockers:") - for blocker in result["blockers"]: - print(f"- {blocker}") - return 0 if result["status"] == "PASS" else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/extensions/intake/scripts/python/intake_validator_common.py b/extensions/intake/scripts/python/intake_validator_common.py deleted file mode 100644 index 67501eff3a..0000000000 --- a/extensions/intake/scripts/python/intake_validator_common.py +++ /dev/null @@ -1,408 +0,0 @@ -"""Shared helpers for Spec Kit intake readiness validators.""" - -from __future__ import annotations - -import hashlib -import json -import re -from pathlib import Path -from typing import Any - -try: - from jsonschema import Draft202012Validator -except ImportError: # pragma: no cover - exercised in user environments - Draft202012Validator = None - -try: - import yaml -except ImportError: # pragma: no cover - exercised in user environments - yaml = None - - -def load_yaml(path: Path) -> dict[str, Any]: - if yaml is None: - raise RuntimeError("PyYAML is required to validate YAML intake artifacts") - data = yaml.safe_load(path.read_text(encoding="utf-8")) - return data if isinstance(data, dict) else {} - - -def sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def is_truthy(value: Any) -> bool: - return value is True or str(value).strip().lower() == "true" - - -def is_remote_ref(value: str) -> bool: - return value.startswith(("http://", "https://", "figma://")) - - -def is_supporting_visual_artifact_ref(value: Any) -> bool: - """Return true when a ref points at helper evidence, not a source-of-truth record.""" - ref = str(value or "").strip().replace("\\", "/").lower() - if not ref: - return False - supporting_markers = ( - "visual-evidence-packet.md", - "figma-evidence-packet.md", - "delivery/index.html", - "/delivery/", - "delivery/", - "/screenshots/", - "screenshots/", - "visual-diff", - "diff-output", - ) - return any(marker in ref for marker in supporting_markers) - - -def supporting_visual_artifact_refs(value: Any) -> list[str]: - if not isinstance(value, list): - return [] - return [str(ref).strip() for ref in value if is_supporting_visual_artifact_ref(ref)] - - -def has_needs_clarification(value: Any) -> bool: - if isinstance(value, str): - return "[NEEDS CLARIFICATION]" in value - if isinstance(value, dict): - return any(has_needs_clarification(item) for item in value.values()) - if isinstance(value, list): - return any(has_needs_clarification(item) for item in value) - return False - - -def non_empty(value: Any) -> bool: - return value not in (None, "", [], {}) - - -def validate_json_schema( - *, - instance_path: Path, - schema_name: str, - details_key: str, - details: dict[str, Any], - blocker_codes: list[str], - schema_error_code: str, -) -> None: - schema_details = details.setdefault("schema_validation", {}) - schema_detail: dict[str, Any] = { - "schema": schema_name, - "instance": str(instance_path), - "valid": False, - "errors": [], - } - schema_details[details_key] = schema_detail - - if Draft202012Validator is None: - schema_detail["errors"].append( - {"path": "$", "message": "jsonschema package is required for schema validation"} - ) - blocker_codes.append(schema_error_code) - return - - schema_path = Path(__file__).resolve().parents[2] / "templates" / "schemas" / schema_name - if not schema_path.exists(): - schema_detail["errors"].append({"path": "$", "message": f"schema file not found: {schema_path}"}) - blocker_codes.append(schema_error_code) - return - - instance = load_yaml(instance_path) - schema = json.loads(schema_path.read_text(encoding="utf-8")) - validator = Draft202012Validator(schema) - errors = sorted(validator.iter_errors(instance), key=lambda error: list(error.path)) - if errors: - for error in errors: - path = "$" - if error.path: - path += "".join( - f"[{part}]" if isinstance(part, int) else f".{part}" for part in error.path - ) - schema_detail["errors"].append({"path": path, "message": error.message}) - blocker_codes.append(schema_error_code) - return - - schema_detail["valid"] = True - - -def validate_source_manifest( - *, - intake_dir: Path, - manifest_name: str, - allowed_source_types: set[str], - required_manifest_fields: list[str], - required_source_file_fields: list[str], - details: dict[str, Any], - blocker_codes: list[str], - missing_manifest_code: str, - unsupported_source_code: str, - missing_file_code: str, - hash_mismatch_code: str, - integrity_code: str, - schema_name: str | None = None, - schema_error_code: str | None = None, -) -> str: - manifest_path = intake_dir / manifest_name - if not manifest_path.exists(): - blocker_codes.append(missing_manifest_code) - details["source_manifest"] = {"missing": True} - return "" - - if schema_name and schema_error_code: - validate_json_schema( - instance_path=manifest_path, - schema_name=schema_name, - details_key="source_manifest", - details=details, - blocker_codes=blocker_codes, - schema_error_code=schema_error_code, - ) - - manifest = load_yaml(manifest_path) - source_type = str(manifest.get("source_type") or "").strip().lower() - missing_manifest_fields = [ - field for field in required_manifest_fields if field not in manifest - ] - - details["source_manifest"] = { - "source_type": source_type, - "source_integrity_complete": manifest.get("source_integrity_complete"), - "captured_at": manifest.get("captured_at"), - "capture_method": manifest.get("capture_method"), - "snapshot_status": manifest.get("snapshot_status"), - "integrity_gap_reason": manifest.get("integrity_gap_reason"), - "missing_required_fields": missing_manifest_fields, - } - - for field in required_manifest_fields: - if field not in {"source_type", "source_integrity_complete", "captured_at", "capture_method", "source_files"}: - details["source_manifest"][field] = manifest.get(field) - - if missing_manifest_fields: - blocker_codes.append(integrity_code) - - if source_type not in allowed_source_types: - blocker_codes.append(unsupported_source_code) - - if not is_truthy(manifest.get("source_integrity_complete")): - blocker_codes.append(integrity_code) - - validate_source_files( - intake_dir=intake_dir, - manifest=manifest, - required_source_file_fields=required_source_file_fields, - details=details, - blocker_codes=blocker_codes, - missing_file_code=missing_file_code, - hash_mismatch_code=hash_mismatch_code, - integrity_code=integrity_code, - ) - return source_type - - -def validate_source_files( - *, - intake_dir: Path, - manifest: dict[str, Any], - required_source_file_fields: list[str], - details: dict[str, Any], - blocker_codes: list[str], - missing_file_code: str, - hash_mismatch_code: str, - integrity_code: str, -) -> None: - source_files = manifest.get("source_files") - if not isinstance(source_files, list) or not source_files: - blocker_codes.append(missing_file_code) - details["source_files"] = [] - return - - validated_files: list[dict[str, Any]] = [] - manifest_remote_gap = ( - str(manifest.get("snapshot_status") or "").strip() in {"not_available", "not_required"} - and non_empty(manifest.get("integrity_gap_reason")) - ) - for source_file in source_files: - if not isinstance(source_file, dict): - blocker_codes.append(missing_file_code) - continue - - rel_path = str(source_file.get("path") or "").strip() - remote_ref = is_remote_ref(rel_path) - remote_gap_fields = {"byte_size", "sha256"} if remote_ref and manifest_remote_gap else set() - missing_source_file_fields = [ - field - for field in required_source_file_fields - if field not in source_file and field not in remote_gap_fields - ] - expected = str(source_file.get("sha256") or "").replace("sha256:", "").strip() - file_detail: dict[str, Any] = { - "path": rel_path, - "exists": False, - "sha256_match": None, - "missing_required_fields": missing_source_file_fields, - "checksum_status": source_file.get("checksum_status"), - "snapshot_status": manifest.get("snapshot_status"), - "integrity_gap_reason": manifest.get("integrity_gap_reason"), - } - - if missing_source_file_fields: - blocker_codes.append(integrity_code) - - if not rel_path: - blocker_codes.append(missing_file_code) - validated_files.append(file_detail) - continue - - if remote_ref: - file_detail["exists"] = True - file_detail["remote_ref"] = True - validated_files.append(file_detail) - continue - - source_path = intake_dir / rel_path - if not source_path.exists(): - blocker_codes.append(missing_file_code) - validated_files.append(file_detail) - continue - - file_detail["exists"] = True - expected_size = source_file.get("byte_size") - if expected_size is not None: - try: - file_detail["byte_size_match"] = int(expected_size) == source_path.stat().st_size - if not file_detail["byte_size_match"]: - blocker_codes.append(hash_mismatch_code) - except (TypeError, ValueError): - blocker_codes.append(integrity_code) - if expected: - actual = sha256(source_path) - file_detail["sha256_match"] = expected == actual - if expected != actual: - blocker_codes.append(hash_mismatch_code) - - validated_files.append(file_detail) - - if not any(item.get("exists") for item in validated_files): - blocker_codes.append(missing_file_code) - - details["source_files"] = validated_files - - -def validate_ready_evidence_packet( - *, - intake_dir: Path, - details: dict[str, Any], - blocker_codes: list[str], - warnings: list[str], - missing_packet_code: str, - ready_without_evidence_code: str, -) -> None: - evidence_path = intake_dir / "evidence-packet.md" - if not evidence_path.exists(): - blocker_codes.append(missing_packet_code) - return - - details["evidence_packet"] = evidence_path.name - evidence_text = evidence_path.read_text(encoding="utf-8", errors="replace") - packet_status = parse_evidence_packet_status(evidence_text) - details["evidence_packet_metadata"] = packet_status["metadata"] - if packet_status["warnings"]: - warnings.extend(packet_status["warnings"]) - if packet_status["errors"]: - blocker_codes.append(ready_without_evidence_code) - 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": - blocker_codes.append(ready_without_evidence_code) - return - - if blocker_codes or has_packet_blockers: - blocker_codes.append(ready_without_evidence_code) - - -def parse_evidence_packet_status(evidence_text: str) -> dict[str, Any]: - text = evidence_text.lstrip("\ufeff") - result: dict[str, Any] = { - "ready_gate": "", - "metadata": {}, - "errors": [], - "warnings": [], - "used_front_matter": False, - } - front_matter = re.match(r"\A---\s*\r?\n(.*?)\r?\n---\s*(?:\r?\n|\Z)", text, re.DOTALL) - if front_matter: - result["used_front_matter"] = True - if yaml is None: - result["errors"].append("PyYAML is required to parse evidence packet front matter") - return result - metadata = yaml.safe_load(front_matter.group(1)) or {} - if not isinstance(metadata, dict): - result["errors"].append("evidence packet front matter must be a mapping") - return result - result["metadata"] = metadata - missing_fields = [ - field - for field in [ - "ready_gate", - "blockers", - "source_ref_count", - "extracted_item_count", - "generated_at", - ] - if field not in metadata - ] - if missing_fields: - result["errors"].append(f"missing evidence packet metadata fields: {', '.join(missing_fields)}") - ready_gate = str(metadata.get("ready_gate") or "").strip().upper() - if ready_gate not in {"PASS", "BLOCKED"}: - result["errors"].append("evidence packet ready_gate must be PASS or BLOCKED") - if not isinstance(metadata.get("blockers"), list): - result["errors"].append("evidence packet blockers must be an array") - for count_field in ["source_ref_count", "extracted_item_count"]: - value = metadata.get(count_field) - if not isinstance(value, int) or value < 0: - result["errors"].append(f"evidence packet {count_field} must be a non-negative integer") - if not non_empty(metadata.get("generated_at")): - result["errors"].append("evidence packet generated_at must be populated") - result["ready_gate"] = ready_gate - return result - - result["errors"].append("evidence packet readiness metadata not found") - return result - - -def emit( - *, - label: str, - json_mode: bool, - details: dict[str, Any], - blockers: list[str], - warnings: list[str], -) -> int: - result = { - "status": "BLOCKED" if blockers else "PASS", - "blockers": sorted(set(blockers)), - "warnings": warnings, - "details": details, - } - if json_mode: - print(json.dumps(result, indent=2, sort_keys=True)) - else: - print(f"{label} intake readiness: {result['status']}") - if result["blockers"]: - print("Blockers:") - for blocker in result["blockers"]: - print(f"- {blocker}") - if warnings: - print("Warnings:") - for warning in warnings: - print(f"- {warning}") - return 1 if result["blockers"] else 0 diff --git a/extensions/intake/scripts/python/normalize_figma_layout.py b/extensions/intake/scripts/python/normalize_figma_layout.py deleted file mode 100644 index 92daab001a..0000000000 --- a/extensions/intake/scripts/python/normalize_figma_layout.py +++ /dev/null @@ -1,350 +0,0 @@ -"""Derive a provider-neutral normalized Figma layout tree from raw metadata shards.""" - -from __future__ import annotations - -import argparse -import json -import re -import sys -from pathlib import Path -from typing import Any -from xml.etree import ElementTree - -try: - import yaml -except ImportError: # pragma: no cover - exercised in user environments - yaml = None - - -SHARD_GLOB = "figma-metadata.part-*.xml" -INDEX_NAME = "figma-metadata.index.yaml" -INVENTORY_NAME = "figma-node-inventory.yaml" -NORMALIZED_TREE_NAME = "figma-normalized-tree.yaml" - - -def load_yaml(path: Path) -> dict[str, Any]: - if yaml is None: - raise RuntimeError("PyYAML is required to normalize Figma layout metadata") - data = yaml.safe_load(path.read_text(encoding="utf-8")) - return data if isinstance(data, dict) else {} - - -def dump_yaml(path: Path, data: dict[str, Any]) -> None: - if yaml is None: - raise RuntimeError("PyYAML is required to write normalized Figma layout metadata") - path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=False), encoding="utf-8") - - -def slug(value: str) -> str: - text = re.sub(r"[^a-z0-9]+", "-", value.strip().lower()) - return text.strip("-") or "unnamed" - - -def normalize_name(name: str, node_id: str) -> str: - cleaned = re.sub(r"\s+", " ", name.strip()) - if cleaned: - return cleaned - return f"unnamed-{slug(node_id)}" - - -def infer_role_hint(tag: str, name: str, node_type: str) -> str: - haystack = f"{tag} {name} {node_type}".lower() - role_markers = [ - ("button", "button"), - ("input", "input"), - ("field", "input"), - ("nav", "navigation"), - ("header", "header"), - ("footer", "footer"), - ("image", "image"), - ("icon", "icon"), - ("text", "text"), - ("frame", "frame"), - ("component", "component"), - ("instance", "instance"), - ] - for marker, role in role_markers: - if marker in haystack: - return role - return "node" - - -def numeric_attr(attrs: dict[str, Any], names: tuple[str, ...]) -> float | None: - for name in names: - value = attrs.get(name) - if value is None: - continue - try: - return float(value) - except (TypeError, ValueError): - continue - return None - - -def bounds_from_attrs(attrs: dict[str, Any]) -> dict[str, float] | None: - x = numeric_attr(attrs, ("x", "absolute_x", "absoluteBoundingBox.x")) - y = numeric_attr(attrs, ("y", "absolute_y", "absoluteBoundingBox.y")) - width = numeric_attr(attrs, ("width", "w", "absoluteBoundingBox.width")) - height = numeric_attr(attrs, ("height", "h", "absoluteBoundingBox.height")) - bounds = { - key: value - for key, value in {"x": x, "y": y, "width": width, "height": height}.items() - if value is not None - } - return bounds or None - - -def flatten_json_attrs(value: dict[str, Any]) -> dict[str, Any]: - attrs = dict(value) - box = value.get("absoluteBoundingBox") - if isinstance(box, dict): - for key in ("x", "y", "width", "height"): - if key in box: - attrs[f"absoluteBoundingBox.{key}"] = box[key] - return attrs - - -def extract_xml_nodes(text: str, shard_name: str) -> list[dict[str, Any]]: - root = ElementTree.fromstring(text) - rows: list[dict[str, Any]] = [] - - def walk(element: ElementTree.Element, parent_id: str | None, depth: int, sibling_index: int) -> None: - attrs = dict(element.attrib) - node_id = attrs.get("id") - if node_id: - name = attrs.get("name") or attrs.get("label") or "" - node_type = attrs.get("type") or element.tag - rows.append( - { - "source_node_id": str(node_id), - "parent_source_node_id": parent_id, - "original_name": str(name), - "node_type": str(node_type), - "role_hint": infer_role_hint(element.tag, str(name), str(node_type)), - "bounds": bounds_from_attrs(attrs), - "depth": depth, - "sibling_index": sibling_index, - "source_refs": [f"{shard_name}#node={node_id}"], - } - ) - parent_id = str(node_id) - for index, child in enumerate(list(element)): - walk(child, parent_id, depth + 1, index) - - walk(root, None, 0, 0) - return rows - - -def extract_json_nodes(text: str, shard_name: str) -> list[dict[str, Any]]: - data = json.loads(text) - rows: list[dict[str, Any]] = [] - - def node_id_for(value: dict[str, Any]) -> str: - for key in ("id", "nodeId", "node_id"): - node_id = value.get(key) - if isinstance(node_id, (str, int)): - return str(node_id) - return "" - - def children_for(value: dict[str, Any]) -> list[Any]: - children = value.get("children") - if isinstance(children, list): - return children - document = value.get("document") - if isinstance(document, dict): - return [document] - return [] - - def walk(value: Any, parent_id: str | None, depth: int, sibling_index: int) -> None: - if not isinstance(value, dict): - return - node_id = node_id_for(value) - next_parent = parent_id - if node_id: - attrs = flatten_json_attrs(value) - name = value.get("name") or value.get("label") or "" - node_type = value.get("type") or value.get("node_type") or "node" - rows.append( - { - "source_node_id": node_id, - "parent_source_node_id": parent_id, - "original_name": str(name), - "node_type": str(node_type), - "role_hint": infer_role_hint("node", str(name), str(node_type)), - "bounds": bounds_from_attrs(attrs), - "depth": depth, - "sibling_index": sibling_index, - "source_refs": [f"{shard_name}#node={node_id}"], - } - ) - next_parent = node_id - for index, child in enumerate(children_for(value)): - walk(child, next_parent, depth + 1, index) - - walk(data, None, 0, 0) - return rows - - -def extract_nodes(path: Path) -> tuple[list[dict[str, Any]], str | None]: - text = path.read_text(encoding="utf-8-sig", errors="replace") - try: - return extract_xml_nodes(text, path.name), None - except ElementTree.ParseError: - try: - return extract_json_nodes(text, path.name), None - except json.JSONDecodeError as exc: - return [], f"{path.name}: metadata is neither parseable XML nor JSON: {exc}" - - -def sort_key_for(row: dict[str, Any]) -> tuple[float, float, int, int, str]: - bounds = row.get("bounds") - if not isinstance(bounds, dict): - bounds = {} - y = float(bounds.get("y", 0)) - x = float(bounds.get("x", 0)) - return (y, x, int(row.get("depth", 0)), int(row.get("sibling_index", 0)), row["source_node_id"]) - - -def build_normalized_tree(intake_dir: Path) -> dict[str, Any]: - index_path = intake_dir / INDEX_NAME - inventory_path = intake_dir / INVENTORY_NAME - index = load_yaml(index_path) if index_path.exists() else {} - inventory = load_yaml(inventory_path) if inventory_path.exists() else {} - gaps: list[dict[str, str]] = [] - - if not index_path.exists(): - gaps.append({"code": "FIGMA_METADATA_INDEX_MISSING", "reason": f"{INDEX_NAME} is required"}) - if not inventory_path.exists(): - gaps.append({"code": "FIGMA_METADATA_PARITY_FAILED", "reason": f"{INVENTORY_NAME} is required"}) - if index and not bool(index.get("raw_metadata_complete")): - gaps.append({"code": "FIGMA_READY_WITHOUT_COMPLETENESS_PROOF", "reason": "raw_metadata_complete is not true"}) - if index and not bool(index.get("selected_subtree_complete")): - gaps.append({"code": "FIGMA_SELECTED_SUBTREE_INCOMPLETE", "reason": "selected_subtree_complete is not true"}) - for shard in index.get("shards", []) if isinstance(index.get("shards"), list) else []: - if isinstance(shard, dict) and bool(shard.get("truncated")): - gaps.append({"code": "FIGMA_RAW_METADATA_TRUNCATED", "reason": f"{shard.get('path')} is marked truncated"}) - if inventory and not bool(inventory.get("parity_passed")): - gaps.append({"code": "FIGMA_METADATA_PARITY_FAILED", "reason": "parity_passed is not true"}) - if inventory and str(inventory.get("node_inventory_coverage") or "") != "100%": - gaps.append({"code": "FIGMA_METADATA_PARITY_FAILED", "reason": "node_inventory_coverage is not 100%"}) - if inventory and bool(inventory.get("truncated_raw_evidence")): - gaps.append({"code": "FIGMA_RAW_METADATA_TRUNCATED", "reason": "truncated_raw_evidence is true"}) - - nodes: list[dict[str, Any]] = [] - for shard in sorted(intake_dir.glob(SHARD_GLOB)): - shard_nodes, error = extract_nodes(shard) - if error: - gaps.append({"code": "FIGMA_NORMALIZED_TREE_INCOMPLETE", "reason": error}) - nodes.extend(shard_nodes) - - seen: set[str] = set() - deduped: list[dict[str, Any]] = [] - duplicate_ids: list[str] = [] - for node in nodes: - source_node_id = node["source_node_id"] - if source_node_id in seen: - duplicate_ids.append(source_node_id) - continue - seen.add(source_node_id) - deduped.append(node) - - sorted_nodes = sorted(deduped, key=sort_key_for) - group_keys_by_node: dict[str, str] = {} - for visual_order, node in enumerate(sorted_nodes, start=1): - normalized_name = normalize_name(node["original_name"], node["source_node_id"]) - group_key = f"{visual_order:04d}-{slug(normalized_name)}-{slug(node['source_node_id'])}" - group_keys_by_node[node["source_node_id"]] = group_key - node["normalized_name"] = normalized_name - node["group_key"] = group_key - node["visual_order"] = visual_order - node["sort_key"] = { - "method": "top_to_bottom_left_to_right_depth_sibling_id", - "value": list(sort_key_for(node)), - } - if node.get("bounds"): - node["bounds_ref"] = node["bounds"] - - for node in sorted_nodes: - parent_id = node.get("parent_source_node_id") - node["parent_group_key"] = group_keys_by_node.get(parent_id) if parent_id else None - node.pop("bounds", None) - node.pop("depth", None) - node.pop("sibling_index", None) - - raw_node_count = int(inventory.get("raw_node_count") or 0) - normalized_node_count = len(sorted_nodes) - coverage = "100%" if raw_node_count and raw_node_count == normalized_node_count and not gaps and not duplicate_ids else "incomplete" - if duplicate_ids: - gaps.append( - { - "code": "FIGMA_NORMALIZED_TREE_INCOMPLETE", - "reason": f"duplicate source_node_id values skipped: {', '.join(sorted(duplicate_ids))}", - } - ) - - return { - "normalization_complete": coverage == "100%", - "source_metadata_refs": [shard.name for shard in sorted(intake_dir.glob(SHARD_GLOB))], - "source_index_ref": INDEX_NAME, - "source_inventory_ref": INVENTORY_NAME, - "normalization_rules_applied": [ - "rename: normalized_name is derived from original_name without changing source_node_id", - "grouper: group_key is stable within the normalized tree and parent_group_key mirrors source containment", - "re-sort: visual_order uses top-to-bottom, left-to-right, depth, source sibling index, then source_node_id", - ], - "rename_rule": "preserve source_node_id and original_name; only normalized_name is provider-neutralized", - "group_rule": "derive group_key from normalized visual order, normalized_name, and source_node_id; preserve parent_group_key", - "sort_rule": "top_to_bottom_left_to_right_depth_sibling_id", - "raw_node_count": raw_node_count, - "normalized_node_count": normalized_node_count, - "node_coverage": coverage, - "selected_node_ids": index.get("selected_node_ids") or [], - "gaps": gaps, - "nodes": sorted_nodes, - } - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("intake_dir", type=Path, help="visual-design intake directory") - parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON result") - return parser.parse_args(argv) - - -def main(argv: list[str] | None = None) -> int: - args = parse_args(argv or sys.argv[1:]) - try: - tree = build_normalized_tree(args.intake_dir) - dump_yaml(args.intake_dir / NORMALIZED_TREE_NAME, tree) - except Exception as exc: # pragma: no cover - command-line guard - result = {"status": "BLOCKED", "blockers": ["FIGMA_NORMALIZED_TREE_MISSING"], "error": str(exc)} - if args.json: - print(json.dumps(result, indent=2, sort_keys=True)) - else: - print("Figma layout normalization: BLOCKED") - print(str(exc)) - return 1 - - blockers = [] if tree["normalization_complete"] else ["FIGMA_NORMALIZED_TREE_INCOMPLETE"] - result = { - "status": "PASS" if not blockers else "BLOCKED", - "blockers": blockers, - "output": str(args.intake_dir / NORMALIZED_TREE_NAME), - "raw_node_count": tree["raw_node_count"], - "normalized_node_count": tree["normalized_node_count"], - "node_coverage": tree["node_coverage"], - } - if args.json: - print(json.dumps(result, indent=2, sort_keys=True)) - else: - print(f"Figma layout normalization: {result['status']}") - print(f"Output: {result['output']}") - if blockers: - print("Blockers:") - for blocker in blockers: - print(f"- {blocker}") - return 0 if not blockers else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/extensions/intake/scripts/python/validate_prd_intake.py b/extensions/intake/scripts/python/validate_prd_intake.py deleted file mode 100644 index 3cc993aaf0..0000000000 --- a/extensions/intake/scripts/python/validate_prd_intake.py +++ /dev/null @@ -1,255 +0,0 @@ -#!/usr/bin/env python3 -"""Validate Spec Kit PRD intake artifacts.""" - -from __future__ import annotations - -import argparse -import sys -from pathlib import Path -from typing import Any - -from intake_validator_common import ( - emit, - has_needs_clarification, - is_truthy, - load_yaml, - non_empty, - validate_json_schema, - validate_ready_evidence_packet, - validate_source_manifest, -) - - -ALLOWED_SOURCE_TYPES = {"markdown", "pdf", "doc", "url", "issue", "mixed"} -ALLOWED_CATEGORIES = { - "goal", - "non_goal", - "user", - "actor", - "scope", - "flow", - "business_rule", - "data", - "permission", - "integration", - "compliance", - "acceptance", - "metric", - "risk", - "open_question", -} -ALLOWED_EVIDENCE_TYPES = {"observed", "inferred", "missing", "out_of_scope"} - -BLOCKERS = { - "SOURCE_MANIFEST_MISSING": "PRD_SOURCE_MANIFEST_MISSING", - "SOURCE_TYPE_UNSUPPORTED": "PRD_SOURCE_TYPE_UNSUPPORTED", - "SOURCE_FILE_MISSING": "PRD_SOURCE_FILE_MISSING", - "SOURCE_HASH_MISMATCH": "PRD_SOURCE_HASH_MISMATCH", - "SOURCE_INTEGRITY_INCOMPLETE": "PRD_SOURCE_INTEGRITY_INCOMPLETE", - "INTAKE_MISSING": "PRD_INTAKE_MISSING", - "FACTS_UNTRACEABLE": "PRD_FACTS_UNTRACEABLE", - "ACCEPTANCE_EVIDENCE_MISSING": "PRD_ACCEPTANCE_EVIDENCE_MISSING", - "CLARIFICATION_MARKING_MISSING": "PRD_CLARIFICATION_MARKING_MISSING", - "READY_WITHOUT_EVIDENCE": "PRD_READY_WITHOUT_EVIDENCE", - "EVIDENCE_PACKET_MISSING": "PRD_EVIDENCE_PACKET_MISSING", - "BLOCKER_LINT_ERRORS": "PRD_BLOCKER_LINT_ERRORS", - "SCHEMA_INVALID": "PRD_SCHEMA_INVALID", -} - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("intake_dir", help="Directory containing PRD intake artifacts") - parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON") - args = parser.parse_args() - - intake_dir = Path(args.intake_dir) - blocker_codes: list[str] = [] - warnings: list[str] = [] - details: dict[str, Any] = {"intake_dir": str(intake_dir)} - - if not intake_dir.exists() or not intake_dir.is_dir(): - blocker_codes.extend( - [ - BLOCKERS["SOURCE_MANIFEST_MISSING"], - BLOCKERS["INTAKE_MISSING"], - BLOCKERS["EVIDENCE_PACKET_MISSING"], - ] - ) - return emit( - label="PRD", - json_mode=args.json, - details=details, - blockers=blocker_codes, - warnings=warnings, - ) - - validate_source_manifest( - intake_dir=intake_dir, - manifest_name="source-manifest.yaml", - allowed_source_types=ALLOWED_SOURCE_TYPES, - required_manifest_fields=[ - "source_type", - "source_files", - "source_integrity_complete", - "captured_at", - "capture_method", - "document_version", - "extraction_scope", - ], - required_source_file_fields=["path", "mime_type", "byte_size", "sha256", "role"], - details=details, - blocker_codes=blocker_codes, - missing_manifest_code=BLOCKERS["SOURCE_MANIFEST_MISSING"], - unsupported_source_code=BLOCKERS["SOURCE_TYPE_UNSUPPORTED"], - missing_file_code=BLOCKERS["SOURCE_FILE_MISSING"], - hash_mismatch_code=BLOCKERS["SOURCE_HASH_MISMATCH"], - integrity_code=BLOCKERS["SOURCE_INTEGRITY_INCOMPLETE"], - schema_name="prd-source-manifest.schema.json", - schema_error_code=BLOCKERS["SCHEMA_INVALID"], - ) - - validate_prd_intake(intake_dir, details, blocker_codes) - validate_ready_evidence_packet( - intake_dir=intake_dir, - details=details, - blocker_codes=blocker_codes, - warnings=warnings, - missing_packet_code=BLOCKERS["EVIDENCE_PACKET_MISSING"], - ready_without_evidence_code=BLOCKERS["READY_WITHOUT_EVIDENCE"], - ) - - return emit( - label="PRD", - json_mode=args.json, - details=details, - blockers=blocker_codes, - warnings=warnings, - ) - - -def validate_prd_intake( - intake_dir: Path, - details: dict[str, Any], - blocker_codes: list[str], -) -> None: - intake_path = intake_dir / "prd-intake.yaml" - if not intake_path.exists(): - blocker_codes.append(BLOCKERS["INTAKE_MISSING"]) - return - - validate_json_schema( - instance_path=intake_path, - schema_name="prd-intake.schema.json", - details_key="prd_intake", - details=details, - blocker_codes=blocker_codes, - schema_error_code=BLOCKERS["SCHEMA_INVALID"], - ) - - intake_doc = load_yaml(intake_path) - facts = intake_doc.get("facts") - if not isinstance(facts, list): - facts = [] - - declared_fact_count = intake_doc.get("extracted_fact_count") - try: - extracted_fact_count = len(facts) if declared_fact_count is None else int(declared_fact_count) - except (TypeError, ValueError): - extracted_fact_count = len(facts) - intake_complete = is_truthy(intake_doc.get("prd_intake_complete")) - source_refs_complete = is_truthy(intake_doc.get("source_refs_complete")) - acceptance_evidence_complete = is_truthy(intake_doc.get("acceptance_evidence_complete")) - unresolved_ambiguity_marked = is_truthy(intake_doc.get("unresolved_ambiguity_marked")) - blocker_lint_errors = intake_doc.get("blocker_lint_errors") - - details["prd_intake"] = { - "prd_intake_complete": intake_doc.get("prd_intake_complete"), - "source_refs_complete": intake_doc.get("source_refs_complete"), - "extracted_fact_count": extracted_fact_count, - "acceptance_evidence_complete": intake_doc.get("acceptance_evidence_complete"), - "unresolved_ambiguity_marked": intake_doc.get("unresolved_ambiguity_marked"), - "blocker_lint_errors": blocker_lint_errors, - } - - if not intake_complete or extracted_fact_count <= 0: - blocker_codes.append(BLOCKERS["INTAKE_MISSING"]) - - count_matches = extracted_fact_count == len(facts) - if not count_matches: - blocker_codes.append(BLOCKERS["INTAKE_MISSING"]) - - fact_errors: list[dict[str, Any]] = [] - has_untraceable = not source_refs_complete - has_missing_required = False - has_acceptance_signal = acceptance_evidence_complete - has_clarification_marker = unresolved_ambiguity_marked or has_needs_clarification(intake_doc) - has_blocker_lint = isinstance(blocker_lint_errors, list) and len(blocker_lint_errors) > 0 - - required_fields = [ - "id", - "category", - "statement", - "source_refs", - "evidence_type", - "confidence", - "confidence_rationale", - "downstream_hint", - "acceptance_or_validation_signal", - ] - - for index, fact in enumerate(facts): - if not isinstance(fact, dict): - fact_errors.append({"index": index, "error": "fact must be a mapping"}) - has_missing_required = True - continue - - missing_fields = [field for field in required_fields if not non_empty(fact.get(field))] - if missing_fields: - fact_errors.append({"index": index, "missing_fields": missing_fields}) - has_missing_required = True - - source_refs = fact.get("source_refs") - if not isinstance(source_refs, list) or not source_refs or any(not str(ref).strip() for ref in source_refs): - has_untraceable = True - - category = str(fact.get("category") or "").strip().lower() - if category and category not in ALLOWED_CATEGORIES: - fact_errors.append({"index": index, "unsupported_category": category}) - has_missing_required = True - - evidence_type = str(fact.get("evidence_type") or "").strip().lower() - if evidence_type and evidence_type not in ALLOWED_EVIDENCE_TYPES: - fact_errors.append({"index": index, "unsupported_evidence_type": evidence_type}) - has_missing_required = True - - if category in {"acceptance", "open_question"} or non_empty(fact.get("acceptance_or_validation_signal")): - has_acceptance_signal = True - if has_needs_clarification(fact): - has_clarification_marker = True - blockers = fact.get("blockers") - if isinstance(blockers, list) and blockers: - has_blocker_lint = True - - if non_empty(intake_doc.get("acceptance_gaps")): - has_acceptance_signal = True - if non_empty(intake_doc.get("open_questions")): - has_clarification_marker = has_clarification_marker or has_needs_clarification(intake_doc.get("open_questions")) - - details["prd_intake"]["fact_errors"] = fact_errors - details["prd_intake"]["count_matches_facts"] = count_matches - - if has_missing_required: - blocker_codes.append(BLOCKERS["INTAKE_MISSING"]) - if has_untraceable: - blocker_codes.append(BLOCKERS["FACTS_UNTRACEABLE"]) - if not has_acceptance_signal: - blocker_codes.append(BLOCKERS["ACCEPTANCE_EVIDENCE_MISSING"]) - if not has_clarification_marker: - blocker_codes.append(BLOCKERS["CLARIFICATION_MARKING_MISSING"]) - if has_blocker_lint: - blocker_codes.append(BLOCKERS["BLOCKER_LINT_ERRORS"]) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/extensions/intake/scripts/python/validate_static_html_delivery.py b/extensions/intake/scripts/python/validate_static_html_delivery.py deleted file mode 100644 index a64de2c23c..0000000000 --- a/extensions/intake/scripts/python/validate_static_html_delivery.py +++ /dev/null @@ -1,675 +0,0 @@ -#!/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_test_cases_intake.py b/extensions/intake/scripts/python/validate_test_cases_intake.py deleted file mode 100644 index 4cfaaf26ec..0000000000 --- a/extensions/intake/scripts/python/validate_test_cases_intake.py +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/env python3 -"""Validate Spec Kit test-case intake artifacts.""" - -from __future__ import annotations - -import argparse -import sys -from pathlib import Path -from typing import Any - -from intake_validator_common import ( - emit, - has_needs_clarification, - is_truthy, - load_yaml, - non_empty, - validate_json_schema, - validate_ready_evidence_packet, - validate_source_manifest, -) - - -ALLOWED_SOURCE_TYPES = {"code", "gherkin", "spreadsheet", "test_management", "issue", "mixed"} -ALLOWED_CATEGORIES = { - "unit", - "component", - "integration", - "api", - "e2e", - "manual", - "regression", - "bug_repro", - "performance", - "accessibility", - "security", -} -ALLOWED_EVIDENCE_TYPES = {"observed", "inferred", "missing", "out_of_scope"} - -BLOCKERS = { - "SOURCE_MANIFEST_MISSING": "TEST_SOURCE_MANIFEST_MISSING", - "SOURCE_TYPE_UNSUPPORTED": "TEST_SOURCE_TYPE_UNSUPPORTED", - "SOURCE_FILE_MISSING": "TEST_SOURCE_FILE_MISSING", - "SOURCE_HASH_MISMATCH": "TEST_SOURCE_HASH_MISMATCH", - "SOURCE_INTEGRITY_INCOMPLETE": "TEST_SOURCE_INTEGRITY_INCOMPLETE", - "INTAKE_MISSING": "TEST_CASE_INTAKE_MISSING", - "SCENARIOS_UNTRACEABLE": "TEST_SCENARIOS_UNTRACEABLE", - "ASSERTIONS_MISSING": "TEST_ASSERTIONS_MISSING", - "FIXTURE_EVIDENCE_MISSING": "TEST_FIXTURE_EVIDENCE_MISSING", - "COVERAGE_GAPS_MISSING": "TEST_COVERAGE_GAPS_MISSING", - "READY_WITHOUT_EVIDENCE": "TEST_READY_WITHOUT_EVIDENCE", - "EVIDENCE_PACKET_MISSING": "TEST_EVIDENCE_PACKET_MISSING", - "BLOCKER_LINT_ERRORS": "TEST_BLOCKER_LINT_ERRORS", - "SCHEMA_INVALID": "TEST_SCHEMA_INVALID", -} - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("intake_dir", help="Directory containing test-case intake artifacts") - parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON") - args = parser.parse_args() - - intake_dir = Path(args.intake_dir) - blocker_codes: list[str] = [] - warnings: list[str] = [] - details: dict[str, Any] = {"intake_dir": str(intake_dir)} - - if not intake_dir.exists() or not intake_dir.is_dir(): - blocker_codes.extend( - [ - BLOCKERS["SOURCE_MANIFEST_MISSING"], - BLOCKERS["INTAKE_MISSING"], - BLOCKERS["EVIDENCE_PACKET_MISSING"], - ] - ) - return emit( - label="Test-case", - json_mode=args.json, - details=details, - blockers=blocker_codes, - warnings=warnings, - ) - - validate_source_manifest( - intake_dir=intake_dir, - manifest_name="source-manifest.yaml", - allowed_source_types=ALLOWED_SOURCE_TYPES, - required_manifest_fields=[ - "source_type", - "source_files", - "source_integrity_complete", - "captured_at", - "capture_method", - "framework_or_format", - "execution_scope", - ], - required_source_file_fields=["path", "mime_type", "byte_size", "sha256", "role"], - details=details, - blocker_codes=blocker_codes, - missing_manifest_code=BLOCKERS["SOURCE_MANIFEST_MISSING"], - unsupported_source_code=BLOCKERS["SOURCE_TYPE_UNSUPPORTED"], - missing_file_code=BLOCKERS["SOURCE_FILE_MISSING"], - hash_mismatch_code=BLOCKERS["SOURCE_HASH_MISMATCH"], - integrity_code=BLOCKERS["SOURCE_INTEGRITY_INCOMPLETE"], - schema_name="test-case-source-manifest.schema.json", - schema_error_code=BLOCKERS["SCHEMA_INVALID"], - ) - - validate_test_case_intake(intake_dir, details, blocker_codes) - validate_ready_evidence_packet( - intake_dir=intake_dir, - details=details, - blocker_codes=blocker_codes, - warnings=warnings, - missing_packet_code=BLOCKERS["EVIDENCE_PACKET_MISSING"], - ready_without_evidence_code=BLOCKERS["READY_WITHOUT_EVIDENCE"], - ) - - return emit( - label="Test-case", - json_mode=args.json, - details=details, - blockers=blocker_codes, - warnings=warnings, - ) - - -def validate_test_case_intake( - intake_dir: Path, - details: dict[str, Any], - blocker_codes: list[str], -) -> None: - intake_path = intake_dir / "test-case-intake.yaml" - if not intake_path.exists(): - blocker_codes.append(BLOCKERS["INTAKE_MISSING"]) - return - - validate_json_schema( - instance_path=intake_path, - schema_name="test-case-intake.schema.json", - details_key="test_case_intake", - details=details, - blocker_codes=blocker_codes, - schema_error_code=BLOCKERS["SCHEMA_INVALID"], - ) - - intake_doc = load_yaml(intake_path) - scenarios = intake_doc.get("scenarios") - if not isinstance(scenarios, list): - scenarios = [] - - declared_scenario_count = intake_doc.get("scenario_count") - try: - scenario_count = len(scenarios) if declared_scenario_count is None else int(declared_scenario_count) - except (TypeError, ValueError): - scenario_count = len(scenarios) - intake_complete = is_truthy(intake_doc.get("test_case_intake_complete")) - source_refs_complete = is_truthy(intake_doc.get("source_refs_complete")) - assertions_complete = is_truthy(intake_doc.get("assertions_complete")) - fixture_evidence_complete = is_truthy(intake_doc.get("fixture_evidence_complete")) - coverage_gaps_recorded = is_truthy(intake_doc.get("coverage_gaps_recorded")) - blocker_lint_errors = intake_doc.get("blocker_lint_errors") - - details["test_case_intake"] = { - "test_case_intake_complete": intake_doc.get("test_case_intake_complete"), - "source_refs_complete": intake_doc.get("source_refs_complete"), - "scenario_count": scenario_count, - "assertions_complete": intake_doc.get("assertions_complete"), - "fixture_evidence_complete": intake_doc.get("fixture_evidence_complete"), - "coverage_gaps_recorded": intake_doc.get("coverage_gaps_recorded"), - "blocker_lint_errors": blocker_lint_errors, - } - - if not intake_complete or scenario_count <= 0: - blocker_codes.append(BLOCKERS["INTAKE_MISSING"]) - - count_matches = scenario_count == len(scenarios) - if not count_matches: - blocker_codes.append(BLOCKERS["INTAKE_MISSING"]) - - scenario_errors: list[dict[str, Any]] = [] - has_untraceable = not source_refs_complete - has_missing_required = False - has_assertions = assertions_complete - has_fixture_evidence = fixture_evidence_complete - has_coverage_gaps = coverage_gaps_recorded - has_blocker_lint = isinstance(blocker_lint_errors, list) and len(blocker_lint_errors) > 0 - - required_fields = [ - "id", - "category", - "scenario", - "source_refs", - "evidence_type", - "confidence", - "confidence_rationale", - "actors", - "preconditions", - "actions", - "expected_outcomes", - "assertions", - "fixtures_or_test_data", - "coverage_signal", - ] - - for index, scenario in enumerate(scenarios): - if not isinstance(scenario, dict): - scenario_errors.append({"index": index, "error": "scenario must be a mapping"}) - has_missing_required = True - continue - - missing_fields = [ - field for field in required_fields if not non_empty(scenario.get(field)) - ] - if missing_fields: - scenario_errors.append({"index": index, "missing_fields": missing_fields}) - has_missing_required = True - - source_refs = scenario.get("source_refs") - if not isinstance(source_refs, list) or not source_refs or any(not str(ref).strip() for ref in source_refs): - has_untraceable = True - - category = str(scenario.get("category") or "").strip().lower() - if category and category not in ALLOWED_CATEGORIES: - scenario_errors.append({"index": index, "unsupported_category": category}) - has_missing_required = True - - evidence_type = str(scenario.get("evidence_type") or "").strip().lower() - if evidence_type and evidence_type not in ALLOWED_EVIDENCE_TYPES: - scenario_errors.append({"index": index, "unsupported_evidence_type": evidence_type}) - has_missing_required = True - - assertions = scenario.get("assertions") - if non_empty(assertions) or has_needs_clarification(assertions): - has_assertions = True - fixtures = scenario.get("fixtures_or_test_data") - if non_empty(fixtures) or has_needs_clarification(fixtures): - has_fixture_evidence = True - if non_empty(scenario.get("coverage_signal")): - has_coverage_gaps = True - - blockers = scenario.get("blockers") - if isinstance(blockers, list) and blockers: - has_blocker_lint = True - - if non_empty(intake_doc.get("assertion_gaps")): - has_assertions = True - if non_empty(intake_doc.get("fixture_or_test_data_gaps")): - has_fixture_evidence = True - if non_empty(intake_doc.get("coverage_gaps")) or non_empty(intake_doc.get("flaky_or_skipped_cases")): - has_coverage_gaps = True - - details["test_case_intake"]["scenario_errors"] = scenario_errors - details["test_case_intake"]["count_matches_scenarios"] = count_matches - - if has_missing_required: - blocker_codes.append(BLOCKERS["INTAKE_MISSING"]) - if has_untraceable: - blocker_codes.append(BLOCKERS["SCENARIOS_UNTRACEABLE"]) - if not has_assertions: - blocker_codes.append(BLOCKERS["ASSERTIONS_MISSING"]) - if not has_fixture_evidence: - blocker_codes.append(BLOCKERS["FIXTURE_EVIDENCE_MISSING"]) - if not has_coverage_gaps: - blocker_codes.append(BLOCKERS["COVERAGE_GAPS_MISSING"]) - if has_blocker_lint: - blocker_codes.append(BLOCKERS["BLOCKER_LINT_ERRORS"]) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/extensions/intake/scripts/python/validate_visual_design_intake.py b/extensions/intake/scripts/python/validate_visual_design_intake.py deleted file mode 100644 index 6a02eea683..0000000000 --- a/extensions/intake/scripts/python/validate_visual_design_intake.py +++ /dev/null @@ -1,883 +0,0 @@ -#!/usr/bin/env python3 -"""Validate Spec Kit visual-design intake artifacts.""" - -from __future__ import annotations - -import argparse -import glob -import hashlib -import json -import re -import sys -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, - supporting_visual_artifact_refs, - validate_json_schema, -) - - -ALLOWED_SOURCE_TYPES = {"image", "pdf", "markdown", "figma"} -ALLOWED_FIDELITY_LEVELS = {"low", "medium", "high"} - -BLOCKERS = { - "VISUAL_SOURCE_MANIFEST_MISSING": "VISUAL_SOURCE_MANIFEST_MISSING", - "VISUAL_SOURCE_TYPE_UNSUPPORTED": "VISUAL_SOURCE_TYPE_UNSUPPORTED", - "VISUAL_FIDELITY_LEVEL_UNSUPPORTED": "VISUAL_FIDELITY_LEVEL_UNSUPPORTED", - "VISUAL_SOURCE_FILE_MISSING": "VISUAL_SOURCE_FILE_MISSING", - "VISUAL_SOURCE_HASH_MISMATCH": "VISUAL_SOURCE_HASH_MISMATCH", - "VISUAL_SOURCE_INTEGRITY_INCOMPLETE": "VISUAL_SOURCE_INTEGRITY_INCOMPLETE", - "VISUAL_REQUIREMENTS_MISSING": "VISUAL_REQUIREMENTS_MISSING", - "VISUAL_REQUIREMENTS_UNTRACEABLE": "VISUAL_REQUIREMENTS_UNTRACEABLE", - "VISUAL_FIDELITY_RULES_MISSING": "VISUAL_FIDELITY_RULES_MISSING", - "VISUAL_PARITY_PLAN_MISSING": "VISUAL_PARITY_PLAN_MISSING", - "VISUAL_READY_WITHOUT_EVIDENCE": "VISUAL_READY_WITHOUT_EVIDENCE", - "VISUAL_EVIDENCE_PACKET_MISSING": "VISUAL_EVIDENCE_PACKET_MISSING", - "VISUAL_BLOCKER_LINT_ERRORS": "VISUAL_BLOCKER_LINT_ERRORS", - "VISUAL_INFERENCE_CONTRACT_INVALID": "VISUAL_INFERENCE_CONTRACT_INVALID", - "VISUAL_SCHEMA_INVALID": "VISUAL_SCHEMA_INVALID", - "FIGMA_RENDER_NODE_MISMATCH": "FIGMA_RENDER_NODE_MISMATCH", - "FIGMA_HIDDEN_LAYER_POLLUTION": "FIGMA_HIDDEN_LAYER_POLLUTION", - "FIGMA_NON_INSTANCE_COMPONENT": "FIGMA_NON_INSTANCE_COMPONENT", - "FIGMA_PROTOTYPE_METADATA_MISSING": "FIGMA_PROTOTYPE_METADATA_MISSING", - "FIGMA_UNSUPPORTED_STATE_INFERENCE": "FIGMA_UNSUPPORTED_STATE_INFERENCE", - "FIGMA_BUSINESS_RULE_UNSUPPORTED": "FIGMA_BUSINESS_RULE_UNSUPPORTED", - "FIGMA_INTERACTION_CONFLICT": "FIGMA_INTERACTION_CONFLICT", - "FIGMA_RESPONSIVE_RULE_MISSING": "FIGMA_RESPONSIVE_RULE_MISSING", - "FIGMA_LOW_CONFIDENCE_CANDIDATE": "FIGMA_LOW_CONFIDENCE_CANDIDATE", - "RAW_METADATA_MISSING": "FIGMA_RAW_METADATA_MISSING", - "RAW_METADATA_SUMMARY_SUBSTITUTION": "FIGMA_RAW_METADATA_SUMMARY_SUBSTITUTION", - "RAW_METADATA_TRUNCATED": "FIGMA_RAW_METADATA_TRUNCATED", - "SELECTED_SUBTREE_INCOMPLETE": "FIGMA_SELECTED_SUBTREE_INCOMPLETE", - "METADATA_INDEX_MISSING": "FIGMA_METADATA_INDEX_MISSING", - "METADATA_PARITY_FAILED": "FIGMA_METADATA_PARITY_FAILED", - "READY_WITHOUT_COMPLETENESS_PROOF": "FIGMA_READY_WITHOUT_COMPLETENESS_PROOF", - "NORMALIZED_TREE_MISSING": "FIGMA_NORMALIZED_TREE_MISSING", - "NORMALIZED_TREE_INCOMPLETE": "FIGMA_NORMALIZED_TREE_INCOMPLETE", -} - - -def load_yaml(path: Path) -> dict[str, Any]: - if yaml is None: - raise RuntimeError("PyYAML is required to validate YAML intake artifacts") - data = yaml.safe_load(path.read_text(encoding="utf-8")) - return data if isinstance(data, dict) else {} - - -def sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def is_truthy(value: Any) -> bool: - return value is True or str(value).strip().lower() == "true" - - -def is_remote_ref(value: str) -> bool: - return value.startswith(("http://", "https://", "figma://")) - - -def has_summary_substitution(text: str) -> bool: - patterns = [ - r"\bsummary\b", - r"\bsummarized\b", - r"\bomitted\b", - r"\btruncated for brevity\b", - r"\bnatural language summary\b", - ] - lower = text.lower() - return any(re.search(pattern, lower) for pattern in patterns) - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("intake_dir", help="Directory containing visual-design intake artifacts") - parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON") - args = parser.parse_args() - - intake_dir = Path(args.intake_dir) - blocker_codes: list[str] = [] - warnings: list[str] = [] - details: dict[str, Any] = {"intake_dir": str(intake_dir)} - - if not intake_dir.exists() or not intake_dir.is_dir(): - blocker_codes.extend( - [ - BLOCKERS["VISUAL_SOURCE_MANIFEST_MISSING"], - BLOCKERS["VISUAL_REQUIREMENTS_MISSING"], - BLOCKERS["VISUAL_EVIDENCE_PACKET_MISSING"], - ] - ) - return emit(args.json, details, blocker_codes, warnings) - - manifest_path = intake_dir / "design-source-manifest.yaml" - source_type = "" - visual_gate_active = manifest_path.exists() - - if visual_gate_active: - source_type = validate_visual_contract(intake_dir, manifest_path, details, blocker_codes) - else: - blocker_codes.append(BLOCKERS["VISUAL_SOURCE_MANIFEST_MISSING"]) - details["source_manifest"] = {"missing": True} - - has_figma_artifacts = any( - [ - list(intake_dir.glob("figma-metadata.part-*.xml")), - (intake_dir / "figma-metadata.index.yaml").exists(), - (intake_dir / "figma-node-inventory.yaml").exists(), - ] - ) - if source_type == "figma" or (not source_type and has_figma_artifacts): - validate_figma_provider(intake_dir, details, blocker_codes) - - validate_evidence_packet( - intake_dir=intake_dir, - details=details, - blocker_codes=blocker_codes, - warnings=warnings, - visual_gate_active=visual_gate_active, - ) - - return emit(args.json, details, sorted(set(blocker_codes)), warnings) - - -def validate_visual_contract( - intake_dir: Path, - manifest_path: Path, - details: dict[str, Any], - blocker_codes: list[str], -) -> str: - validate_json_schema( - instance_path=manifest_path, - schema_name="visual-source-manifest.schema.json", - details_key="visual_source_manifest", - details=details, - blocker_codes=blocker_codes, - schema_error_code=BLOCKERS["VISUAL_SCHEMA_INVALID"], - ) - - manifest = load_yaml(manifest_path) - source_type = str(manifest.get("source_type") or "").strip().lower() - required_fidelity = str(manifest.get("required_fidelity") or "").strip().lower() - - details["source_manifest"] = { - "source_type": source_type, - "required_fidelity": required_fidelity, - "source_integrity_complete": manifest.get("source_integrity_complete"), - "captured_at": manifest.get("captured_at"), - "capture_method": manifest.get("capture_method"), - "page_or_frame_count": manifest.get("page_or_frame_count"), - "processed_count": manifest.get("processed_count"), - "extraction_scope": manifest.get("extraction_scope"), - "snapshot_status": manifest.get("snapshot_status"), - "integrity_gap_reason": manifest.get("integrity_gap_reason"), - } - - required_manifest_fields = [ - "source_type", - "required_fidelity", - "source_files", - "source_integrity_complete", - "captured_at", - "capture_method", - "page_or_frame_count", - "processed_count", - "extraction_scope", - ] - missing_manifest_fields = [field for field in required_manifest_fields if field not in manifest] - details["source_manifest"]["missing_required_fields"] = missing_manifest_fields - if missing_manifest_fields: - blocker_codes.append(BLOCKERS["VISUAL_SOURCE_INTEGRITY_INCOMPLETE"]) - - if source_type not in ALLOWED_SOURCE_TYPES: - blocker_codes.append(BLOCKERS["VISUAL_SOURCE_TYPE_UNSUPPORTED"]) - - if required_fidelity not in ALLOWED_FIDELITY_LEVELS: - blocker_codes.append(BLOCKERS["VISUAL_FIDELITY_LEVEL_UNSUPPORTED"]) - - if not is_truthy(manifest.get("source_integrity_complete")): - blocker_codes.append(BLOCKERS["VISUAL_SOURCE_INTEGRITY_INCOMPLETE"]) - - validate_source_files(intake_dir, manifest, source_type, details, blocker_codes) - validate_visual_requirements(intake_dir, required_fidelity, details, blocker_codes) - return source_type - - -def validate_source_files( - intake_dir: Path, - manifest: dict[str, Any], - source_type: str, - details: dict[str, Any], - blocker_codes: list[str], -) -> None: - source_files = manifest.get("source_files") - if not isinstance(source_files, list) or not source_files: - blocker_codes.append(BLOCKERS["VISUAL_SOURCE_FILE_MISSING"]) - details["source_files"] = [] - return - - validated_files: list[dict[str, Any]] = [] - manifest_remote_gap = ( - str(manifest.get("snapshot_status") or "").strip() in {"not_available", "not_required"} - and manifest.get("integrity_gap_reason") not in (None, "", [], {}) - ) - for source_file in source_files: - if not isinstance(source_file, dict): - blocker_codes.append(BLOCKERS["VISUAL_SOURCE_FILE_MISSING"]) - continue - - required_source_file_fields = ["path", "mime_type", "byte_size", "sha256", "role"] - rel_path = str(source_file.get("path") or "").strip() - remote_ref = is_remote_ref(rel_path) - remote_gap_fields = {"byte_size", "sha256"} if remote_ref and manifest_remote_gap else set() - missing_source_file_fields = [ - field - for field in required_source_file_fields - if field not in source_file and field not in remote_gap_fields - ] - expected = str(source_file.get("sha256") or "").replace("sha256:", "").strip() - file_detail: dict[str, Any] = { - "path": rel_path, - "exists": False, - "sha256_match": None, - "missing_required_fields": missing_source_file_fields, - "checksum_status": source_file.get("checksum_status"), - "snapshot_status": manifest.get("snapshot_status"), - "integrity_gap_reason": manifest.get("integrity_gap_reason"), - } - - if missing_source_file_fields: - blocker_codes.append(BLOCKERS["VISUAL_SOURCE_INTEGRITY_INCOMPLETE"]) - - if not rel_path: - blocker_codes.append(BLOCKERS["VISUAL_SOURCE_FILE_MISSING"]) - validated_files.append(file_detail) - continue - - if remote_ref: - file_detail["exists"] = True - file_detail["remote_ref"] = True - validated_files.append(file_detail) - continue - - source_path = intake_dir / rel_path - if not source_path.exists(): - blocker_codes.append(BLOCKERS["VISUAL_SOURCE_FILE_MISSING"]) - validated_files.append(file_detail) - continue - - file_detail["exists"] = True - expected_size = source_file.get("byte_size") - if expected_size is not None: - try: - file_detail["byte_size_match"] = int(expected_size) == source_path.stat().st_size - if not file_detail["byte_size_match"]: - blocker_codes.append(BLOCKERS["VISUAL_SOURCE_HASH_MISMATCH"]) - except (TypeError, ValueError): - blocker_codes.append(BLOCKERS["VISUAL_SOURCE_INTEGRITY_INCOMPLETE"]) - if expected: - actual = sha256(source_path) - file_detail["sha256_match"] = expected == actual - if expected != actual: - blocker_codes.append(BLOCKERS["VISUAL_SOURCE_HASH_MISMATCH"]) - - validated_files.append(file_detail) - - if source_type != "figma" and not any(item.get("exists") for item in validated_files): - blocker_codes.append(BLOCKERS["VISUAL_SOURCE_FILE_MISSING"]) - - details["source_files"] = validated_files - - -def validate_visual_requirements( - intake_dir: Path, - required_fidelity: str, - details: dict[str, Any], - blocker_codes: list[str], -) -> None: - requirements_path = intake_dir / "visual-requirements.yaml" - if not requirements_path.exists(): - blocker_codes.append(BLOCKERS["VISUAL_REQUIREMENTS_MISSING"]) - return - - validate_json_schema( - instance_path=requirements_path, - schema_name="visual-requirements.schema.json", - details_key="visual_requirements", - details=details, - blocker_codes=blocker_codes, - schema_error_code=BLOCKERS["VISUAL_SCHEMA_INVALID"], - ) - - requirements_doc = load_yaml(requirements_path) - requirements = requirements_doc.get("requirements") - if not isinstance(requirements, list): - requirements = [] - - declared_requirements_count = requirements_doc.get("visual_requirements_count") - try: - requirements_count = ( - len(requirements) - if declared_requirements_count is None - else int(declared_requirements_count) - ) - except (TypeError, ValueError): - requirements_count = len(requirements) - visual_requirements_complete = is_truthy(requirements_doc.get("visual_requirements_complete")) - source_refs_complete = is_truthy(requirements_doc.get("source_refs_complete")) - fidelity_rules_applied = is_truthy(requirements_doc.get("fidelity_rules_applied")) - visual_parity_plan_complete = is_truthy(requirements_doc.get("visual_parity_plan_complete")) - blocker_lint_errors = requirements_doc.get("blocker_lint_errors") - parity_plan = requirements_doc.get("parity_plan") - - details["visual_requirements"] = { - "visual_requirements_complete": requirements_doc.get("visual_requirements_complete"), - "visual_requirements_count": requirements_count, - "source_refs_complete": requirements_doc.get("source_refs_complete"), - "fidelity_rules_applied": requirements_doc.get("fidelity_rules_applied"), - "visual_parity_plan_complete": requirements_doc.get("visual_parity_plan_complete"), - "required_fidelity": required_fidelity, - "blocker_lint_errors": blocker_lint_errors, - "parity_plan": parity_plan, - } - - if not visual_requirements_complete or requirements_count <= 0: - blocker_codes.append(BLOCKERS["VISUAL_REQUIREMENTS_MISSING"]) - - count_matches = requirements_count == len(requirements) - if not count_matches: - blocker_codes.append(BLOCKERS["VISUAL_REQUIREMENTS_MISSING"]) - - required_fields = [ - "id", - "category", - "requirement", - "source_refs", - "evidence_type", - "confidence", - "confidence_rationale", - "engineering_action", - "acceptance_check", - "fidelity_level", - ] - allowed_evidence_types = { - "observed", - "inferred", - "candidate", - "unsupported", - "missing", - "out_of_scope", - } - allowed_categories = { - "layout", - "spacing", - "sizing", - "typography", - "color", - "asset", - "component", - "state", - "interaction", - "responsive", - "accessibility", - "content", - } - requirement_errors: list[dict[str, Any]] = [] - has_untraceable = not source_refs_complete - has_missing_required = False - has_bad_fidelity = False - has_blocker_lint = isinstance(blocker_lint_errors, list) and len(blocker_lint_errors) > 0 - has_inference_contract_error = False - evidence_type_counts: dict[str, int] = {} - supporting_source_refs: list[dict[str, Any]] = [] - - for index, item in enumerate(requirements): - if not isinstance(item, dict): - requirement_errors.append({"index": index, "error": "requirement must be a mapping"}) - has_missing_required = True - continue - - missing_fields = [field for field in required_fields if not item.get(field)] - if missing_fields: - requirement_errors.append({"index": index, "missing_fields": missing_fields}) - has_missing_required = True - - source_refs = item.get("source_refs") - if not isinstance(source_refs, list) or not source_refs or any(not str(ref).strip() for ref in source_refs): - has_untraceable = True - else: - helper_refs = supporting_visual_artifact_refs(source_refs) - if helper_refs: - requirement_errors.append({"index": index, "supporting_artifact_source_refs": helper_refs}) - supporting_source_refs.append({"index": index, "refs": helper_refs}) - has_untraceable = True - - evidence_type = str(item.get("evidence_type") or "").strip().lower() - if evidence_type: - evidence_type_counts[evidence_type] = evidence_type_counts.get(evidence_type, 0) + 1 - if evidence_type and evidence_type not in allowed_evidence_types: - requirement_errors.append({"index": index, "unsupported_evidence_type": evidence_type}) - has_missing_required = True - elif evidence_type in {"inferred", "candidate", "unsupported"}: - inference_errors = validate_bounded_inference_claim(item, evidence_type) - if inference_errors: - requirement_errors.append({"index": index, "inference_contract_errors": inference_errors}) - has_inference_contract_error = True - - category = str(item.get("category") or "").strip().lower() - if category and category not in allowed_categories: - requirement_errors.append({"index": index, "unsupported_category": category}) - has_missing_required = True - - fidelity_level = str(item.get("fidelity_level") or "").strip().lower() - if fidelity_level and fidelity_level != required_fidelity: - requirement_errors.append({"index": index, "fidelity_level_mismatch": fidelity_level}) - has_bad_fidelity = True - - blockers = item.get("blockers") - if isinstance(blockers, list) and blockers: - has_blocker_lint = True - - details["visual_requirements"]["requirement_errors"] = requirement_errors - details["visual_requirements"]["evidence_type_counts"] = evidence_type_counts - details["visual_requirements"]["supporting_artifact_source_refs"] = supporting_source_refs - details["visual_requirements"]["count_matches_requirements"] = count_matches - - if has_missing_required: - blocker_codes.append(BLOCKERS["VISUAL_REQUIREMENTS_MISSING"]) - - if has_untraceable: - blocker_codes.append(BLOCKERS["VISUAL_REQUIREMENTS_UNTRACEABLE"]) - - if not fidelity_rules_applied or has_bad_fidelity: - blocker_codes.append(BLOCKERS["VISUAL_FIDELITY_RULES_MISSING"]) - - required_parity_fields = [ - "comparison_targets", - "original_refs", - "comparison_method", - "thresholds", - "accepted_exceptions", - "blocking_difference_categories", - ] - missing_parity_fields: list[str] = [] - if not isinstance(parity_plan, dict): - missing_parity_fields = required_parity_fields - else: - missing_parity_fields = [] - for field in required_parity_fields: - if field not in parity_plan: - missing_parity_fields.append(field) - continue - value = parity_plan.get(field) - if field == "accepted_exceptions": - if value is None: - missing_parity_fields.append(field) - elif value in (None, "") or value == [] or value == {}: - missing_parity_fields.append(field) - details["visual_requirements"]["missing_parity_fields"] = missing_parity_fields - - if not visual_parity_plan_complete or missing_parity_fields: - blocker_codes.append(BLOCKERS["VISUAL_PARITY_PLAN_MISSING"]) - - if has_blocker_lint: - blocker_codes.append(BLOCKERS["VISUAL_BLOCKER_LINT_ERRORS"]) - - if has_inference_contract_error: - blocker_codes.append(BLOCKERS["VISUAL_INFERENCE_CONTRACT_INVALID"]) - - -def validate_bounded_inference_claim(item: dict[str, Any], evidence_type: str) -> list[str]: - errors: list[str] = [] - - def has_non_empty(field: str) -> bool: - value = item.get(field) - return value not in (None, "", [], {}) - - source_refs = item.get("source_refs") - if not isinstance(source_refs, list) or not source_refs: - errors.append("non_observed_claim_requires_source_refs") - - if evidence_type == "inferred": - required = [ - "inference_rule", - "confidence_method", - "score_breakdown", - "downstream_use", - "blocking_conditions", - ] - missing = [field for field in required if not has_non_empty(field)] - if missing: - errors.append(f"inferred_missing_fields:{','.join(missing)}") - if item.get("confidence") != "high": - errors.append("inferred_claim_requires_high_confidence") - if item.get("downstream_use") != "accepted_claim": - errors.append("inferred_claim_requires_downstream_use_accepted_claim") - - if evidence_type == "candidate": - required = [ - "inference_rule", - "confidence_method", - "score_breakdown", - "downstream_use", - "missing_evidence", - "blocking_conditions", - ] - missing = [field for field in required if not has_non_empty(field)] - if missing: - errors.append(f"candidate_missing_fields:{','.join(missing)}") - if item.get("confidence") not in {"low", "medium"}: - errors.append("candidate_claim_requires_low_or_medium_confidence") - if item.get("downstream_use") != "reference_only": - errors.append("candidate_claim_requires_downstream_use_reference_only") - - if evidence_type == "unsupported": - required = ["blocker_code", "reason", "downstream_use", "missing_evidence", "blockers"] - missing = [field for field in required if not has_non_empty(field)] - if missing: - errors.append(f"unsupported_missing_fields:{','.join(missing)}") - if item.get("downstream_use") != "blocked": - errors.append("unsupported_claim_requires_downstream_use_blocked") - blockers = item.get("blockers") - blocker_code = item.get("blocker_code") - if isinstance(blockers, list) and blocker_code and blocker_code not in blockers: - errors.append("unsupported_claim_blocker_code_must_match_blockers") - - score_breakdown = item.get("score_breakdown") - if evidence_type in {"inferred", "candidate"}: - if not isinstance(score_breakdown, list) or not score_breakdown: - errors.append("score_breakdown_requires_at_least_one_signal") - else: - for signal_index, signal in enumerate(score_breakdown): - if not isinstance(signal, dict): - errors.append(f"score_breakdown_signal_{signal_index}_must_be_mapping") - continue - if not has_non_empty_in_mapping(signal, "signal"): - errors.append(f"score_breakdown_signal_{signal_index}_missing_signal") - if "weight" not in signal: - errors.append(f"score_breakdown_signal_{signal_index}_missing_weight") - - return errors - - -def has_non_empty_in_mapping(value: dict[str, Any], field: str) -> bool: - return value.get(field) not in (None, "", [], {}) - - -def validate_figma_provider(intake_dir: Path, details: dict[str, Any], blocker_codes: list[str]) -> None: - metadata_parts = sorted(Path(p) for p in glob.glob(str(intake_dir / "figma-metadata.part-*.xml"))) - details["metadata_shards"] = [p.name for p in metadata_parts] - details["metadata_shard_count"] = len(metadata_parts) - - if not metadata_parts: - blocker_codes.append(BLOCKERS["RAW_METADATA_MISSING"]) - - for part in metadata_parts: - text = part.read_text(encoding="utf-8", errors="replace") - if has_summary_substitution(text): - blocker_codes.append(BLOCKERS["RAW_METADATA_SUMMARY_SUBSTITUTION"]) - if "truncated" in text.lower() and "truncated=\"false\"" not in text.lower(): - blocker_codes.append(BLOCKERS["RAW_METADATA_TRUNCATED"]) - - index_path = intake_dir / "figma-metadata.index.yaml" - inventory_path = intake_dir / "figma-node-inventory.yaml" - - index: dict[str, Any] = {} - inventory: dict[str, Any] = {} - - if not index_path.exists(): - blocker_codes.append(BLOCKERS["METADATA_INDEX_MISSING"]) - else: - validate_json_schema( - instance_path=index_path, - schema_name="figma-metadata-index.schema.json", - details_key="figma_metadata_index", - details=details, - blocker_codes=blocker_codes, - schema_error_code=BLOCKERS["VISUAL_SCHEMA_INVALID"], - ) - index = load_yaml(index_path) - required_source_fields = [ - "file_url", - "file_key", - "page_id", - "selected_node_ids", - "captured_at", - "mcp_tool", - "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", - ] - missing_index_fields = [ - field for field in required_source_fields + required_completeness_fields if field not in index - ] - details["index"] = { - "raw_metadata_complete": index.get("raw_metadata_complete"), - "selected_subtree_complete": index.get("selected_subtree_complete"), - "gap_count": index.get("gap_count"), - "missing_required_fields": missing_index_fields, - } - if missing_index_fields: - blocker_codes.append(BLOCKERS["METADATA_INDEX_MISSING"]) - if index.get("mcp_tool") and str(index.get("mcp_tool")).strip() != "get_metadata": - blocker_codes.append(BLOCKERS["METADATA_INDEX_MISSING"]) - if not is_truthy(index.get("raw_metadata_complete")): - blocker_codes.append(BLOCKERS["READY_WITHOUT_COMPLETENESS_PROOF"]) - if not is_truthy(index.get("selected_subtree_complete")): - blocker_codes.append(BLOCKERS["SELECTED_SUBTREE_INCOMPLETE"]) - shards = index.get("shards", []) - if isinstance(shards, list): - indexed_shard_paths: set[str] = set() - for shard in shards: - if not isinstance(shard, dict): - blocker_codes.append(BLOCKERS["METADATA_INDEX_MISSING"]) - continue - rel_path = shard.get("path") - if not rel_path: - blocker_codes.append(BLOCKERS["METADATA_INDEX_MISSING"]) - continue - indexed_shard_paths.add(str(rel_path)) - missing_shard_fields = [ - field - for field in ["path", "byte_size", "sha256", "root_node_ids", "node_count", "truncated"] - if field not in shard - ] - if missing_shard_fields: - blocker_codes.append(BLOCKERS["METADATA_INDEX_MISSING"]) - shard_path = intake_dir / str(rel_path) - if not shard_path.exists(): - blocker_codes.append(BLOCKERS["RAW_METADATA_MISSING"]) - continue - expected = str(shard.get("sha256", "")).replace("sha256:", "") - if expected and expected != sha256(shard_path): - blocker_codes.append(BLOCKERS["VISUAL_SOURCE_HASH_MISMATCH"]) - expected_size = shard.get("byte_size") - if expected_size is not None: - try: - if int(expected_size) != shard_path.stat().st_size: - blocker_codes.append(BLOCKERS["VISUAL_SOURCE_HASH_MISMATCH"]) - except (TypeError, ValueError): - blocker_codes.append(BLOCKERS["METADATA_INDEX_MISSING"]) - if is_truthy(shard.get("truncated")): - blocker_codes.append(BLOCKERS["RAW_METADATA_TRUNCATED"]) - actual_shard_paths = {part.name for part in metadata_parts} - if actual_shard_paths != indexed_shard_paths: - blocker_codes.append(BLOCKERS["METADATA_INDEX_MISSING"]) - else: - blocker_codes.append(BLOCKERS["METADATA_INDEX_MISSING"]) - - if not inventory_path.exists(): - blocker_codes.append(BLOCKERS["METADATA_PARITY_FAILED"]) - else: - validate_json_schema( - instance_path=inventory_path, - schema_name="figma-node-inventory.schema.json", - details_key="figma_node_inventory", - details=details, - blocker_codes=blocker_codes, - schema_error_code=BLOCKERS["VISUAL_SCHEMA_INVALID"], - ) - inventory = load_yaml(inventory_path) - raw_node_count = int(inventory.get("raw_node_count") or 0) - inventory_node_count = int(inventory.get("inventory_node_count") or 0) - excluded_node_count = int(inventory.get("excluded_node_count") or 0) - missing_node_count = int(inventory.get("missing_node_count") or 0) - duplicate_node_count = int(inventory.get("duplicate_node_count") or 0) - coverage = str(inventory.get("node_inventory_coverage") or "") - parity_passed = is_truthy(inventory.get("parity_passed")) - truncated_raw_evidence = is_truthy(inventory.get("truncated_raw_evidence")) - - details["inventory"] = { - "raw_node_count": raw_node_count, - "inventory_node_count": inventory_node_count, - "excluded_node_count": excluded_node_count, - "missing_node_count": missing_node_count, - "duplicate_node_count": duplicate_node_count, - "node_inventory_coverage": coverage, - "parity_passed": parity_passed, - "truncated_raw_evidence": truncated_raw_evidence, - } - - balanced = inventory_node_count + excluded_node_count + missing_node_count == raw_node_count - if not balanced or duplicate_node_count != 0 or missing_node_count != 0: - blocker_codes.append(BLOCKERS["METADATA_PARITY_FAILED"]) - if coverage != "100%" or not parity_passed or truncated_raw_evidence: - blocker_codes.append(BLOCKERS["METADATA_PARITY_FAILED"]) - if truncated_raw_evidence: - blocker_codes.append(BLOCKERS["RAW_METADATA_TRUNCATED"]) - - validate_figma_layout_normalization(intake_dir, inventory, details, blocker_codes) - - -def validate_figma_layout_normalization( - intake_dir: Path, - inventory: dict[str, Any], - details: dict[str, Any], - blocker_codes: list[str], -) -> None: - normalized_path = intake_dir / "figma-normalized-tree.yaml" - if not normalized_path.exists(): - blocker_codes.append(BLOCKERS["NORMALIZED_TREE_MISSING"]) - details["figma_normalized_tree"] = {"missing": True} - return - - validate_json_schema( - instance_path=normalized_path, - schema_name="figma-normalized-tree.schema.json", - details_key="figma_normalized_tree", - details=details, - blocker_codes=blocker_codes, - schema_error_code=BLOCKERS["VISUAL_SCHEMA_INVALID"], - ) - - normalized = load_yaml(normalized_path) - nodes = normalized.get("nodes") - if not isinstance(nodes, list): - nodes = [] - - raw_node_count = int(inventory.get("raw_node_count") or normalized.get("raw_node_count") or 0) - normalized_node_count = int(normalized.get("normalized_node_count") or 0) - source_node_ids: list[str] = [] - visual_orders: list[int] = [] - node_errors: list[dict[str, Any]] = [] - for index, node in enumerate(nodes): - if not isinstance(node, dict): - node_errors.append({"index": index, "error": "node must be a mapping"}) - continue - source_node_id = str(node.get("source_node_id") or "").strip() - if not source_node_id: - node_errors.append({"index": index, "error": "source_node_id is required"}) - else: - source_node_ids.append(source_node_id) - if node.get("normalized_name") in (None, "") or node.get("group_key") in (None, ""): - node_errors.append({"index": index, "error": "normalized_name and group_key are required"}) - source_refs = node.get("source_refs") - if not isinstance(source_refs, list) or not source_refs: - node_errors.append({"index": index, "error": "source_refs are required"}) - try: - visual_orders.append(int(node.get("visual_order"))) - except (TypeError, ValueError): - node_errors.append({"index": index, "error": "visual_order must be an integer"}) - - duplicate_source_ids = sorted({node_id for node_id in source_node_ids if source_node_ids.count(node_id) > 1}) - duplicate_visual_orders = sorted({order for order in visual_orders if visual_orders.count(order) > 1}) - expected_visual_orders = list(range(1, len(nodes) + 1)) - visual_order_contiguous = sorted(visual_orders) == expected_visual_orders - count_matches = normalized_node_count == len(nodes) == raw_node_count - coverage_passed = ( - is_truthy(normalized.get("normalization_complete")) - and normalized.get("node_coverage") == "100%" - and count_matches - and not duplicate_source_ids - and not duplicate_visual_orders - and visual_order_contiguous - and not node_errors - and not normalized.get("gaps") - ) - - details["figma_layout_normalization"] = { - "normalization_complete": normalized.get("normalization_complete"), - "raw_node_count": raw_node_count, - "normalized_node_count": normalized_node_count, - "actual_node_count": len(nodes), - "node_coverage": normalized.get("node_coverage"), - "count_matches_raw_nodes": count_matches, - "duplicate_source_node_ids": duplicate_source_ids, - "duplicate_visual_orders": duplicate_visual_orders, - "visual_order_contiguous": visual_order_contiguous, - "node_errors": node_errors, - "gaps": normalized.get("gaps"), - } - - if not coverage_passed: - blocker_codes.append(BLOCKERS["NORMALIZED_TREE_INCOMPLETE"]) - - -def validate_evidence_packet( - intake_dir: Path, - details: dict[str, Any], - blocker_codes: list[str], - warnings: list[str], - visual_gate_active: bool, -) -> None: - packet_candidates = [ - intake_dir / "visual-evidence-packet.md", - intake_dir / "figma-evidence-packet.md", - ] - evidence_path = next((path for path in packet_candidates if path.exists()), None) - if evidence_path is None: - blocker_codes.append(BLOCKERS["VISUAL_EVIDENCE_PACKET_MISSING"]) - return - if evidence_path.name == "figma-evidence-packet.md": - warnings.append("figma-evidence-packet.md is a legacy evidence packet alias; prefer visual-evidence-packet.md") - details["evidence_packet"] = evidence_path.name - - evidence_text = evidence_path.read_text(encoding="utf-8", errors="replace") - packet_status = parse_evidence_packet_status(evidence_text) - details["evidence_packet_metadata"] = packet_status["metadata"] - if packet_status["warnings"]: - warnings.extend(packet_status["warnings"]) - if packet_status["errors"]: - if visual_gate_active: - blocker_codes.append(BLOCKERS["VISUAL_READY_WITHOUT_EVIDENCE"]) - else: - blocker_codes.append(BLOCKERS["READY_WITHOUT_COMPLETENESS_PROOF"]) - return - - if packet_status["ready_gate"] != "PASS": - blocker_codes.append(BLOCKERS["VISUAL_READY_WITHOUT_EVIDENCE"]) - return - - visual_requirements = details.get("visual_requirements", {}) - index = details.get("index", {}) - packet_blockers = packet_status["metadata"].get("blockers") - has_packet_blockers = isinstance(packet_blockers, list) and bool(packet_blockers) - - if visual_gate_active: - if blocker_codes or has_packet_blockers or not is_truthy(visual_requirements.get("visual_requirements_complete")): - blocker_codes.append(BLOCKERS["VISUAL_READY_WITHOUT_EVIDENCE"]) - elif blocker_codes or has_packet_blockers or not is_truthy(index.get("raw_metadata_complete")): - blocker_codes.append(BLOCKERS["READY_WITHOUT_COMPLETENESS_PROOF"]) - - -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"Visual design intake 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/templates/intake-prd-contract.md b/extensions/intake/templates/intake-prd-contract.md deleted file mode 100644 index e440f3f31b..0000000000 --- a/extensions/intake/templates/intake-prd-contract.md +++ /dev/null @@ -1,125 +0,0 @@ -# PRD Intake Contract - -Required PRD intake artifacts and readiness gates. Runtime agents or external intake tools extract source-backed product facts before downstream SDD workflows project them into requirements, scope, acceptance criteria, and clarification items. - -Intake does not generate requirements. It preserves all reachable source files, stable source refs, checksums or retrieval metadata, and schema-required source-backed facts that SDD `specify` can consume accurately. - -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. - -This contract is intentionally provider-neutral. It must not require downstream-owned requirement IDs, implementation tasks, code component names, or product semantics that are not evidenced by the source. - -## Supported Sources - -`source-manifest.yaml` must identify the original PRD source and preserve source integrity. - -Required source fields: - -- source_type: markdown|pdf|doc|url|issue|mixed -- 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|export|attachment|snapshot -- source_integrity_complete: -- captured_at: -- capture_method: -- document_version: -- extraction_scope: - -Source-specific requirements: - -- Markdown or text PRDs must record heading coverage, linked asset refs, and parsed section coverage. -- PDF or exported docs must record original file hash, page count, processed page count, and text extraction status. -- URL, issue, or epic sources must record stable URL, retrieval timestamp, visible title, author or owner, and snapshot refs; unavailable values must be represented by `snapshot_status` and `integrity_gap_reason`. -- Remote source refs may use a stable URL 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. -- Mixed stakeholder notes must record each source separately and mark conflicting or unsupported claims. - -## Vertical Scenario Coverage - -- Product brief: capture problem, target users, value proposition, scope, non-goals, success signals, and open business questions. -- Feature PRD: capture flows, functional requirements, user stories, edge cases, acceptance criteria, dependencies, rollout or migration notes, and unresolved decisions. -- Executive or strategy doc: capture goals, constraints, metrics, launch criteria, risks, and non-functional expectations without inventing detailed behavior. -- Issue or epic thread: capture requested behavior, comments that change scope, linked bugs, decisions, and stale or contradictory context. -- Mixed source packet: reconcile source precedence, record conflicts, and keep each extracted fact traceable to one or more source refs. - -## PRD Intake Facts - -`prd-intake.yaml` must normalize source-backed product facts into engineering input. - -Required top-level fields: - -- prd_intake_complete: -- source_refs_complete: -- extracted_fact_count: -- acceptance_evidence_complete: -- unresolved_ambiguity_marked: -- acceptance_gaps: -- open_questions: -- blocker_lint_errors: -- facts: - -Each fact must include: - -- id: -- category: goal|non_goal|user|actor|scope|flow|business_rule|data|permission|integration|compliance|acceptance|metric|risk|open_question -- statement: -- source_refs: -- evidence_type: observed|inferred|missing|out_of_scope -- confidence: low|medium|high -- confidence_rationale: -- downstream_hint: -- acceptance_or_validation_signal: - -When evidenced by the source, include provider-neutral optional fields: - -- priority: -- dependency_refs: -- related_decisions: -- conflict_refs: -- blockers: - -## Readiness Gate - -PRD intake is ready only when: - -- source_integrity_complete: true -- prd_intake_complete: true -- source_refs_complete: true -- extracted_fact_count greater than 0 and equal to the number of records in `facts` -- acceptance evidence or explicit acceptance gaps are recorded -- unresolved ambiguity is marked `[NEEDS CLARIFICATION]` -- no blocker lint errors exist - -## Blocker Lint Errors - -- PRD_SOURCE_MANIFEST_MISSING -- PRD_SOURCE_TYPE_UNSUPPORTED -- PRD_SOURCE_FILE_MISSING -- PRD_SOURCE_HASH_MISMATCH -- PRD_SOURCE_INTEGRITY_INCOMPLETE -- PRD_INTAKE_MISSING -- PRD_FACTS_UNTRACEABLE -- PRD_ACCEPTANCE_EVIDENCE_MISSING -- PRD_CLARIFICATION_MARKING_MISSING -- PRD_READY_WITHOUT_EVIDENCE -- PRD_EVIDENCE_PACKET_MISSING -- PRD_BLOCKER_LINT_ERRORS -- PRD_SCHEMA_INVALID - -## Gap Rules - -Record a gap instead of passing silently when source evidence is missing, contradictory, stale, inaccessible, untraceable, or only implied. Product intent may be inferred only when marked `evidence_type: inferred` with confidence and source refs. - -## Evidence Packet Metadata - -`evidence-packet.md` must start with YAML front matter: - -- ready_gate: `PASS|BLOCKED` -- blockers: -- source_ref_count: -- extracted_item_count: -- generated_at: - -Human-readable sections may summarize the same records, but readiness metadata is validated from the front matter when present. diff --git a/extensions/intake/templates/intake-prd-evidence-packet-template.md b/extensions/intake/templates/intake-prd-evidence-packet-template.md deleted file mode 100644 index ce0d0ad378..0000000000 --- a/extensions/intake/templates/intake-prd-evidence-packet-template.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -ready_gate: BLOCKED -blockers: [] -source_ref_count: 0 -extracted_item_count: 0 -generated_at: ---- - -# PRD Evidence Packet - -Purpose: summarize source-backed product evidence for downstream Spec Kit workflows while preserving enough traceability to keep product intent, scope, acceptance criteria, and clarification gaps grounded in the original source. - -This packet is a human-readable readiness summary. Machine-readable product facts are recorded in `prd-intake.yaml` and validated by `templates/schemas/prd-intake.schema.json`. This packet does not define downstream workflow schemas, downstream-owned requirement IDs, implementation tasks, code component names, or ownership assignments. - -## Source - -- Source type: markdown|pdf|doc|url|issue|mixed -- Source path or URL: -- Source files / sections / issues: -- Document version / timestamp: -- Product area: -- Capture method: - -## Source Integrity - -- source-manifest.yaml: -- source files preserved: -- source checksums or snapshots verified: -- section/page/item coverage: -- source_integrity_complete: - -## Scenario Route - -- Product brief: -- Feature PRD: -- Executive or strategy doc: -- Issue or epic thread: -- Mixed source packet: -- Scenario routing notes: - -## Extraction Context - -- Runtime agent: -- Input tooling availability: -- PDF/text extraction availability: -- URL or issue retrieval availability: -- Linked asset parsing availability: -- Existing intake artifacts preserved: - -## Intake Readiness - -- source-manifest.yaml: -- prd-intake.yaml: -- evidence-packet.md: -- source integrity completeness: -- PRD intake completeness: -- source refs completeness: -- acceptance evidence completeness: -- blocker lint errors: -- readiness front matter synchronized: yes|no - -## Machine-Readable Artifacts - -- source-manifest.yaml: -- prd-intake.yaml: -- schema validation result: -- readiness validator result: - -## Product Fact Summary - -- Goals: -- Non-goals: -- Users / actors / roles: -- Scope boundaries: -- Product flows / user journeys: -- Business rules: -- Data constraints: -- Permissions: -- Integrations: -- Compliance constraints: -- Acceptance criteria: -- Success metrics: -- Risks: - -## Conflicts / Gaps - -- Conflicting source facts: -- Missing acceptance evidence: -- Stale or superseded context: -- Unsupported assumptions: -- Items marked `[NEEDS CLARIFICATION]`: - -## Traceability Summary - -- Source refs represented: -- Product fact categories represented: -- Acceptance or validation signals represented: -- Facts requiring downstream clarification: - -## Consumer Handoff Notes - -- Supported requirement sections: -- Clarification items that must remain unresolved: -- Source refs required in downstream artifacts: - -## Open Questions - -- [NEEDS CLARIFICATION] diff --git a/extensions/intake/templates/intake-static-html-delivery-contract.md b/extensions/intake/templates/intake-static-html-delivery-contract.md deleted file mode 100644 index 50b139c814..0000000000 --- a/extensions/intake/templates/intake-static-html-delivery-contract.md +++ /dev/null @@ -1,244 +0,0 @@ -# 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-test-cases-contract.md b/extensions/intake/templates/intake-test-cases-contract.md deleted file mode 100644 index 0b5c162418..0000000000 --- a/extensions/intake/templates/intake-test-cases-contract.md +++ /dev/null @@ -1,138 +0,0 @@ -# Test Case Intake Contract - -Required test-case intake artifacts and readiness gates. Runtime agents or external intake tools extract existing behavioral evidence before downstream SDD workflows project scenarios, assertions, fixtures, and coverage gaps into specifications. - -Intake does not generate requirements. It preserves all reachable test sources, stable source refs, checksums or retrieval metadata, and schema-required behavioral evidence that SDD `specify` can consume accurately. - -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. - -Tests are evidence, not the only source of truth. This contract preserves what existing tests prove while keeping inferred product intent explicit. - -## Supported Sources - -`source-manifest.yaml` must identify the original test source and preserve source integrity. - -Required source fields: - -- source_type: code|gherkin|spreadsheet|test_management|issue|mixed -- 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|export|attachment|snapshot -- source_integrity_complete: -- captured_at: -- capture_method: -- framework_or_format: -- execution_scope: - -Source-specific requirements: - -- Automated test files must record framework, file paths, test names, skipped markers, fixtures, mocks, and execution status; unavailable values must be represented by explicit fixture, assertion, or coverage gaps. -- Gherkin sources must record feature, background, scenario, examples, tags, and step coverage. -- Spreadsheets or test management exports must record sheet or suite names, row IDs, case IDs, priorities, statuses, and imported range coverage. -- Issue or bug repro sources must record stable URLs, repro steps, expected and actual behavior, environment, and linked artifacts. -- Remote source refs may use a stable URL 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 pretending integrity is complete. -- Mixed source packets must preserve source precedence and record duplicate or conflicting scenarios. - -## Vertical Scenario Coverage - -- Unit or component tests: capture subject under test, setup, inputs, assertions, mocks, and boundary cases. -- Integration or API tests: capture endpoints, contracts, fixtures, state transitions, permissions, and external dependencies. -- End-to-end tests: capture user journey, actor, preconditions, actions, expected outcomes, screenshots or traces, and environment assumptions. -- Manual QA cases: capture case IDs, steps, expected results, priority, test data, platform matrix, and pass or fail history. -- Regression or bug repros: capture trigger, affected version, expected behavior, actual behavior, assertion gap, and linked fix context. - -## Test Case Intake Facts - -`test-case-intake.yaml` must normalize existing behavioral evidence into engineering input. - -Required top-level fields: - -- test_case_intake_complete: -- source_refs_complete: -- scenario_count: -- assertions_complete: -- fixture_evidence_complete: -- coverage_gaps_recorded: -- assertion_gaps: -- fixture_or_test_data_gaps: -- coverage_gaps: -- flaky_or_skipped_cases: -- blocker_lint_errors: -- scenarios: - -Each scenario must include: - -- id: -- category: unit|component|integration|api|e2e|manual|regression|bug_repro|performance|accessibility|security -- scenario: -- source_refs: -- evidence_type: observed|inferred|missing|out_of_scope -- confidence: low|medium|high -- confidence_rationale: -- actors: -- preconditions: -- actions: -- expected_outcomes: -- assertions: -- fixtures_or_test_data: -- coverage_signal: - -When evidenced by the source, include provider-neutral optional fields: - -- tags: -- priority: -- risk: -- status: -- skipped_or_flaky_reason: -- related_requirement_refs: -- blockers: - -## Readiness Gate - -Test-case intake is ready only when: - -- source_integrity_complete: true -- test_case_intake_complete: true -- source_refs_complete: true -- scenario_count greater than 0 and equal to the number of records in `scenarios` -- when `assertions_complete: true`, scenario assertions are recorded; when false, `assertion_gaps` records the reason and source refs -- fixtures, mocks, or test data assumptions are recorded when relevant -- coverage gaps and flaky or skipped cases are explicit -- no blocker lint errors exist - -## Blocker Lint Errors - -- TEST_SOURCE_MANIFEST_MISSING -- TEST_SOURCE_TYPE_UNSUPPORTED -- TEST_SOURCE_FILE_MISSING -- TEST_SOURCE_HASH_MISMATCH -- TEST_SOURCE_INTEGRITY_INCOMPLETE -- TEST_CASE_INTAKE_MISSING -- TEST_SCENARIOS_UNTRACEABLE -- TEST_ASSERTIONS_MISSING -- TEST_FIXTURE_EVIDENCE_MISSING -- TEST_COVERAGE_GAPS_MISSING -- TEST_READY_WITHOUT_EVIDENCE -- TEST_EVIDENCE_PACKET_MISSING -- TEST_BLOCKER_LINT_ERRORS -- TEST_SCHEMA_INVALID - -## Gap Rules - -Record a gap instead of passing silently when test evidence is missing, untraceable, skipped, flaky, obsolete, duplicated, contradictory, or too implementation-specific to support product behavior. Inferred product intent must remain marked `evidence_type: inferred` or `[NEEDS CLARIFICATION]`. - -## Evidence Packet Metadata - -`evidence-packet.md` must start with YAML front matter: - -- ready_gate: `PASS|BLOCKED` -- blockers: -- source_ref_count: -- extracted_item_count: -- generated_at: - -Human-readable sections may summarize the same records, but readiness metadata is validated from the front matter when present. diff --git a/extensions/intake/templates/intake-test-cases-evidence-packet-template.md b/extensions/intake/templates/intake-test-cases-evidence-packet-template.md deleted file mode 100644 index 7bfd7d266c..0000000000 --- a/extensions/intake/templates/intake-test-cases-evidence-packet-template.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -ready_gate: BLOCKED -blockers: [] -source_ref_count: 0 -extracted_item_count: 0 -generated_at: ---- - -# Test Case Evidence Packet - -Purpose: summarize existing behavioral evidence for downstream Spec Kit workflows while preserving enough traceability to understand what current tests prove, where coverage is missing, and where product intent is only inferred. - -This packet is a human-readable readiness summary. Machine-readable behavioral evidence is recorded in `test-case-intake.yaml` and validated by `templates/schemas/test-case-intake.schema.json`. This packet does not treat tests as the sole product source of truth and does not define downstream-owned requirement IDs, implementation tasks, or ownership assignments. - -## Source - -- Source type: code|gherkin|spreadsheet|test_management|issue|mixed -- Source path or URL: -- Source files / suites / cases: -- Framework or format: -- Execution scope: -- Capture method: - -## Source Integrity - -- source-manifest.yaml: -- source files preserved: -- source checksums or snapshots verified: -- suite/file/range coverage: -- source_integrity_complete: - -## Scenario Route - -- Unit or component tests: -- Integration or API tests: -- End-to-end tests: -- Manual QA cases: -- Regression or bug repros: -- Scenario routing notes: - -## Extraction Context - -- Runtime agent: -- Input tooling availability: -- Test execution availability: -- Spreadsheet parsing availability: -- Test management export availability: -- Existing intake artifacts preserved: - -## Intake Readiness - -- source-manifest.yaml: -- test-case-intake.yaml: -- evidence-packet.md: -- source integrity completeness: -- test-case intake completeness: -- source refs completeness: -- assertion completeness: -- fixture/test data completeness: -- coverage gap reporting: -- blocker lint errors: -- readiness front matter synchronized: yes|no - -## Machine-Readable Artifacts - -- source-manifest.yaml: -- test-case-intake.yaml: -- schema validation result: -- readiness validator result: - -## Behavioral Evidence Summary - -- Scenarios: -- Preconditions: -- Actions: -- Expected outcomes: -- Assertions: -- Fixtures / mocks / test data: -- Environment assumptions: -- Tags / priorities / risks: -- Related requirement refs: - -## Gaps / Reliability - -- Missing assertions: -- Skipped tests: -- Flaky cases: -- Obsolete or duplicate cases: -- Coverage gaps: -- Product intent inferred from tests: -- Items marked `[NEEDS CLARIFICATION]`: - -## Traceability Summary - -- Source refs represented: -- Scenario categories represented: -- Assertion and fixture evidence represented: -- Coverage gaps requiring downstream clarification: - -## Consumer Handoff Notes - -- Supported requirement sections: -- Test evidence that must remain inferred: -- Source refs required in downstream artifacts: - -## Open Questions - -- [NEEDS CLARIFICATION] diff --git a/extensions/intake/templates/intake-visual-design-contract.md b/extensions/intake/templates/intake-visual-design-contract.md deleted file mode 100644 index f9e3ac757e..0000000000 --- a/extensions/intake/templates/intake-visual-design-contract.md +++ /dev/null @@ -1,227 +0,0 @@ -# Visual Design Intake Contract - -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 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. - -## Artifact Families - -Default root: - -```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 - -`design-source-manifest.yaml` must identify the original design source and preserve source integrity. - -Required source fields: - -- `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 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 - -- 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`. - -## Visual Requirements - -`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` -- `confidence_rationale` -- `engineering_action` -- `acceptance_check` -- `fidelity_level` - -`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. - -## Bounded Inference - -Visual intake may preserve direct observations, rule-backed inferred claims, and candidate completions. It must not smooth missing or contradictory evidence into confirmed behavior. - -- `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. - -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. - -## Visual IR - -Visual IR is the deterministic internal structure used to build the final static HTML. It is not a downstream implementation schema. - -Every visual IR file must include: - -- `ready_gate: PASS|BLOCKED` -- `blockers` -- source refs or upstream refs for every ready record - -The visual IR must cover: - -- 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 - -## Clarification Gate - -When evidence is missing and affects static HTML visual parity or user-operation replay, the command must ask the user instead of inventing behavior. - -`visual-ir/clarification-log.yaml` must record each question: - -- `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` - -Readiness is blocked when any required question is unanswered. - -## Static HTML Delivery - -`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. - -Delivery readiness requires: - -- 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 - -## Figma Provider Contract - -For `source_type: figma`, raw `get_metadata` output must be preserved in `figma-metadata.part-*.xml`. - -Readiness requires: - -- 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` and `delivery/evidence-packet.md` must start with YAML front matter: - -- `ready_gate: PASS|BLOCKED` -- `blockers` -- `source_ref_count` -- `extracted_item_count` -- `generated_at` - -Evidence packets summarize validator-backed readiness. They are not source-of-truth records. diff --git a/extensions/intake/templates/intake-visual-design-evidence-packet-template.md b/extensions/intake/templates/intake-visual-design-evidence-packet-template.md deleted file mode 100644 index 63085f7027..0000000000 --- a/extensions/intake/templates/intake-visual-design-evidence-packet-template.md +++ /dev/null @@ -1,168 +0,0 @@ ---- -ready_gate: BLOCKED -blockers: [] -source_ref_count: 0 -extracted_item_count: 0 -generated_at: ---- - -# Visual Design Evidence Packet - -Purpose: summarize provider-neutral visual design evidence for downstream Spec Kit workflows, while preserving enough traceability for downstream workflows to define their own visual verification criteria against the original design source. - -This packet is a human-readable readiness summary. Machine-readable visual facts are recorded in `visual-requirements.yaml` and validated by `templates/schemas/visual-requirements.schema.json`. This packet does not define downstream workflow schemas, downstream-owned item IDs, code component names, product semantics, or implementation ownership. - -## Design Source - -- Source type: image|pdf|markdown|figma -- Source path or URL: -- Source files / pages / frames / nodes: -- Design version / timestamp: -- Target platform: -- Required fidelity: low|medium|high -- Capture method: - -## Source Integrity - -- design-source-manifest.yaml: -- source files preserved: -- source checksums verified: -- page/frame/image coverage: -- source_integrity_complete: - -## Fidelity Profile - -- Fidelity level applied: -- Low-fidelity requirements captured: -- Medium-fidelity requirements captured: -- High-fidelity requirements captured: -- Accepted fidelity gaps: -- fidelity_rules_applied: - -## Extraction Context - -- Runtime agent: -- Input tooling availability: -- Figma MCP availability: -- Image/OCR availability: -- PDF render/text extraction availability: -- Markdown asset parsing availability: -- Screenshots or rendered pages captured: -- Variables / styles / tokens captured: -- Component metadata captured: - -## Intake Readiness - -- design-source-manifest.yaml: -- visual-requirements.yaml: -- visual-evidence-packet.md: -- figma-metadata.part-*.xml: -- figma-metadata.index.yaml: -- figma-node-inventory.yaml: -- source integrity completeness: -- visual requirements completeness: -- source refs completeness: -- fidelity rules completeness: -- visual parity plan completeness: -- Figma metadata completeness: -- Figma inventory parity: -- blocker lint errors: -- readiness front matter synchronized: yes|no - -## Machine-Readable Artifacts - -- design-source-manifest.yaml: -- visual-requirements.yaml: -- figma-metadata.index.yaml: -- figma-node-inventory.yaml: -- schema validation result: -- readiness validator result: - -## Observed Visual Fact Summary - -- Layout hierarchy: -- Spacing / sizing / grid: -- Typography: -- Colors / tokens: -- Effects: -- Assets: -- Components / variants: -- States: -- Prototype or interaction links: -- Responsive evidence: -- Accessibility evidence: - -## Bounded Inference Summary - -- Observed claim count: -- Inferred claim count: -- Candidate claim count: -- Unsupported claim count: -- Inference rules applied: -- Confidence methods applied: -- Candidate claims kept `downstream_use: reference_only`: -- Unsupported claims with blocker codes: -- Required clarification or acceptance gap: -- Conflict or parity blockers: - -## Missing / Needs Clarification - -- Business semantics: -- Dynamic states: -- Responsive behavior: -- Permissions: -- Validation: -- Error handling: -- Data source: -- Analytics / tracking: -- Items marked `[NEEDS CLARIFICATION]`: - -## Engineering Input Summary - -- Requirement categories represented: -- Source refs represented: -- Acceptance checks represented: -- Required fidelity: -- Blocker status: - -## Traceability Summary - -- Source evidence refs represented: -- Requirement categories represented: -- Comparison methods represented: -- Accepted exceptions: - -## Intake Parity Plan - -- Original design refs: -- Comparison method: visual_diff|measurement|token_match|asset_match|manual_review -- Thresholds: -- Blocking difference categories: -- Accepted exception policy: -- Intake readiness front matter synchronized: yes|no - -## Source Asset Summary - -- Asset ID: -- Asset role: -- Resource type: image|icon|video|lottie|svg|font -- Source ref: -- Source export requirement: -- Required variants: -- Blocker status: - -## Source Component Summary - -- Source component or pattern: -- Variant coverage: -- Missing mappings: - -## Consumer Handoff Notes - -- Supported requirement sections: -- Clarification items that must remain unresolved: -- Source refs required in downstream artifacts: - -## Open Questions - -- [NEEDS CLARIFICATION] diff --git a/extensions/intake/templates/schemas/figma-metadata-index.schema.json b/extensions/intake/templates/schemas/figma-metadata-index.schema.json deleted file mode 100644 index 2c7f63aa27..0000000000 --- a/extensions/intake/templates/schemas/figma-metadata-index.schema.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://spec-kit-intake.local/schemas/figma-metadata-index.schema.json", - "title": "Figma Metadata Index", - "type": "object", - "required": [ - "file_url", - "file_key", - "page_id", - "selected_node_ids", - "captured_at", - "mcp_tool", - "design_version_or_timestamp", - "selected_subtree_complete", - "raw_metadata_complete", - "expected_root_node_ids", - "captured_root_node_ids", - "missing_root_node_ids", - "gap_count", - "gaps", - "shards" - ], - "properties": { - "file_url": { "type": "string", "minLength": 1 }, - "file_key": { "type": "string", "minLength": 1 }, - "page_id": { "type": "string", "minLength": 1 }, - "selected_node_ids": { "type": "array" }, - "captured_at": { "type": "string", "minLength": 1 }, - "mcp_tool": { "const": "get_metadata" }, - "design_version_or_timestamp": { "type": "string", "minLength": 1 }, - "selected_subtree_complete": { "type": "boolean" }, - "raw_metadata_complete": { "type": "boolean" }, - "expected_root_node_ids": { "type": "array" }, - "captured_root_node_ids": { "type": "array" }, - "missing_root_node_ids": { "type": "array" }, - "gap_count": { "type": "integer", "minimum": 0 }, - "gaps": { "type": "array" }, - "shards": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": ["path", "byte_size", "sha256", "root_node_ids", "node_count", "truncated"], - "properties": { - "path": { "type": "string", "minLength": 1 }, - "byte_size": { "type": "integer", "minimum": 0 }, - "sha256": { "type": "string", "minLength": 1 }, - "root_node_ids": { "type": "array" }, - "node_count": { "type": "integer", "minimum": 0 }, - "truncated": { "type": "boolean" } - }, - "additionalProperties": true - } - } - }, - "additionalProperties": true -} diff --git a/extensions/intake/templates/schemas/figma-node-inventory.schema.json b/extensions/intake/templates/schemas/figma-node-inventory.schema.json deleted file mode 100644 index 3e3519f696..0000000000 --- a/extensions/intake/templates/schemas/figma-node-inventory.schema.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://spec-kit-intake.local/schemas/figma-node-inventory.schema.json", - "title": "Figma Node Inventory", - "type": "object", - "required": [ - "raw_node_count", - "inventory_node_count", - "excluded_node_count", - "missing_node_count", - "duplicate_node_count", - "truncated_raw_evidence", - "node_inventory_coverage", - "parity_passed" - ], - "properties": { - "raw_node_count": { "type": "integer", "minimum": 0 }, - "inventory_node_count": { "type": "integer", "minimum": 0 }, - "excluded_node_count": { "type": "integer", "minimum": 0 }, - "missing_node_count": { "type": "integer", "minimum": 0 }, - "duplicate_node_count": { "type": "integer", "minimum": 0 }, - "truncated_raw_evidence": { "type": "boolean" }, - "node_inventory_coverage": { "const": "100%" }, - "parity_passed": { "type": "boolean" } - }, - "additionalProperties": true -} diff --git a/extensions/intake/templates/schemas/figma-normalized-tree.schema.json b/extensions/intake/templates/schemas/figma-normalized-tree.schema.json deleted file mode 100644 index f07629f3ea..0000000000 --- a/extensions/intake/templates/schemas/figma-normalized-tree.schema.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://spec-kit-intake.local/schemas/figma-normalized-tree.schema.json", - "title": "Figma Normalized Layout Tree", - "type": "object", - "propertyNames": { "$ref": "#/$defs/no_downstream_field_name" }, - "required": [ - "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" - ], - "properties": { - "normalization_complete": { "type": "boolean" }, - "source_metadata_refs": { - "type": "array", - "minItems": 1, - "items": { "type": "string", "minLength": 1 } - }, - "source_index_ref": { "const": "figma-metadata.index.yaml" }, - "source_inventory_ref": { "const": "figma-node-inventory.yaml" }, - "normalization_rules_applied": { - "type": "array", - "minItems": 3, - "items": { "type": "string", "minLength": 1 } - }, - "rename_rule": { "type": "string", "minLength": 1 }, - "group_rule": { "type": "string", "minLength": 1 }, - "sort_rule": { - "enum": [ - "top_to_bottom_left_to_right_depth_sibling_id" - ] - }, - "raw_node_count": { "type": "integer", "minimum": 0 }, - "normalized_node_count": { "type": "integer", "minimum": 0 }, - "node_coverage": { "const": "100%" }, - "selected_node_ids": { "type": "array" }, - "gaps": { "type": "array" }, - "nodes": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "propertyNames": { "$ref": "#/$defs/no_downstream_field_name" }, - "required": [ - "source_node_id", - "original_name", - "normalized_name", - "node_type", - "role_hint", - "group_key", - "parent_group_key", - "sort_key", - "visual_order", - "source_refs" - ], - "properties": { - "source_node_id": { "type": "string", "minLength": 1 }, - "parent_source_node_id": { - "anyOf": [ - { "type": "string", "minLength": 1 }, - { "type": "null" } - ] - }, - "original_name": { "type": "string" }, - "normalized_name": { "type": "string", "minLength": 1 }, - "node_type": { "type": "string", "minLength": 1 }, - "role_hint": { "type": "string", "minLength": 1 }, - "group_key": { "type": "string", "minLength": 1 }, - "parent_group_key": { - "anyOf": [ - { "type": "string", "minLength": 1 }, - { "type": "null" } - ] - }, - "sort_key": { - "type": "object", - "required": ["method", "value"], - "properties": { - "method": { "const": "top_to_bottom_left_to_right_depth_sibling_id" }, - "value": { - "type": "array", - "minItems": 5 - } - }, - "additionalProperties": true - }, - "visual_order": { "type": "integer", "minimum": 1 }, - "bounds_ref": { - "type": "object", - "properties": { - "x": { "type": "number" }, - "y": { "type": "number" }, - "width": { "type": "number" }, - "height": { "type": "number" } - }, - "additionalProperties": true - }, - "source_refs": { - "type": "array", - "minItems": 1, - "items": { "type": "string", "minLength": 1 } - } - }, - "additionalProperties": true - } - } - }, - "$defs": { - "no_downstream_field_name": { - "not": { - "pattern": "(preview|delivery|screenshot|diff|evidence_packet|requirement|implementation|task|selector|locator|code_component|component_name|visual_spec|acceptance|prd|test_case)" - } - } - }, - "additionalProperties": true -} diff --git a/extensions/intake/templates/schemas/prd-intake.schema.json b/extensions/intake/templates/schemas/prd-intake.schema.json deleted file mode 100644 index 43bce129fe..0000000000 --- a/extensions/intake/templates/schemas/prd-intake.schema.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://spec-kit-intake.local/schemas/prd-intake.schema.json", - "title": "PRD Intake", - "type": "object", - "required": [ - "prd_intake_complete", - "source_refs_complete", - "extracted_fact_count", - "acceptance_evidence_complete", - "unresolved_ambiguity_marked", - "acceptance_gaps", - "open_questions", - "blocker_lint_errors", - "facts" - ], - "properties": { - "prd_intake_complete": { "type": "boolean" }, - "source_refs_complete": { "type": "boolean" }, - "extracted_fact_count": { "type": "integer", "minimum": 0 }, - "acceptance_evidence_complete": { "type": "boolean" }, - "unresolved_ambiguity_marked": { "type": "boolean" }, - "acceptance_gaps": { "type": "array" }, - "open_questions": { "type": "array" }, - "blocker_lint_errors": { "type": "array" }, - "facts": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "id", - "category", - "statement", - "source_refs", - "evidence_type", - "confidence", - "confidence_rationale", - "downstream_hint", - "acceptance_or_validation_signal" - ], - "properties": { - "id": { "type": "string", "minLength": 1 }, - "category": { - "enum": [ - "goal", - "non_goal", - "user", - "actor", - "scope", - "flow", - "business_rule", - "data", - "permission", - "integration", - "compliance", - "acceptance", - "metric", - "risk", - "open_question" - ] - }, - "statement": { "type": "string", "minLength": 1 }, - "source_refs": { - "type": "array", - "minItems": 1, - "items": { "type": "string", "minLength": 1 } - }, - "evidence_type": { "enum": ["observed", "inferred", "missing", "out_of_scope"] }, - "confidence": { "enum": ["low", "medium", "high"] }, - "confidence_rationale": { "type": "string", "minLength": 1 }, - "downstream_hint": { "type": "string", "minLength": 1 }, - "acceptance_or_validation_signal": { "type": "string", "minLength": 1 }, - "blockers": { - "type": "array", - "items": { "$ref": "#/$defs/blocker_code" } - } - }, - "additionalProperties": true - } - } - }, - "$defs": { - "blocker_code": { - "enum": [ - "PRD_SOURCE_MANIFEST_MISSING", - "PRD_SOURCE_TYPE_UNSUPPORTED", - "PRD_SOURCE_FILE_MISSING", - "PRD_SOURCE_HASH_MISMATCH", - "PRD_SOURCE_INTEGRITY_INCOMPLETE", - "PRD_INTAKE_MISSING", - "PRD_FACTS_UNTRACEABLE", - "PRD_ACCEPTANCE_EVIDENCE_MISSING", - "PRD_CLARIFICATION_MARKING_MISSING", - "PRD_READY_WITHOUT_EVIDENCE", - "PRD_EVIDENCE_PACKET_MISSING", - "PRD_BLOCKER_LINT_ERRORS", - "PRD_SCHEMA_INVALID" - ] - } - }, - "additionalProperties": true -} diff --git a/extensions/intake/templates/schemas/prd-source-manifest.schema.json b/extensions/intake/templates/schemas/prd-source-manifest.schema.json deleted file mode 100644 index 247488e6d7..0000000000 --- a/extensions/intake/templates/schemas/prd-source-manifest.schema.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://spec-kit-intake.local/schemas/prd-source-manifest.schema.json", - "title": "PRD Source Manifest", - "type": "object", - "required": [ - "source_type", - "source_files", - "source_integrity_complete", - "captured_at", - "capture_method", - "document_version", - "extraction_scope" - ], - "properties": { - "source_type": { "enum": ["markdown", "pdf", "doc", "url", "issue", "mixed"] }, - "source_integrity_complete": { "type": "boolean" }, - "captured_at": { "type": "string", "minLength": 1 }, - "capture_method": { "type": "string", "minLength": 1 }, - "document_version": { "type": "string", "minLength": 1 }, - "extraction_scope": { "type": "string", "minLength": 1 }, - "source_files": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/source_file" } - }, - "source_details": { "type": "object" }, - "retrieval_metadata": { "type": "object" }, - "snapshot_status": { "enum": ["captured", "not_available", "not_required"] }, - "integrity_gap_reason": { "type": "string", "minLength": 1 }, - "source_precedence": { "type": "array" } - }, - "allOf": [ - { - "if": { "properties": { "source_type": { "const": "markdown" } } }, - "then": { - "required": ["source_details"], - "properties": { - "source_details": { - "type": "object", - "required": ["heading_coverage", "parsed_section_coverage"], - "properties": { - "heading_coverage": { "type": "string", "minLength": 1 }, - "parsed_section_coverage": { "type": "string", "minLength": 1 }, - "linked_asset_refs": { "type": "array" } - }, - "additionalProperties": true - } - } - } - }, - { - "if": { "properties": { "source_type": { "enum": ["pdf", "doc"] } } }, - "then": { - "required": ["source_details"], - "properties": { - "source_details": { - "type": "object", - "required": ["page_count", "processed_page_count", "text_extraction_status"], - "properties": { - "page_count": { "type": "integer", "minimum": 1 }, - "processed_page_count": { "type": "integer", "minimum": 0 }, - "text_extraction_status": { "enum": ["complete", "partial", "unavailable"] } - }, - "additionalProperties": true - } - } - } - }, - { - "if": { "properties": { "source_type": { "enum": ["url", "issue"] } } }, - "then": { - "required": ["retrieval_metadata", "snapshot_status"], - "properties": { - "retrieval_metadata": { - "type": "object", - "required": ["retrieved_at", "stable_url", "visible_title"], - "properties": { - "retrieved_at": { "type": "string", "minLength": 1 }, - "stable_url": { "type": "string", "minLength": 1 }, - "visible_title": { "type": "string", "minLength": 1 }, - "author_or_owner": { "type": "string" } - }, - "additionalProperties": true - } - } - } - }, - { - "if": { - "properties": { - "source_files": { - "contains": { - "type": "object", - "properties": { - "path": { "pattern": "^(https?://|figma://)" } - }, - "required": ["path"] - } - } - }, - "required": ["source_files"] - }, - "then": { "required": ["snapshot_status", "integrity_gap_reason"] } - }, - { - "if": { - "properties": { - "snapshot_status": { "enum": ["not_available", "not_required"] } - }, - "required": ["snapshot_status"] - }, - "then": { "required": ["integrity_gap_reason"] } - }, - { - "if": { "properties": { "source_type": { "const": "mixed" } } }, - "then": { "required": ["source_precedence"] } - } - ], - "$defs": { - "source_file": { - "type": "object", - "required": ["path", "mime_type", "role"], - "properties": { - "path": { "type": "string", "minLength": 1 }, - "mime_type": { "type": "string", "minLength": 1 }, - "byte_size": { "type": "integer", "minimum": 0 }, - "sha256": { "type": "string", "minLength": 1 }, - "checksum_status": { "enum": ["verified", "unavailable", "not_applicable"] }, - "role": { "enum": ["original", "export", "attachment", "snapshot"] } - }, - "allOf": [ - { - "if": { - "properties": { - "path": { "pattern": "^(https?://|figma://)" } - }, - "required": ["path"] - }, - "then": { - "anyOf": [ - { "required": ["byte_size", "sha256"] }, - { - "required": ["checksum_status"], - "properties": { - "checksum_status": { "enum": ["unavailable", "not_applicable"] } - } - } - ] - }, - "else": { "required": ["byte_size", "sha256"] } - } - ], - "additionalProperties": true - } - }, - "additionalProperties": true -} diff --git a/extensions/intake/templates/schemas/static-html-delivery.schema.json b/extensions/intake/templates/schemas/static-html-delivery.schema.json deleted file mode 100644 index 8e33074e94..0000000000 --- a/extensions/intake/templates/schemas/static-html-delivery.schema.json +++ /dev/null @@ -1,240 +0,0 @@ -{ - "$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/test-case-intake.schema.json b/extensions/intake/templates/schemas/test-case-intake.schema.json deleted file mode 100644 index ab22c2fb4c..0000000000 --- a/extensions/intake/templates/schemas/test-case-intake.schema.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://spec-kit-intake.local/schemas/test-case-intake.schema.json", - "title": "Test Case Intake", - "type": "object", - "required": [ - "test_case_intake_complete", - "source_refs_complete", - "scenario_count", - "assertions_complete", - "fixture_evidence_complete", - "coverage_gaps_recorded", - "assertion_gaps", - "fixture_or_test_data_gaps", - "coverage_gaps", - "flaky_or_skipped_cases", - "blocker_lint_errors", - "scenarios" - ], - "properties": { - "test_case_intake_complete": { "type": "boolean" }, - "source_refs_complete": { "type": "boolean" }, - "scenario_count": { "type": "integer", "minimum": 0 }, - "assertions_complete": { "type": "boolean" }, - "fixture_evidence_complete": { "type": "boolean" }, - "coverage_gaps_recorded": { "type": "boolean" }, - "assertion_gaps": { "type": "array" }, - "fixture_or_test_data_gaps": { "type": "array" }, - "coverage_gaps": { "type": "array" }, - "flaky_or_skipped_cases": { "type": "array" }, - "blocker_lint_errors": { "type": "array" }, - "scenarios": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": [ - "id", - "category", - "scenario", - "source_refs", - "evidence_type", - "confidence", - "confidence_rationale", - "actors", - "preconditions", - "actions", - "expected_outcomes", - "assertions", - "fixtures_or_test_data", - "coverage_signal" - ], - "properties": { - "id": { "type": "string", "minLength": 1 }, - "category": { - "enum": [ - "unit", - "component", - "integration", - "api", - "e2e", - "manual", - "regression", - "bug_repro", - "performance", - "accessibility", - "security" - ] - }, - "scenario": { "type": "string", "minLength": 1 }, - "source_refs": { - "type": "array", - "minItems": 1, - "items": { "type": "string", "minLength": 1 } - }, - "evidence_type": { "enum": ["observed", "inferred", "missing", "out_of_scope"] }, - "confidence": { "enum": ["low", "medium", "high"] }, - "confidence_rationale": { "type": "string", "minLength": 1 }, - "actors": { "type": "array" }, - "preconditions": { "type": "array" }, - "actions": { "type": "array" }, - "expected_outcomes": { "type": "array" }, - "assertions": { "type": "array" }, - "fixtures_or_test_data": { "type": "array" }, - "coverage_signal": { "type": "string", "minLength": 1 }, - "blockers": { - "type": "array", - "items": { "$ref": "#/$defs/blocker_code" } - } - }, - "additionalProperties": true - } - } - }, - "$defs": { - "blocker_code": { - "enum": [ - "TEST_SOURCE_MANIFEST_MISSING", - "TEST_SOURCE_TYPE_UNSUPPORTED", - "TEST_SOURCE_FILE_MISSING", - "TEST_SOURCE_HASH_MISMATCH", - "TEST_SOURCE_INTEGRITY_INCOMPLETE", - "TEST_CASE_INTAKE_MISSING", - "TEST_SCENARIOS_UNTRACEABLE", - "TEST_ASSERTIONS_MISSING", - "TEST_FIXTURE_EVIDENCE_MISSING", - "TEST_COVERAGE_GAPS_MISSING", - "TEST_READY_WITHOUT_EVIDENCE", - "TEST_EVIDENCE_PACKET_MISSING", - "TEST_BLOCKER_LINT_ERRORS", - "TEST_SCHEMA_INVALID" - ] - } - }, - "additionalProperties": true -} diff --git a/extensions/intake/templates/schemas/test-case-source-manifest.schema.json b/extensions/intake/templates/schemas/test-case-source-manifest.schema.json deleted file mode 100644 index e5a5feeae6..0000000000 --- a/extensions/intake/templates/schemas/test-case-source-manifest.schema.json +++ /dev/null @@ -1,176 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://spec-kit-intake.local/schemas/test-case-source-manifest.schema.json", - "title": "Test Case Source Manifest", - "type": "object", - "required": [ - "source_type", - "source_files", - "source_integrity_complete", - "captured_at", - "capture_method", - "framework_or_format", - "execution_scope" - ], - "properties": { - "source_type": { "enum": ["code", "gherkin", "spreadsheet", "test_management", "issue", "mixed"] }, - "source_integrity_complete": { "type": "boolean" }, - "captured_at": { "type": "string", "minLength": 1 }, - "capture_method": { "type": "string", "minLength": 1 }, - "framework_or_format": { "type": "string", "minLength": 1 }, - "execution_scope": { "type": "string", "minLength": 1 }, - "source_files": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/source_file" } - }, - "source_details": { "type": "object" }, - "retrieval_metadata": { "type": "object" }, - "snapshot_status": { "enum": ["captured", "not_available", "not_required"] }, - "integrity_gap_reason": { "type": "string", "minLength": 1 }, - "source_precedence": { "type": "array" } - }, - "allOf": [ - { - "if": { "properties": { "source_type": { "const": "code" } } }, - "then": { - "required": ["source_details"], - "properties": { - "source_details": { - "type": "object", - "required": ["test_names", "execution_status"], - "properties": { - "test_names": { "type": "array", "minItems": 1 }, - "execution_status": { "enum": ["passed", "failed", "not_run", "unknown"] }, - "skipped_markers": { "type": "array" }, - "fixture_refs": { "type": "array" } - }, - "additionalProperties": true - } - } - } - }, - { - "if": { "properties": { "source_type": { "const": "gherkin" } } }, - "then": { - "required": ["source_details"], - "properties": { - "source_details": { - "type": "object", - "required": ["feature_names", "scenario_count", "step_coverage"], - "properties": { - "feature_names": { "type": "array", "minItems": 1 }, - "scenario_count": { "type": "integer", "minimum": 0 }, - "step_coverage": { "type": "string", "minLength": 1 } - }, - "additionalProperties": true - } - } - } - }, - { - "if": { "properties": { "source_type": { "enum": ["spreadsheet", "test_management"] } } }, - "then": { - "required": ["source_details"], - "properties": { - "source_details": { - "type": "object", - "required": ["suite_or_sheet_names", "imported_range_coverage"], - "properties": { - "suite_or_sheet_names": { "type": "array", "minItems": 1 }, - "imported_range_coverage": { "type": "string", "minLength": 1 }, - "case_id_fields": { "type": "array" } - }, - "additionalProperties": true - } - } - } - }, - { - "if": { "properties": { "source_type": { "const": "issue" } } }, - "then": { - "required": ["retrieval_metadata", "snapshot_status"], - "properties": { - "retrieval_metadata": { - "type": "object", - "required": ["retrieved_at", "stable_url", "visible_title"], - "properties": { - "retrieved_at": { "type": "string", "minLength": 1 }, - "stable_url": { "type": "string", "minLength": 1 }, - "visible_title": { "type": "string", "minLength": 1 } - }, - "additionalProperties": true - } - } - } - }, - { - "if": { - "properties": { - "source_files": { - "contains": { - "type": "object", - "properties": { - "path": { "pattern": "^(https?://|figma://)" } - }, - "required": ["path"] - } - } - }, - "required": ["source_files"] - }, - "then": { "required": ["snapshot_status", "integrity_gap_reason"] } - }, - { - "if": { - "properties": { - "snapshot_status": { "enum": ["not_available", "not_required"] } - }, - "required": ["snapshot_status"] - }, - "then": { "required": ["integrity_gap_reason"] } - }, - { - "if": { "properties": { "source_type": { "const": "mixed" } } }, - "then": { "required": ["source_precedence"] } - } - ], - "$defs": { - "source_file": { - "type": "object", - "required": ["path", "mime_type", "role"], - "properties": { - "path": { "type": "string", "minLength": 1 }, - "mime_type": { "type": "string", "minLength": 1 }, - "byte_size": { "type": "integer", "minimum": 0 }, - "sha256": { "type": "string", "minLength": 1 }, - "checksum_status": { "enum": ["verified", "unavailable", "not_applicable"] }, - "role": { "enum": ["original", "export", "attachment", "snapshot"] } - }, - "allOf": [ - { - "if": { - "properties": { - "path": { "pattern": "^(https?://|figma://)" } - }, - "required": ["path"] - }, - "then": { - "anyOf": [ - { "required": ["byte_size", "sha256"] }, - { - "required": ["checksum_status"], - "properties": { - "checksum_status": { "enum": ["unavailable", "not_applicable"] } - } - } - ] - }, - "else": { "required": ["byte_size", "sha256"] } - } - ], - "additionalProperties": true - } - }, - "additionalProperties": true -} diff --git a/extensions/intake/templates/schemas/visual-requirements.schema.json b/extensions/intake/templates/schemas/visual-requirements.schema.json deleted file mode 100644 index 19b69430ee..0000000000 --- a/extensions/intake/templates/schemas/visual-requirements.schema.json +++ /dev/null @@ -1,264 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://spec-kit-intake.local/schemas/visual-requirements.schema.json", - "title": "Visual Requirements Intake", - "type": "object", - "propertyNames": { "$ref": "#/$defs/no_normalization_field_name" }, - "required": [ - "visual_requirements_complete", - "visual_requirements_count", - "source_refs_complete", - "fidelity_rules_applied", - "visual_parity_plan_complete", - "parity_plan", - "requirements" - ], - "properties": { - "visual_requirements_complete": { "type": "boolean" }, - "visual_requirements_count": { "type": "integer", "minimum": 0 }, - "source_refs_complete": { "type": "boolean" }, - "fidelity_rules_applied": { "type": "boolean" }, - "visual_parity_plan_complete": { "type": "boolean" }, - "blocker_lint_errors": { "type": "array" }, - "parity_plan": { - "type": "object", - "required": [ - "comparison_targets", - "original_refs", - "comparison_method", - "thresholds", - "accepted_exceptions", - "blocking_difference_categories" - ], - "properties": { - "comparison_targets": { "type": "array", "minItems": 1 }, - "original_refs": { "type": "array", "minItems": 1 }, - "comparison_method": { - "enum": ["visual_diff", "measurement", "token_match", "asset_match", "manual_review"] - }, - "thresholds": { - "type": "object", - "minProperties": 1, - "properties": { - "max_pixel_diff_percent": { "type": "number", "minimum": 0 }, - "max_spacing_delta_px": { "type": "number", "minimum": 0 }, - "token_match_required": { "type": "boolean" }, - "asset_match_required": { "type": "boolean" }, - "manual_review_checklist": { "type": "array", "minItems": 1 } - }, - "additionalProperties": true - }, - "accepted_exceptions": { "type": "array" }, - "blocking_difference_categories": { "type": "array", "minItems": 1 } - }, - "additionalProperties": true - }, - "requirements": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "propertyNames": { "$ref": "#/$defs/no_normalization_field_name" }, - "required": [ - "id", - "category", - "requirement", - "source_refs", - "evidence_type", - "confidence", - "confidence_rationale", - "engineering_action", - "acceptance_check", - "fidelity_level" - ], - "properties": { - "id": { "type": "string", "minLength": 1 }, - "category": { - "enum": [ - "layout", - "spacing", - "sizing", - "typography", - "color", - "asset", - "component", - "state", - "interaction", - "responsive", - "accessibility", - "content" - ] - }, - "requirement": { "type": "string", "minLength": 1 }, - "source_refs": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/source_ref" } - }, - "evidence_type": { - "enum": ["observed", "inferred", "candidate", "unsupported", "missing", "out_of_scope"] - }, - "confidence": { "enum": ["low", "medium", "high"] }, - "confidence_rationale": { "type": "string", "minLength": 1 }, - "inference_rule": { "type": "string", "minLength": 1 }, - "confidence_method": { "type": "string", "minLength": 1 }, - "score_breakdown": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "required": ["signal", "weight"], - "properties": { - "signal": { "type": "string", "minLength": 1 }, - "weight": { "type": "number" }, - "source_refs": { - "type": "array", - "items": { "$ref": "#/$defs/source_ref" } - } - }, - "additionalProperties": true - } - }, - "downstream_use": { - "enum": ["accepted_claim", "reference_only", "blocked", "out_of_scope"] - }, - "missing_evidence": { - "type": "array", - "minItems": 1, - "items": { "type": "string", "minLength": 1 } - }, - "blocking_conditions": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "blocker_code": { "$ref": "#/$defs/blocker_code" }, - "reason": { "type": "string", "minLength": 1 }, - "engineering_action": { "type": "string", "minLength": 1 }, - "acceptance_check": { "type": "string", "minLength": 1 }, - "fidelity_level": { "enum": ["low", "medium", "high"] }, - "blockers": { - "type": "array", - "items": { "$ref": "#/$defs/blocker_code" } - } - }, - "allOf": [ - { - "if": { - "required": ["evidence_type"], - "properties": { "evidence_type": { "const": "inferred" } } - }, - "then": { - "required": [ - "inference_rule", - "confidence_method", - "score_breakdown", - "downstream_use", - "blocking_conditions" - ], - "properties": { - "confidence": { "const": "high" }, - "downstream_use": { "const": "accepted_claim" } - } - } - }, - { - "if": { - "required": ["evidence_type"], - "properties": { "evidence_type": { "const": "candidate" } } - }, - "then": { - "required": [ - "inference_rule", - "confidence_method", - "score_breakdown", - "downstream_use", - "missing_evidence", - "blocking_conditions" - ], - "properties": { - "confidence": { "enum": ["low", "medium"] }, - "downstream_use": { "const": "reference_only" } - } - } - }, - { - "if": { - "required": ["evidence_type"], - "properties": { "evidence_type": { "const": "unsupported" } } - }, - "then": { - "required": [ - "blocker_code", - "reason", - "downstream_use", - "missing_evidence", - "blockers" - ], - "properties": { - "downstream_use": { "const": "blocked" }, - "blockers": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/blocker_code" } - } - } - } - } - ], - "additionalProperties": true - } - } - }, - "$defs": { - "blocker_code": { - "enum": [ - "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" - ] - }, - "source_ref": { - "type": "string", - "minLength": 1, - "not": { - "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|visual_diff_ref)" - } - } - }, - "additionalProperties": true -} diff --git a/extensions/intake/templates/schemas/visual-source-manifest.schema.json b/extensions/intake/templates/schemas/visual-source-manifest.schema.json deleted file mode 100644 index b3c19d46e1..0000000000 --- a/extensions/intake/templates/schemas/visual-source-manifest.schema.json +++ /dev/null @@ -1,186 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://spec-kit-intake.local/schemas/visual-source-manifest.schema.json", - "title": "Visual Design Source Manifest", - "type": "object", - "required": [ - "source_type", - "required_fidelity", - "source_files", - "source_integrity_complete", - "captured_at", - "capture_method", - "page_or_frame_count", - "processed_count", - "extraction_scope" - ], - "properties": { - "source_type": { "enum": ["image", "pdf", "markdown", "figma"] }, - "required_fidelity": { "enum": ["low", "medium", "high"] }, - "source_integrity_complete": { "type": "boolean" }, - "captured_at": { "type": "string", "minLength": 1 }, - "capture_method": { "type": "string", "minLength": 1 }, - "page_or_frame_count": { "type": "integer", "minimum": 0 }, - "processed_count": { "type": "integer", "minimum": 0 }, - "extraction_scope": { "type": "string", "minLength": 1 }, - "source_files": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/source_file" } - }, - "source_details": { "type": "object" }, - "retrieval_metadata": { "type": "object" }, - "snapshot_status": { "enum": ["captured", "not_available", "not_required"] }, - "integrity_gap_reason": { "type": "string", "minLength": 1 } - }, - "allOf": [ - { - "if": { "properties": { "source_type": { "const": "image" } } }, - "then": { - "required": ["source_details"], - "properties": { - "source_details": { - "type": "object", - "required": ["image_dimensions", "region_coverage", "ocr_status"], - "properties": { - "image_dimensions": { - "type": "object", - "required": ["width_px", "height_px"], - "properties": { - "width_px": { "type": "integer", "minimum": 1 }, - "height_px": { "type": "integer", "minimum": 1 } - }, - "additionalProperties": true - }, - "region_coverage": { "type": "string", "minLength": 1 }, - "ocr_status": { "enum": ["complete", "partial", "not_applicable", "unavailable"] }, - "asset_refs": { "type": "array" } - }, - "additionalProperties": true - } - } - } - }, - { - "if": { "properties": { "source_type": { "const": "pdf" } } }, - "then": { - "required": ["source_details"], - "properties": { - "source_details": { - "type": "object", - "required": ["page_count", "processed_page_count", "rendered_page_refs", "text_extraction_status"], - "properties": { - "page_count": { "type": "integer", "minimum": 1 }, - "processed_page_count": { "type": "integer", "minimum": 0 }, - "rendered_page_refs": { "type": "array", "minItems": 1 }, - "text_extraction_status": { "enum": ["complete", "partial", "unavailable"] } - }, - "additionalProperties": true - } - } - } - }, - { - "if": { "properties": { "source_type": { "const": "markdown" } } }, - "then": { - "required": ["source_details"], - "properties": { - "source_details": { - "type": "object", - "required": ["heading_structure", "design_note_parsing_status"], - "properties": { - "heading_structure": { "type": "array", "minItems": 1 }, - "embedded_or_linked_asset_refs": { "type": "array" }, - "design_note_parsing_status": { "enum": ["complete", "partial", "unavailable"] } - }, - "additionalProperties": true - } - } - } - }, - { - "if": { "properties": { "source_type": { "const": "figma" } } }, - "then": { - "required": ["source_details"], - "properties": { - "source_details": { - "type": "object", - "required": ["file_url", "file_key", "selected_node_ids"], - "properties": { - "file_url": { "type": "string", "minLength": 1 }, - "file_key": { "type": "string", "minLength": 1 }, - "selected_node_ids": { "type": "array", "minItems": 1 } - }, - "additionalProperties": true - } - } - } - }, - { - "if": { - "properties": { - "source_files": { - "contains": { - "type": "object", - "properties": { - "path": { "pattern": "^(https?://|figma://)" } - }, - "required": ["path"] - } - } - }, - "required": ["source_files"] - }, - "then": { "required": ["snapshot_status", "integrity_gap_reason"] } - }, - { - "if": { - "properties": { - "snapshot_status": { "enum": ["not_available", "not_required"] } - }, - "required": ["snapshot_status"] - }, - "then": { "required": ["integrity_gap_reason"] } - } - ], - "$defs": { - "source_file": { - "type": "object", - "required": ["path", "mime_type", "role"], - "properties": { - "path": { "type": "string", "minLength": 1 }, - "mime_type": { "type": "string", "minLength": 1 }, - "byte_size": { "type": "integer", "minimum": 0 }, - "sha256": { "type": "string", "minLength": 1 }, - "checksum_status": { "enum": ["verified", "unavailable", "not_applicable"] }, - "role": { - "enum": ["original", "rendered_page", "screenshot", "asset", "markdown"] - } - }, - "allOf": [ - { - "if": { - "properties": { - "path": { "pattern": "^(https?://|figma://)" } - }, - "required": ["path"] - }, - "then": { - "anyOf": [ - { "required": ["byte_size", "sha256"] }, - { - "required": ["checksum_status"], - "properties": { - "checksum_status": { "enum": ["unavailable", "not_applicable"] } - } - } - ] - }, - "else": { "required": ["byte_size", "sha256"] } - } - ], - "additionalProperties": true - } - }, - "additionalProperties": true -} diff --git a/extensions/intake/tests/test_extension_contract.py b/extensions/intake/tests/test_extension_contract.py deleted file mode 100644 index cf5d8bf8fc..0000000000 --- a/extensions/intake/tests/test_extension_contract.py +++ /dev/null @@ -1,2308 +0,0 @@ -import os -import json -import shutil -import subprocess -import sys -from pathlib import Path - -import pytest -import yaml - - -ROOT = Path(__file__).resolve().parents[1] -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" -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" - - -def write_visual_intake_fixture(intake: Path, source_type: str, fidelity: str, file_name: str): - intake.mkdir(parents=True, exist_ok=True) - source_dir = intake / "source-files" - source_dir.mkdir() - source = source_dir / file_name - source.write_bytes(f"{source_type}:{fidelity}:source".encode("utf-8")) - - import hashlib - - digest = hashlib.sha256(source.read_bytes()).hexdigest() - rel_source = f"source-files/{file_name}" - if source_type == "image": - source_details = [ - "source_details:", - " image_dimensions:", - " width_px: 100", - " height_px: 100", - " region_coverage: full", - " ocr_status: not_applicable", - ] - elif source_type == "pdf": - source_details = [ - "source_details:", - " page_count: 1", - " processed_page_count: 1", - " rendered_page_refs:", - f" - {rel_source}#page=1", - " text_extraction_status: complete", - ] - elif source_type == "markdown": - source_details = [ - "source_details:", - " heading_structure:", - " - Design brief", - " embedded_or_linked_asset_refs: []", - " design_note_parsing_status: complete", - ] - elif source_type == "figma": - source_details = [ - "source_details:", - " file_url: https://www.figma.com/file/example", - " file_key: example", - " selected_node_ids:", - " - '1'", - ] - else: - source_details = [] - - (intake / "design-source-manifest.yaml").write_text( - "\n".join( - [ - f"source_type: {source_type}", - f"required_fidelity: {fidelity}", - "source_integrity_complete: true", - "captured_at: '2026-06-23T00:00:00Z'", - "capture_method: local_fixture", - "page_or_frame_count: 1", - "processed_count: 1", - "extraction_scope: full", - "source_files:", - f" - path: {rel_source}", - " mime_type: application/octet-stream", - f" byte_size: {source.stat().st_size}", - f" sha256: {digest}", - " role: original", - *source_details, - "", - ] - ), - encoding="utf-8", - ) - - (intake / "visual-requirements.yaml").write_text( - "\n".join( - [ - "visual_requirements_complete: true", - "visual_requirements_count: 1", - "source_refs_complete: true", - "fidelity_rules_applied: true", - "visual_parity_plan_complete: true", - "blocker_lint_errors: []", - "parity_plan:", - " comparison_targets:", - " - primary_surface", - f" original_refs: ['{rel_source}#full']", - " comparison_method: manual_review", - " thresholds:", - " manual_review_checklist:", - " - compare primary hierarchy", - " accepted_exceptions: []", - " blocking_difference_categories:", - " - missing_required_visual_fact", - "requirements:", - " - id: VR-001", - " category: layout", - " requirement: Preserve primary content hierarchy", - f" source_refs: ['{rel_source}#full']", - " evidence_type: observed", - " confidence: high", - " confidence_rationale: Source artifact directly shows the primary hierarchy.", - " engineering_action: Implement matching hierarchy", - " acceptance_check: Compare implementation screenshot with source", - f" fidelity_level: {fidelity}", - "", - ] - ), - encoding="utf-8", - ) - - (intake / "visual-evidence-packet.md").write_text( - "---\n" - "ready_gate: PASS\n" - "blockers: []\n" - "source_ref_count: 1\n" - "extracted_item_count: 1\n" - "generated_at: '2026-06-23T00:00:00Z'\n" - "---\n" - "# Visual Design Evidence Packet\n", - encoding="utf-8", - ) - - -def write_figma_metadata_fixture(intake: Path): - import hashlib - - metadata = intake / "figma-metadata.part-001.xml" - metadata.write_text( - '\n', - encoding="utf-8", - ) - digest = hashlib.sha256(metadata.read_bytes()).hexdigest() - (intake / "figma-metadata.index.yaml").write_text( - "\n".join( - [ - "file_url: https://www.figma.com/file/example", - "file_key: example", - "page_id: page-1", - "selected_node_ids: ['1']", - "captured_at: '2026-06-23T00:00:00Z'", - "mcp_tool: get_metadata", - "design_version_or_timestamp: '2026-06-23T00:00:00Z'", - "selected_subtree_complete: true", - "raw_metadata_complete: true", - "expected_root_node_ids: ['1']", - "captured_root_node_ids: ['1']", - "missing_root_node_ids: []", - "gap_count: 0", - "gaps: []", - "shards:", - " - path: figma-metadata.part-001.xml", - f" byte_size: {metadata.stat().st_size}", - f" sha256: {digest}", - " root_node_ids: ['1']", - " node_count: 2", - " truncated: false", - "", - ] - ), - encoding="utf-8", - ) - (intake / "figma-node-inventory.yaml").write_text( - "\n".join( - [ - "raw_node_count: 2", - "inventory_node_count: 2", - "excluded_node_count: 0", - "missing_node_count: 0", - "duplicate_node_count: 0", - "truncated_raw_evidence: false", - "node_inventory_coverage: 100%", - "parity_passed: true", - "", - ] - ), - encoding="utf-8", - ) - write_figma_normalized_tree_fixture(intake, ["1", "2"]) - - -def write_figma_normalized_tree_fixture(intake: Path, source_node_ids=None): - source_node_ids = source_node_ids or ["1"] - nodes = [] - for index, node_id in enumerate(source_node_ids, start=1): - parent_id = source_node_ids[0] if index > 1 else None - nodes.append( - { - "source_node_id": str(node_id), - "parent_source_node_id": parent_id, - "original_name": "Root" if index == 1 else f"Node {node_id}", - "normalized_name": "Root" if index == 1 else f"Node {node_id}", - "node_type": "node", - "role_hint": "node", - "group_key": f"{index:04d}-node-{str(node_id).replace(':', '-')}", - "parent_group_key": "0001-node-1" if parent_id else None, - "sort_key": { - "method": "top_to_bottom_left_to_right_depth_sibling_id", - "value": [0, 0, index - 1, index - 1, str(node_id)], - }, - "visual_order": index, - "source_refs": [f"figma-metadata.part-001.xml#node={node_id}"], - } - ) - (intake / "figma-normalized-tree.yaml").write_text( - yaml.safe_dump( - { - "normalization_complete": True, - "source_metadata_refs": ["figma-metadata.part-001.xml"], - "source_index_ref": "figma-metadata.index.yaml", - "source_inventory_ref": "figma-node-inventory.yaml", - "normalization_rules_applied": [ - "rename: preserve source_node_id and original_name while writing normalized_name", - "grouper: group_key and parent_group_key mirror source containment", - "re-sort: visual_order follows top-to-bottom left-to-right source order", - ], - "rename_rule": "preserve source_node_id and original_name", - "group_rule": "derive group_key without changing source identity", - "sort_rule": "top_to_bottom_left_to_right_depth_sibling_id", - "raw_node_count": len(source_node_ids), - "normalized_node_count": len(source_node_ids), - "node_coverage": "100%", - "selected_node_ids": [str(source_node_ids[0])] if source_node_ids else [], - "gaps": [], - "nodes": nodes, - }, - sort_keys=False, - ), - encoding="utf-8", - ) -def write_prd_intake_fixture(intake: Path): - intake.mkdir(parents=True, exist_ok=True) - source_dir = intake / "source-files" - source_dir.mkdir() - source = source_dir / "feature-prd.md" - source.write_text("# Feature PRD\n\nUsers can save draft content.\n", encoding="utf-8") - - import hashlib - - digest = hashlib.sha256(source.read_bytes()).hexdigest() - - (intake / "source-manifest.yaml").write_text( - "\n".join( - [ - "source_type: markdown", - "source_integrity_complete: true", - "captured_at: '2026-06-23T00:00:00Z'", - "capture_method: local_fixture", - "document_version: fixture-v1", - "extraction_scope: full", - "source_files:", - " - path: source-files/feature-prd.md", - " mime_type: text/markdown", - f" byte_size: {source.stat().st_size}", - f" sha256: {digest}", - " role: original", - "source_details:", - " heading_coverage: full", - " parsed_section_coverage: full", - " linked_asset_refs: []", - "", - ] - ), - encoding="utf-8", - ) - - (intake / "prd-intake.yaml").write_text( - "\n".join( - [ - "prd_intake_complete: true", - "source_refs_complete: true", - "extracted_fact_count: 1", - "acceptance_evidence_complete: true", - "unresolved_ambiguity_marked: true", - "acceptance_gaps: []", - "open_questions:", - " - '[NEEDS CLARIFICATION] Pricing rules are outside this fixture.'", - "blocker_lint_errors: []", - "facts:", - " - id: PI-001", - " category: acceptance", - " statement: Users can save draft content.", - " source_refs: ['source-files/feature-prd.md#L3']", - " evidence_type: observed", - " confidence: high", - " confidence_rationale: Source statement directly describes the accepted behavior.", - " downstream_hint: candidate_acceptance_input", - " acceptance_or_validation_signal: Draft save behavior is explicitly stated.", - "", - ] - ), - encoding="utf-8", - ) - - (intake / "evidence-packet.md").write_text( - "---\n" - "ready_gate: PASS\n" - "blockers: []\n" - "source_ref_count: 1\n" - "extracted_item_count: 1\n" - "generated_at: '2026-06-23T00:00:00Z'\n" - "---\n" - "# PRD Evidence Packet\n", - encoding="utf-8", - ) - - -def write_test_case_intake_fixture(intake: Path): - intake.mkdir(parents=True, exist_ok=True) - source_dir = intake / "source-files" - source_dir.mkdir() - source = source_dir / "test_feature.py" - source.write_text("def test_save_draft():\n assert True\n", encoding="utf-8") - - import hashlib - - digest = hashlib.sha256(source.read_bytes()).hexdigest() - - (intake / "source-manifest.yaml").write_text( - "\n".join( - [ - "source_type: code", - "source_integrity_complete: true", - "captured_at: '2026-06-23T00:00:00Z'", - "capture_method: local_fixture", - "framework_or_format: pytest", - "execution_scope: unit", - "source_files:", - " - path: source-files/test_feature.py", - " mime_type: text/x-python", - f" byte_size: {source.stat().st_size}", - f" sha256: {digest}", - " role: original", - "source_details:", - " test_names:", - " - test_save_draft", - " execution_status: passed", - " skipped_markers: []", - " fixture_refs:", - " - local pytest fixture", - "", - ] - ), - encoding="utf-8", - ) - - (intake / "test-case-intake.yaml").write_text( - "\n".join( - [ - "test_case_intake_complete: true", - "source_refs_complete: true", - "scenario_count: 1", - "assertions_complete: true", - "fixture_evidence_complete: true", - "coverage_gaps_recorded: true", - "assertion_gaps: []", - "fixture_or_test_data_gaps: []", - "coverage_gaps:", - " - Error-state coverage is not present in the fixture.", - "flaky_or_skipped_cases: []", - "blocker_lint_errors: []", - "scenarios:", - " - id: TC-001", - " category: unit", - " scenario: Saving draft content succeeds.", - " source_refs: ['source-files/test_feature.py#L1']", - " evidence_type: observed", - " confidence: high", - " confidence_rationale: Test source directly exercises the scenario.", - " actors: ['registered_user']", - " preconditions: ['draft content exists']", - " actions: ['save draft']", - " expected_outcomes: ['draft is persisted']", - " assertions: ['save path returns success']", - " fixtures_or_test_data: ['local pytest fixture']", - " coverage_signal: happy_path_present_error_path_missing", - "", - ] - ), - encoding="utf-8", - ) - - (intake / "evidence-packet.md").write_text( - "---\n" - "ready_gate: PASS\n" - "blockers: []\n" - "source_ref_count: 1\n" - "extracted_item_count: 1\n" - "generated_at: '2026-06-23T00:00:00Z'\n" - "---\n" - "# Test Case Evidence Packet\n", - encoding="utf-8", - ) - - -def write_image_visual_intake_fixture(intake: Path): - write_visual_intake_fixture(intake, "image", "low", "wireframe.png") - - -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") - - 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", - ) - (ir_dir / "layout-tree.yaml").write_text( - ir_common - + "boxes:\n" - + " - id: box-home\n" - + " source_refs: [figma://node/1]\n", - encoding="utf-8", - ) - (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", - ) - (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", - ) - - (delivery_dir / "index.html").write_text( - '
' - '
' - 'Logo' - '' - 'Saved' - "
" - "
", - encoding="utf-8", - ) - (delivery_dir / "render-replay-report.yaml").write_text( - "\n".join( - [ - "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", - ) - (delivery_dir / "evidence-packet.md").write_text( - "---\n" - "ready_gate: PASS\n" - "blockers: []\n" - "source_ref_count: 1\n" - "extracted_item_count: 6\n" - "generated_at: '2026-07-01T00:00:00Z'\n" - "---\n" - "# Static HTML Delivery Evidence Packet\n", - encoding="utf-8", - ) - - -def test_manifest_loads_with_spec_kit_checkout(): - spec_kit_src = ROOT.parent / "spec-kit" / "src" - if not spec_kit_src.exists(): - pytest.skip("spec-kit checkout not available next to extension") - - env = os.environ.copy() - env["PYTHONPATH"] = str(spec_kit_src) - - code = ( - "from pathlib import Path; " - "from specify_cli.extensions import ExtensionManifest; " - "m=ExtensionManifest(Path('extension.yml')); " - "assert m.id == 'intake'; " - "assert len(m.commands) == 3; " - "assert {c['name'] for c in m.commands} == {'speckit.intake.visual-design', 'speckit.intake.prd', 'speckit.intake.test-cases'}; " - "assert m.hooks" - ) - - result = subprocess.run( - [sys.executable, "-c", code], - cwd=ROOT, - env=env, - text=True, - capture_output=True, - ) - - assert result.returncode == 0, result.stderr - - -def test_config_template_matches_extension_defaults(): - extension = yaml.safe_load((ROOT / "extension.yml").read_text(encoding="utf-8-sig")) - config = yaml.safe_load((ROOT / "config-template.yml").read_text(encoding="utf-8")) - - defaults = extension["defaults"] - assert defaults["artifacts"] == config["artifacts"] - assert defaults["readiness"] == config["readiness"] - assert defaults["capture"] == config["capture"] - - -def test_manifest_declared_files_exist(): - extension = yaml.safe_load((ROOT / "extension.yml").read_text(encoding="utf-8-sig")) - - for command in extension["provides"]["commands"]: - assert (ROOT / command["file"]).exists(), command["file"] - for config in extension["provides"].get("config", []): - assert (ROOT / config["template"]).exists(), config["template"] - - for value in extension["defaults"]["artifacts"].values(): - if not isinstance(value, str): - continue - if value.startswith(("commands/", "templates/", "scripts/python/")): - assert (ROOT / value).exists(), value - - -def test_release_provenance_contract_is_documented_and_generated(): - readme = (ROOT / "README.md").read_text(encoding="utf-8") - workflow = (ROOT / ".github" / "workflows" / "extension-artifact.yml").read_text(encoding="utf-8") - for field in [ - "repository_url", - "release_version", - "source_commit_sha", - "download_url", - "validation_evidence", - ]: - assert field in readme - assert field in workflow - assert "release-provenance.json" in workflow - - -def test_readme_release_url_matches_extension_version(): - extension = yaml.safe_load((ROOT / "extension.yml").read_text(encoding="utf-8-sig")) - version = extension["extension"]["version"] - readme = (ROOT / "README.md").read_text(encoding="utf-8") - assert f"archive/refs/tags/v{version}.zip" in readme - - -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_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 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_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" 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_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 "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(): - 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/normalize_figma_layout.py" in document - assert "templates/schemas/figma-normalized-tree.schema.json" in document - assert "figma-normalized-tree.yaml" in document - assert "require_figma_layout_normalization_for_figma" in document - - assert FIGMA_LAYOUT_NORMALIZE.exists() - assert (ROOT / "templates" / "schemas" / "figma-normalized-tree.schema.json").exists() - - -def test_validator_blocks_missing_directory(): - result = subprocess.run( - [sys.executable, str(VALIDATOR), "missing-dir"], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert result.returncode == 1 - assert "VISUAL_SOURCE_MANIFEST_MISSING" in result.stdout - assert "VISUAL_REQUIREMENTS_MISSING" in result.stdout - assert "VISUAL_EVIDENCE_PACKET_MISSING" in result.stdout - - -def test_prd_validator_blocks_missing_directory(): - result = subprocess.run( - [sys.executable, str(PRD_VALIDATOR), "missing-dir"], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert result.returncode == 1 - assert "PRD_SOURCE_MANIFEST_MISSING" in result.stdout - assert "PRD_INTAKE_MISSING" in result.stdout - assert "PRD_EVIDENCE_PACKET_MISSING" in result.stdout - - -def test_test_case_validator_blocks_missing_directory(): - result = subprocess.run( - [sys.executable, str(TEST_CASE_VALIDATOR), "missing-dir"], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert result.returncode == 1 - assert "TEST_SOURCE_MANIFEST_MISSING" in result.stdout - assert "TEST_CASE_INTAKE_MISSING" in result.stdout - assert "TEST_EVIDENCE_PACKET_MISSING" in result.stdout - - -@pytest.mark.parametrize( - ("source_type", "fidelity", "file_name"), - [ - ("image", "low", "wireframe.png"), - ("pdf", "medium", "design-pack.pdf"), - ("markdown", "high", "design-brief.md"), - ], -) -def test_validator_passes_visual_source_matrix(source_type, fidelity, file_name): - work_dir = ROOT / ".tmp" / f"test-validator-{source_type}-{fidelity}" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "visual-design" - write_visual_intake_fixture(intake, source_type, fidelity, file_name) - - result = subprocess.run( - [sys.executable, str(VALIDATOR), str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert result.returncode == 0, result.stdout + result.stderr - assert "Visual design intake readiness: PASS" in result.stdout - - shutil.rmtree(work_dir) - -def test_visual_validator_allows_remote_source_gap_but_blocks_integrity(): - work_dir = ROOT / ".tmp" / "test-validator-remote-source-gap" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "visual-design" - write_visual_intake_fixture(intake, "figma", "high", "figma-source.txt") - - (intake / "design-source-manifest.yaml").write_text( - "\n".join( - [ - "source_type: figma", - "required_fidelity: high", - "source_integrity_complete: false", - "captured_at: '2026-07-01T00:00:00Z'", - "capture_method: figma_url", - "page_or_frame_count: 1", - "processed_count: 1", - "extraction_scope: selected_node", - "snapshot_status: not_available", - "integrity_gap_reason: Figma source URL was provided without a local export snapshot.", - "retrieval_metadata:", - " retrieved_at: '2026-07-01T00:00:00Z'", - " stable_url: https://www.figma.com/file/example", - " visible_title: Fixture design", - "source_files:", - " - path: figma://file/example", - " mime_type: application/x-figma", - " checksum_status: unavailable", - " role: original", - "source_details:", - " file_url: https://www.figma.com/file/example", - " file_key: example", - " selected_node_ids:", - " - '1'", - "", - ] - ), - encoding="utf-8", - ) - - result = subprocess.run( - [sys.executable, str(VALIDATOR), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "VISUAL_SOURCE_INTEGRITY_INCOMPLETE" in payload["blockers"] - assert "VISUAL_SCHEMA_INVALID" not in payload["blockers"] - assert "VISUAL_SOURCE_FILE_MISSING" not in payload["blockers"] - assert payload["details"]["source_files"][0]["remote_ref"] is True - - shutil.rmtree(work_dir) - - -def test_prd_validator_passes_complete_minimal_intake(): - work_dir = ROOT / ".tmp" / "test-prd-validator-pass" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "prd" - write_prd_intake_fixture(intake) - - result = subprocess.run( - [sys.executable, str(PRD_VALIDATOR), str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert result.returncode == 0, result.stdout + result.stderr - assert "PRD intake readiness: PASS" in result.stdout - - shutil.rmtree(work_dir) - - -def test_prd_validator_allows_remote_source_gap_but_blocks_integrity(): - work_dir = ROOT / ".tmp" / "test-prd-validator-remote-source-gap" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "prd" - write_prd_intake_fixture(intake) - - (intake / "source-manifest.yaml").write_text( - "\n".join( - [ - "source_type: url", - "source_integrity_complete: false", - "captured_at: '2026-07-01T00:00:00Z'", - "capture_method: remote_url", - "document_version: remote-v1", - "extraction_scope: full", - "snapshot_status: not_available", - "integrity_gap_reason: Source URL was accessible but no local snapshot was provided.", - "retrieval_metadata:", - " retrieved_at: '2026-07-01T00:00:00Z'", - " stable_url: https://example.com/prd", - " visible_title: Remote PRD", - " author_or_owner: product", - "source_files:", - " - path: https://example.com/prd", - " mime_type: text/html", - " checksum_status: unavailable", - " role: original", - "", - ] - ), - encoding="utf-8", - ) - - result = subprocess.run( - [sys.executable, str(PRD_VALIDATOR), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "PRD_SOURCE_INTEGRITY_INCOMPLETE" in payload["blockers"] - assert "PRD_READY_WITHOUT_EVIDENCE" in payload["blockers"] - assert "PRD_SCHEMA_INVALID" not in payload["blockers"] - assert "PRD_SOURCE_FILE_MISSING" not in payload["blockers"] - assert payload["details"]["source_files"][0]["remote_ref"] is True - - shutil.rmtree(work_dir) - - -def test_prd_validator_blocks_untraceable_facts(): - work_dir = ROOT / ".tmp" / "test-prd-validator-untraceable" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "prd" - write_prd_intake_fixture(intake) - - text = (intake / "prd-intake.yaml").read_text(encoding="utf-8") - text = text.replace("source_refs_complete: true", "source_refs_complete: false") - text = text.replace("source_refs: ['source-files/feature-prd.md#L3']", "source_refs: []") - (intake / "prd-intake.yaml").write_text(text, encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(PRD_VALIDATOR), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "PRD_FACTS_UNTRACEABLE" in payload["blockers"] - assert "PRD_READY_WITHOUT_EVIDENCE" in payload["blockers"] - assert "PRD_SCHEMA_INVALID" in payload["blockers"] - assert payload["details"]["schema_validation"]["prd_intake"]["valid"] is False - - shutil.rmtree(work_dir) - - -def test_prd_validator_blocks_invalid_confidence_enum(): - work_dir = ROOT / ".tmp" / "test-prd-validator-confidence" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "prd" - write_prd_intake_fixture(intake) - - text = (intake / "prd-intake.yaml").read_text(encoding="utf-8") - text = text.replace(" confidence: high", " confidence: certain") - (intake / "prd-intake.yaml").write_text(text, encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(PRD_VALIDATOR), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "PRD_SCHEMA_INVALID" in payload["blockers"] - assert payload["details"]["schema_validation"]["prd_intake"]["valid"] is False - - shutil.rmtree(work_dir) - - -@pytest.mark.parametrize( - ( - "kind", - "writer", - "validator", - "artifact", - "source_refs_line", - "schema_blocker", - "detail_key", - ), - [ - ( - "prd", - write_prd_intake_fixture, - PRD_VALIDATOR, - "prd-intake.yaml", - " source_refs: ['source-files/feature-prd.md#L3']", - "PRD_SCHEMA_INVALID", - "prd_intake", - ), - ( - "test-case", - write_test_case_intake_fixture, - TEST_CASE_VALIDATOR, - "test-case-intake.yaml", - " source_refs: ['source-files/test_feature.py#L1']", - "TEST_SCHEMA_INVALID", - "test_case_intake", - ), - ( - "visual", - write_image_visual_intake_fixture, - VALIDATOR, - "visual-requirements.yaml", - " source_refs: ['source-files/wireframe.png#full']", - "VISUAL_SCHEMA_INVALID", - "visual_requirements", - ), - ], -) -def test_validators_require_string_source_refs( - kind, - writer, - validator, - artifact, - source_refs_line, - schema_blocker, - detail_key, -): - work_dir = ROOT / ".tmp" / f"test-{kind}-validator-numeric-source-ref" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / kind - writer(intake) - - path = intake / artifact - text = path.read_text(encoding="utf-8") - text = text.replace(source_refs_line, " source_refs: [123]") - path.write_text(text, encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(validator), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert schema_blocker in payload["blockers"] - assert payload["details"]["schema_validation"][detail_key]["valid"] is False - - shutil.rmtree(work_dir) - - -@pytest.mark.parametrize( - ( - "kind", - "writer", - "validator", - "artifact", - "anchor_line", - "schema_blocker", - "detail_key", - ), - [ - ( - "prd", - write_prd_intake_fixture, - PRD_VALIDATOR, - "prd-intake.yaml", - " acceptance_or_validation_signal: Draft save behavior is explicitly stated.", - "PRD_SCHEMA_INVALID", - "prd_intake", - ), - ( - "test-case", - write_test_case_intake_fixture, - TEST_CASE_VALIDATOR, - "test-case-intake.yaml", - " coverage_signal: happy_path_present_error_path_missing", - "TEST_SCHEMA_INVALID", - "test_case_intake", - ), - ( - "visual", - write_image_visual_intake_fixture, - VALIDATOR, - "visual-requirements.yaml", - " fidelity_level: low", - "VISUAL_SCHEMA_INVALID", - "visual_requirements", - ), - ], -) -def test_validators_reject_unknown_blocker_codes( - kind, - writer, - validator, - artifact, - anchor_line, - schema_blocker, - detail_key, -): - work_dir = ROOT / ".tmp" / f"test-{kind}-validator-unknown-blocker" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / kind - writer(intake) - - path = intake / artifact - text = path.read_text(encoding="utf-8") - text = text.replace(anchor_line, f"{anchor_line}\n blockers: [NOT_A_BLOCKER]") - path.write_text(text, encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(validator), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert schema_blocker in payload["blockers"] - assert payload["details"]["schema_validation"][detail_key]["valid"] is False - - shutil.rmtree(work_dir) - - -@pytest.mark.parametrize( - ( - "kind", - "writer", - "validator", - "artifact", - "rationale_line", - "schema_blocker", - ), - [ - ( - "prd", - write_prd_intake_fixture, - PRD_VALIDATOR, - "prd-intake.yaml", - " confidence_rationale: Source statement directly describes the accepted behavior.\n", - "PRD_SCHEMA_INVALID", - ), - ( - "test-case", - write_test_case_intake_fixture, - TEST_CASE_VALIDATOR, - "test-case-intake.yaml", - " confidence_rationale: Test source directly exercises the scenario.\n", - "TEST_SCHEMA_INVALID", - ), - ( - "visual", - write_image_visual_intake_fixture, - VALIDATOR, - "visual-requirements.yaml", - " confidence_rationale: Source artifact directly shows the primary hierarchy.\n", - "VISUAL_SCHEMA_INVALID", - ), - ], -) -def test_validators_require_confidence_rationale( - kind, - writer, - validator, - artifact, - rationale_line, - schema_blocker, -): - work_dir = ROOT / ".tmp" / f"test-{kind}-validator-missing-confidence-rationale" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / kind - writer(intake) - - path = intake / artifact - text = path.read_text(encoding="utf-8") - text = text.replace(rationale_line, "") - path.write_text(text, encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(validator), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert schema_blocker in payload["blockers"] - - shutil.rmtree(work_dir) - - -@pytest.mark.parametrize( - ( - "kind", - "writer", - "validator", - "artifact", - "count_line", - "blocker", - "detail_key", - "match_key", - ), - [ - ( - "prd", - write_prd_intake_fixture, - PRD_VALIDATOR, - "prd-intake.yaml", - "extracted_fact_count: 1", - "PRD_INTAKE_MISSING", - "prd_intake", - "count_matches_facts", - ), - ( - "test-case", - write_test_case_intake_fixture, - TEST_CASE_VALIDATOR, - "test-case-intake.yaml", - "scenario_count: 1", - "TEST_CASE_INTAKE_MISSING", - "test_case_intake", - "count_matches_scenarios", - ), - ( - "visual", - write_image_visual_intake_fixture, - VALIDATOR, - "visual-requirements.yaml", - "visual_requirements_count: 1", - "VISUAL_REQUIREMENTS_MISSING", - "visual_requirements", - "count_matches_requirements", - ), - ], -) -def test_validators_block_declared_count_mismatch( - kind, - writer, - validator, - artifact, - count_line, - blocker, - detail_key, - match_key, -): - work_dir = ROOT / ".tmp" / f"test-{kind}-validator-count-mismatch" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / kind - writer(intake) - - path = intake / artifact - text = path.read_text(encoding="utf-8") - text = text.replace(count_line, count_line.replace(": 1", ": 2")) - path.write_text(text, encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(validator), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert blocker in payload["blockers"] - assert payload["details"][detail_key][match_key] is False - - shutil.rmtree(work_dir) - - -def test_prd_validator_blocks_incomplete_evidence_front_matter(): - work_dir = ROOT / ".tmp" / "test-prd-validator-front-matter" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "prd" - write_prd_intake_fixture(intake) - - (intake / "evidence-packet.md").write_text( - "---\nready_gate: PASS\n---\n# PRD Evidence Packet\n", - encoding="utf-8", - ) - - result = subprocess.run( - [sys.executable, str(PRD_VALIDATOR), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "PRD_READY_WITHOUT_EVIDENCE" in payload["blockers"] - - shutil.rmtree(work_dir) - - -@pytest.mark.parametrize( - ("kind", "writer", "validator", "packet_name", "ready_blocker"), - [ - ("prd", write_prd_intake_fixture, PRD_VALIDATOR, "evidence-packet.md", "PRD_READY_WITHOUT_EVIDENCE"), - ( - "test-case", - write_test_case_intake_fixture, - TEST_CASE_VALIDATOR, - "evidence-packet.md", - "TEST_READY_WITHOUT_EVIDENCE", - ), - ( - "visual", - write_image_visual_intake_fixture, - VALIDATOR, - "visual-evidence-packet.md", - "VISUAL_READY_WITHOUT_EVIDENCE", - ), - ], -) -def test_validators_block_blocked_evidence_packet( - kind, - writer, - validator, - packet_name, - ready_blocker, -): - work_dir = ROOT / ".tmp" / f"test-{kind}-validator-blocked-packet" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / kind - writer(intake) - - packet = intake / packet_name - text = packet.read_text(encoding="utf-8") - text = text.replace("ready_gate: PASS", "ready_gate: BLOCKED") - packet.write_text(text, encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(validator), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert ready_blocker in payload["blockers"] - - shutil.rmtree(work_dir) - - -def test_test_case_validator_passes_complete_minimal_intake(): - work_dir = ROOT / ".tmp" / "test-case-validator-pass" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "test-cases" - write_test_case_intake_fixture(intake) - - result = subprocess.run( - [sys.executable, str(TEST_CASE_VALIDATOR), str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert result.returncode == 0, result.stdout + result.stderr - assert "Test-case intake readiness: PASS" in result.stdout - - shutil.rmtree(work_dir) - - -def test_test_case_validator_allows_remote_source_gap_but_blocks_integrity(): - work_dir = ROOT / ".tmp" / "test-case-validator-remote-source-gap" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "test-cases" - write_test_case_intake_fixture(intake) - - (intake / "source-manifest.yaml").write_text( - "\n".join( - [ - "source_type: issue", - "source_integrity_complete: false", - "captured_at: '2026-07-01T00:00:00Z'", - "capture_method: remote_issue", - "framework_or_format: issue", - "execution_scope: regression", - "snapshot_status: not_available", - "integrity_gap_reason: Issue was referenced without a local exported snapshot.", - "retrieval_metadata:", - " retrieved_at: '2026-07-01T00:00:00Z'", - " stable_url: https://example.com/issues/1", - " visible_title: Regression case", - "source_files:", - " - path: https://example.com/issues/1", - " mime_type: text/html", - " checksum_status: unavailable", - " role: original", - "", - ] - ), - encoding="utf-8", - ) - - result = subprocess.run( - [sys.executable, str(TEST_CASE_VALIDATOR), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "TEST_SOURCE_INTEGRITY_INCOMPLETE" in payload["blockers"] - assert "TEST_READY_WITHOUT_EVIDENCE" in payload["blockers"] - assert "TEST_SCHEMA_INVALID" not in payload["blockers"] - assert "TEST_SOURCE_FILE_MISSING" not in payload["blockers"] - assert payload["details"]["source_files"][0]["remote_ref"] is True - - shutil.rmtree(work_dir) - - -def test_test_case_validator_blocks_missing_assertions_and_coverage(): - work_dir = ROOT / ".tmp" / "test-case-validator-missing-assertions" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "test-cases" - write_test_case_intake_fixture(intake) - - text = (intake / "test-case-intake.yaml").read_text(encoding="utf-8") - text = text.replace("assertions_complete: true", "assertions_complete: false") - text = text.replace("coverage_gaps_recorded: true", "coverage_gaps_recorded: false") - text = text.replace(" assertions: ['save path returns success']", " assertions: []") - text = text.replace("coverage_gaps:\n - Error-state coverage is not present in the fixture.\n", "coverage_gaps: []\n") - text = text.replace(" coverage_signal: happy_path_present_error_path_missing", " coverage_signal: ''") - (intake / "test-case-intake.yaml").write_text(text, encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(TEST_CASE_VALIDATOR), str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert result.returncode == 1 - assert "TEST_ASSERTIONS_MISSING" in result.stdout - assert "TEST_COVERAGE_GAPS_MISSING" in result.stdout - assert "TEST_READY_WITHOUT_EVIDENCE" in result.stdout - - shutil.rmtree(work_dir) - - -def test_test_case_validator_reports_schema_errors_in_json(): - work_dir = ROOT / ".tmp" / "test-case-validator-schema-error" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "test-cases" - write_test_case_intake_fixture(intake) - - text = (intake / "test-case-intake.yaml").read_text(encoding="utf-8") - text = text.replace(" category: unit", " category: smoke") - (intake / "test-case-intake.yaml").write_text(text, encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(TEST_CASE_VALIDATOR), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "TEST_SCHEMA_INVALID" in payload["blockers"] - assert "TEST_READY_WITHOUT_EVIDENCE" in payload["blockers"] - assert payload["details"]["schema_validation"]["test_case_intake"]["valid"] is False - - shutil.rmtree(work_dir) - - -def test_visual_validator_blocks_missing_source_type_details(): - work_dir = ROOT / ".tmp" / "test-validator-missing-source-details" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "visual-design" - write_visual_intake_fixture(intake, "image", "low", "wireframe.png") - - text = (intake / "design-source-manifest.yaml").read_text(encoding="utf-8") - text = text.split("source_details:", 1)[0] - (intake / "design-source-manifest.yaml").write_text(text, encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(VALIDATOR), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "VISUAL_SCHEMA_INVALID" in payload["blockers"] - assert payload["details"]["schema_validation"]["visual_source_manifest"]["valid"] is False - - shutil.rmtree(work_dir) - - -def test_validator_blocks_unsupported_visual_source_type(): - work_dir = ROOT / ".tmp" / "test-validator-unsupported-source" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "visual-design" - write_visual_intake_fixture(intake, "sketch", "high", "design.sketch") - - result = subprocess.run( - [sys.executable, str(VALIDATOR), str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert result.returncode == 1 - assert "VISUAL_SOURCE_TYPE_UNSUPPORTED" in result.stdout - assert "VISUAL_SCHEMA_INVALID" in result.stdout - assert "VISUAL_READY_WITHOUT_EVIDENCE" in result.stdout - - shutil.rmtree(work_dir) - - -def test_visual_validator_blocks_unbounded_inferred_claim(): - work_dir = ROOT / ".tmp" / "test-validator-unbounded-inference" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "visual-design" - write_visual_intake_fixture(intake, "image", "medium", "wireframe.png") - - text = (intake / "visual-requirements.yaml").read_text(encoding="utf-8") - text = text.replace(" evidence_type: observed", " evidence_type: inferred") - (intake / "visual-requirements.yaml").write_text(text, encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(VALIDATOR), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "VISUAL_SCHEMA_INVALID" in payload["blockers"] - assert "VISUAL_INFERENCE_CONTRACT_INVALID" in payload["blockers"] - assert payload["details"]["visual_requirements"]["evidence_type_counts"]["inferred"] == 1 - - shutil.rmtree(work_dir) - - -def test_visual_validator_blocks_candidate_promoted_to_accepted_claim(): - work_dir = ROOT / ".tmp" / "test-validator-candidate-promoted" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "visual-design" - write_visual_intake_fixture(intake, "image", "medium", "wireframe.png") - - text = (intake / "visual-requirements.yaml").read_text(encoding="utf-8") - text = text.replace(" evidence_type: observed", " evidence_type: candidate") - text = text.replace(" confidence: high", " confidence: medium") - text = text.replace( - f" fidelity_level: medium", - "\n".join( - [ - " inference_rule: visual_button_shape + short_text_label", - " confidence_method: rule_score_v1", - " score_breakdown:", - " - signal: visual_button_shape", - " weight: 0.25", - " - signal: short_text_label", - " weight: 0.2", - " downstream_use: accepted_claim", - " missing_evidence:", - " - component_instance", - " blocking_conditions:", - " - promote only after component or prototype evidence exists", - " fidelity_level: medium", - ] - ), - ) - (intake / "visual-requirements.yaml").write_text(text, encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(VALIDATOR), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "VISUAL_SCHEMA_INVALID" in payload["blockers"] - assert "VISUAL_INFERENCE_CONTRACT_INVALID" in payload["blockers"] - - shutil.rmtree(work_dir) - - -def test_visual_validator_blocks_unsupported_claim_even_when_packet_says_pass(): - work_dir = ROOT / ".tmp" / "test-validator-unsupported-claim" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "visual-design" - write_visual_intake_fixture(intake, "figma", "high", "figma-source.txt") - - text = (intake / "visual-requirements.yaml").read_text(encoding="utf-8") - text = text.replace(" evidence_type: observed", " evidence_type: unsupported") - text = text.replace( - " engineering_action: Implement matching hierarchy", - "\n".join( - [ - " blocker_code: FIGMA_UNSUPPORTED_STATE_INFERENCE", - " reason: No variant, prototype state, naming convention, or source note defines loading behavior.", - " downstream_use: blocked", - " missing_evidence:", - " - variant_state", - " - prototype_state", - " blockers:", - " - FIGMA_UNSUPPORTED_STATE_INFERENCE", - " engineering_action: Keep the loading state unresolved", - ] - ), - ) - (intake / "visual-requirements.yaml").write_text(text, encoding="utf-8") - - metadata = intake / "figma-metadata.part-001.xml" - metadata.write_text("\n", encoding="utf-8") - - import hashlib - - digest = hashlib.sha256(metadata.read_bytes()).hexdigest() - (intake / "figma-metadata.index.yaml").write_text( - "\n".join( - [ - "file_url: https://www.figma.com/file/example", - "file_key: example", - "page_id: page-1", - "selected_node_ids: ['1']", - "captured_at: '2026-06-22T00:00:00Z'", - "mcp_tool: get_metadata", - "design_version_or_timestamp: '2026-06-22T00:00:00Z'", - "selected_subtree_complete: true", - "raw_metadata_complete: true", - "expected_root_node_ids: ['1']", - "captured_root_node_ids: ['1']", - "missing_root_node_ids: []", - "gap_count: 0", - "gaps: []", - "shards:", - " - path: figma-metadata.part-001.xml", - f" byte_size: {metadata.stat().st_size}", - f" sha256: {digest}", - " root_node_ids: ['1']", - " node_count: 1", - " truncated: false", - "", - ] - ), - encoding="utf-8", - ) - (intake / "figma-node-inventory.yaml").write_text( - "\n".join( - [ - "raw_node_count: 1", - "inventory_node_count: 1", - "excluded_node_count: 0", - "missing_node_count: 0", - "duplicate_node_count: 0", - "truncated_raw_evidence: false", - "node_inventory_coverage: 100%", - "parity_passed: true", - "", - ] - ), - encoding="utf-8", - ) - write_figma_normalized_tree_fixture(intake, ["1"]) - - result = subprocess.run( - [sys.executable, str(VALIDATOR), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "VISUAL_BLOCKER_LINT_ERRORS" in payload["blockers"] - assert "VISUAL_READY_WITHOUT_EVIDENCE" in payload["blockers"] - assert payload["details"]["visual_requirements"]["evidence_type_counts"]["unsupported"] == 1 - - shutil.rmtree(work_dir) - - -def test_visual_validator_blocks_helper_artifacts_as_source_refs(): - work_dir = ROOT / ".tmp" / "test-visual-helper-artifact-source-refs" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "visual-design" - write_visual_intake_fixture(intake, "image", "medium", "wireframe.png") - - requirements = yaml.safe_load((intake / "visual-requirements.yaml").read_text(encoding="utf-8")) - requirements["requirements"][0]["source_refs"] = [ - "delivery/index.html#home-page", - "source-files/wireframe.png#full", - ] - (intake / "visual-requirements.yaml").write_text(yaml.safe_dump(requirements), encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(VALIDATOR), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - 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"] == ["delivery/index.html#home-page"] - - shutil.rmtree(work_dir) - - -def test_figma_metadata_capture_stages_shards_and_passes_validator(): - work_dir = ROOT / ".tmp" / "test-figma-metadata-capture-pass" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "visual-design" - write_visual_intake_fixture(intake, "figma", "high", "figma-source.txt") - raw_dir = work_dir / "raw" - raw_dir.mkdir(parents=True) - (raw_dir / "root-1.xml").write_text( - '\n', - encoding="utf-8", - ) - (raw_dir / "root-2.xml").write_text( - '\n', - encoding="utf-8", - ) - - capture = subprocess.run( - [ - sys.executable, - str(FIGMA_METADATA_CAPTURE), - str(intake), - "--metadata-source", - str(raw_dir / "root-1.xml"), - "--metadata-source", - str(raw_dir / "root-2.xml"), - "--file-url", - "https://www.figma.com/design/example/Foo", - "--file-key", - "example", - "--page-id", - "page-1", - "--node-id", - "1", - "--node-id", - "2", - "--captured-at", - "2026-07-02T00:00:00Z", - ], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert capture.returncode == 0, capture.stdout + capture.stderr - assert (intake / "figma-metadata.part-001.xml").exists() - assert (intake / "figma-metadata.part-002.xml").exists() - index = yaml.safe_load((intake / "figma-metadata.index.yaml").read_text(encoding="utf-8")) - inventory = yaml.safe_load((intake / "figma-node-inventory.yaml").read_text(encoding="utf-8")) - assert index["raw_metadata_complete"] is True - assert index["selected_subtree_complete"] is True - assert index["captured_root_node_ids"] == ["1", "2"] - assert index["shards"][0]["sha256"] - assert inventory["raw_node_count"] == 3 - assert inventory["parity_passed"] is True - - normalize = subprocess.run( - [sys.executable, str(FIGMA_LAYOUT_NORMALIZE), str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert normalize.returncode == 0, normalize.stdout + normalize.stderr - normalized = yaml.safe_load((intake / "figma-normalized-tree.yaml").read_text(encoding="utf-8")) - assert normalized["normalization_complete"] is True - assert normalized["normalized_node_count"] == 3 - - result = subprocess.run( - [sys.executable, str(VALIDATOR), str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert result.returncode == 0, result.stdout + result.stderr - assert "Visual design intake readiness: PASS" in result.stdout - - shutil.rmtree(work_dir) - - -def test_figma_metadata_capture_blocks_truncated_shard(): - work_dir = ROOT / ".tmp" / "test-figma-metadata-capture-truncated" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "visual-design" - intake.mkdir(parents=True) - raw = work_dir / "truncated.xml" - raw.write_text('\n', encoding="utf-8") - - capture = subprocess.run( - [ - sys.executable, - str(FIGMA_METADATA_CAPTURE), - str(intake), - "--metadata-source", - str(raw), - "--file-url", - "https://www.figma.com/design/example/Foo", - "--file-key", - "example", - "--page-id", - "page-1", - "--node-id", - "1", - "--captured-at", - "2026-07-02T00:00:00Z", - "--json", - ], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(capture.stdout) - index = yaml.safe_load((intake / "figma-metadata.index.yaml").read_text(encoding="utf-8")) - assert capture.returncode == 1 - assert payload["status"] == "BLOCKED" - assert "FIGMA_RAW_METADATA_TRUNCATED" in payload["blockers"] - assert index["raw_metadata_complete"] is False - assert index["shards"][0]["truncated"] is True - - shutil.rmtree(work_dir) - - -def test_validator_passes_complete_minimal_figma_intake(): - work_dir = ROOT / ".tmp" / "test-validator-pass" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "visual-design" - write_visual_intake_fixture(intake, "figma", "high", "figma-source.txt") - - metadata = intake / "figma-metadata.part-001.xml" - metadata.write_text("\n", encoding="utf-8") - - import hashlib - - digest = hashlib.sha256(metadata.read_bytes()).hexdigest() - - (intake / "figma-metadata.index.yaml").write_text( - "\n".join( - [ - "file_url: https://www.figma.com/file/example", - "file_key: example", - "page_id: page-1", - "selected_node_ids:", - " - '1'", - "captured_at: '2026-06-22T00:00:00Z'", - "mcp_tool: get_metadata", - "design_version_or_timestamp: '2026-06-22T00:00:00Z'", - "selected_subtree_complete: true", - "raw_metadata_complete: true", - "expected_root_node_ids:", - " - '1'", - "captured_root_node_ids:", - " - '1'", - "missing_root_node_ids: []", - "gap_count: 0", - "gaps: []", - "shards:", - " - path: figma-metadata.part-001.xml", - f" byte_size: {metadata.stat().st_size}", - f" sha256: {digest}", - " root_node_ids:", - " - '1'", - " node_count: 1", - " truncated: false", - "", - ] - ), - encoding="utf-8", - ) - - (intake / "figma-node-inventory.yaml").write_text( - "\n".join( - [ - "raw_node_count: 1", - "inventory_node_count: 1", - "excluded_node_count: 0", - "missing_node_count: 0", - "duplicate_node_count: 0", - "truncated_raw_evidence: false", - "node_inventory_coverage: 100%", - "parity_passed: true", - "", - ] - ), - encoding="utf-8", - ) - write_figma_normalized_tree_fixture(intake, ["1"]) - result = subprocess.run( - [sys.executable, str(VALIDATOR), str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert result.returncode == 0, result.stdout + result.stderr - assert "Visual design intake readiness: PASS" in result.stdout - - shutil.rmtree(work_dir) - - -def test_visual_validator_blocks_missing_figma_normalized_tree(): - work_dir = ROOT / ".tmp" / "test-validator-missing-figma-normalized-tree" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "visual-design" - write_visual_intake_fixture(intake, "figma", "high", "figma-source.txt") - write_figma_metadata_fixture(intake) - (intake / "figma-normalized-tree.yaml").unlink() - - result = subprocess.run( - [sys.executable, str(VALIDATOR), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "FIGMA_NORMALIZED_TREE_MISSING" in payload["blockers"] - assert payload["details"]["figma_normalized_tree"]["missing"] is True - - shutil.rmtree(work_dir) - - -def test_visual_validator_blocks_invalid_figma_normalized_tree(): - work_dir = ROOT / ".tmp" / "test-validator-invalid-figma-normalized-tree" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "visual-design" - write_visual_intake_fixture(intake, "figma", "high", "figma-source.txt") - write_figma_metadata_fixture(intake) - - normalized = yaml.safe_load((intake / "figma-normalized-tree.yaml").read_text(encoding="utf-8")) - normalized["node_coverage"] = "incomplete" - normalized["nodes"][1]["visual_order"] = normalized["nodes"][0]["visual_order"] - (intake / "figma-normalized-tree.yaml").write_text(yaml.safe_dump(normalized), encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(VALIDATOR), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "VISUAL_SCHEMA_INVALID" in payload["blockers"] - assert "FIGMA_NORMALIZED_TREE_INCOMPLETE" in payload["blockers"] - assert payload["details"]["schema_validation"]["figma_normalized_tree"]["valid"] is False - assert payload["details"]["figma_layout_normalization"]["duplicate_visual_orders"] == [1] - - shutil.rmtree(work_dir) - - -def test_visual_validator_blocks_downstream_fields_in_figma_normalized_tree(): - work_dir = ROOT / ".tmp" / "test-validator-downstream-field-figma-normalized-tree" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "visual-design" - write_visual_intake_fixture(intake, "figma", "high", "figma-source.txt") - write_figma_metadata_fixture(intake) - - normalized = yaml.safe_load((intake / "figma-normalized-tree.yaml").read_text(encoding="utf-8")) - 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") - - result = subprocess.run( - [sys.executable, str(VALIDATOR), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "VISUAL_SCHEMA_INVALID" in payload["blockers"] - assert payload["details"]["schema_validation"]["figma_normalized_tree"]["valid"] is False - - shutil.rmtree(work_dir) - - -def test_figma_layout_normalization_blocks_incomplete_raw_metadata(): - work_dir = ROOT / ".tmp" / "test-normalize-blocks-incomplete-raw-metadata" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "visual-design" - write_visual_intake_fixture(intake, "figma", "high", "figma-source.txt") - write_figma_metadata_fixture(intake) - - index = yaml.safe_load((intake / "figma-metadata.index.yaml").read_text(encoding="utf-8")) - index["raw_metadata_complete"] = False - index["shards"][0]["truncated"] = True - (intake / "figma-metadata.index.yaml").write_text(yaml.safe_dump(index), encoding="utf-8") - - result = subprocess.run( - [sys.executable, str(FIGMA_LAYOUT_NORMALIZE), "--json", str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - normalized = yaml.safe_load((intake / "figma-normalized-tree.yaml").read_text(encoding="utf-8")) - assert result.returncode == 1 - assert payload["status"] == "BLOCKED" - assert "FIGMA_NORMALIZED_TREE_INCOMPLETE" in payload["blockers"] - assert normalized["normalization_complete"] is False - assert normalized["node_coverage"] == "incomplete" - assert any(gap["code"] == "FIGMA_RAW_METADATA_TRUNCATED" for gap in normalized["gaps"]) - - shutil.rmtree(work_dir) - - -def test_validator_blocks_legacy_figma_only_without_manifest(): - work_dir = ROOT / ".tmp" / "test-validator-legacy-figma" - if work_dir.exists(): - shutil.rmtree(work_dir) - intake = work_dir / "visual-design" - intake.mkdir(parents=True) - - metadata = intake / "figma-metadata.part-001.xml" - metadata.write_text("\n", encoding="utf-8") - - import hashlib - - digest = hashlib.sha256(metadata.read_bytes()).hexdigest() - - (intake / "figma-metadata.index.yaml").write_text( - "\n".join( - [ - "file_url: https://www.figma.com/file/example", - "file_key: example", - "page_id: page-1", - "selected_node_ids:", - " - '1'", - "captured_at: '2026-06-22T00:00:00Z'", - "mcp_tool: get_metadata", - "design_version_or_timestamp: '2026-06-22T00:00:00Z'", - "selected_subtree_complete: true", - "raw_metadata_complete: true", - "expected_root_node_ids:", - " - '1'", - "captured_root_node_ids:", - " - '1'", - "missing_root_node_ids: []", - "gap_count: 0", - "gaps: []", - "shards:", - " - path: figma-metadata.part-001.xml", - f" byte_size: {metadata.stat().st_size}", - f" sha256: {digest}", - " root_node_ids:", - " - '1'", - " node_count: 1", - " truncated: false", - "", - ] - ), - encoding="utf-8", - ) - - (intake / "figma-node-inventory.yaml").write_text( - "\n".join( - [ - "raw_node_count: 1", - "inventory_node_count: 1", - "excluded_node_count: 0", - "missing_node_count: 0", - "duplicate_node_count: 0", - "truncated_raw_evidence: false", - "node_inventory_coverage: 100%", - "parity_passed: true", - "", - ] - ), - encoding="utf-8", - ) - - (intake / "figma-evidence-packet.md").write_text( - "# Figma Evidence Packet\n\n- ready_gate: PASS\n", - encoding="utf-8", - ) - - result = subprocess.run( - [sys.executable, str(VALIDATOR), str(intake)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert result.returncode == 1 - assert "VISUAL_SOURCE_MANIFEST_MISSING" in result.stdout - assert "FIGMA_READY_WITHOUT_COMPLETENESS_PROOF" in result.stdout - - shutil.rmtree(work_dir) - - -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) - delivery_dir = work_dir / "visual-design" / "delivery" - write_static_html_delivery_fixture(delivery_dir) - - result = subprocess.run( - [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 "Static HTML delivery readiness: PASS" in result.stdout - - shutil.rmtree(work_dir) - - -def test_static_html_delivery_validator_blocks_missing_directory(): - result = subprocess.run( - [sys.executable, str(STATIC_HTML_DELIVERY_VALIDATOR), "missing-dir"], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert result.returncode == 1 - assert "STATIC_HTML_REQUIRED_ARTIFACT_MISSING" in result.stdout - - -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) - 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" - "blockers: [VISUAL_REQUIREMENTS_MISSING]\n" - "source_ref_count: 1\n" - "extracted_item_count: 0\n" - "generated_at: '2026-06-23T00:00:00Z'\n" - "---\n", - encoding="utf-8", - ) - - result = subprocess.run( - [sys.executable, str(STATIC_HTML_DELIVERY_VALIDATOR), "--json", str(delivery_dir)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "STATIC_HTML_SOURCE_INTAKE_BLOCKED" in payload["blockers"] - - shutil.rmtree(work_dir) - - -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) - 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(STATIC_HTML_DELIVERY_VALIDATOR), "--json", str(delivery_dir)], - cwd=ROOT, - text=True, - capture_output=True, - ) - - payload = json.loads(result.stdout) - assert result.returncode == 1 - assert "STATIC_HTML_SCHEMA_INVALID" in payload["blockers"] - assert payload["details"]["schema_validation"]["static_html_delivery"]["valid"] is False - - shutil.rmtree(work_dir) - - -@pytest.mark.parametrize( - ("edit_kind", "expected_blocker"), - [ - ("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_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) - 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", - ) - 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": - report["viewports"][0]["screenshot_refs"] = ["screenshots/missing.png"] - elif edit_kind == "visual_diff": - report["visual_diffs"][0]["status"] = "blocked" - report["visual_diffs"][0]["blockers"] = ["STATIC_HTML_VISUAL_DIFF_BLOCKED"] - elif edit_kind == "evidence_packet": - (delivery_dir / "evidence-packet.md").write_text( - "---\n" - "ready_gate: BLOCKED\n" - "blockers: [STATIC_HTML_READY_WITHOUT_EVIDENCE]\n" - "source_ref_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(STATIC_HTML_DELIVERY_VALIDATOR), "--json", str(delivery_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) diff --git a/extensions/repository-governance/.extensionignore b/extensions/repository-governance/.extensionignore deleted file mode 100644 index 73a6e73711..0000000000 --- a/extensions/repository-governance/.extensionignore +++ /dev/null @@ -1,15 +0,0 @@ -.git/ -.github/ -docs/ -AGENTS.md -pyproject.toml -uv.lock -__pycache__/ -*.pyc -.pytest_cache/ -tests/ -tools/ -.DS_Store -.venv/ -dist/ -*.zip diff --git a/extensions/repository-governance/.gitignore b/extensions/repository-governance/.gitignore deleted file mode 100644 index 717ad111dc..0000000000 --- a/extensions/repository-governance/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -__pycache__/ -*.py[cod] -.pytest_cache/ -.mypy_cache/ -.ruff_cache/ -.coverage -htmlcov/ -.DS_Store -Thumbs.db -.venv/ -venv/ -dist/ -build/ -*.egg-info/ -*.zip -*.log -.env -.env.* -!.env.example -.tmp/ -tmp/ -temp/ -.worktrees/ diff --git a/extensions/repository-governance/CHANGELOG.md b/extensions/repository-governance/CHANGELOG.md deleted file mode 100644 index f085dd9146..0000000000 --- a/extensions/repository-governance/CHANGELOG.md +++ /dev/null @@ -1,124 +0,0 @@ -# Changelog - -## Unreleased - -### Changed - -- Replace abstract vertical SSOT domains with fixed directory-tree SSOT entries: `agent-runtime/`, `engineering-runtime/`, `poc/`, `source-code/`, `test-code/`, and `other-tools/`. -- Treat matched directories and fixed directory-tree files as SSOT index source refs, including agent runtime paths, CI/CD and Docker/runtime files, source paths, test paths, and auxiliary tool configs. -- Rename the generated fallback and agent harness wording to Directory Tree and `agent-runtime/` wording. -- Add `poc/` as the pre-iteration exploration SSOT for technical spikes, architecture drafts, UC designs, prototypes, research notes, validation reports, and validation conclusions. -- Require POC promotion to cite a formal design or task before `source-code/` edits, and require validation record source refs or `NEEDS_CLARIFICATION:poc-validation-record`. - -## [3.0.2] - 2026-07-03 - -### Changed - -- Restructure generated projections into Copilot-like repository-wide, SSOT routing, path/task scope, and agent harness instruction layers while preserving the single active-target write surface. -- Merge Toolchain SSOT coverage into Engineering SSOT to remove overlapping vertical ownership. -- Remove the cache/managed-section model; generation now overwrites the active agent platform target from the current repository scan. -- Make generated projections subordinate to vertical SSOT documents and source-backed repository facts on substantive conflicts. -- Preserve `SKILL.md` trigger metadata in the scenario capability index. -- Restrict custom `context_file` projection targets to safe agent/rules/instructions context paths. -- Tighten generated write-boundary instructions around cache-free active-target generation, `CONTEXT_FILES` legacy cleanup, and protected-write validation gates. -- Rename the public command and packaged script from `refresh` to `generate`. -- Separate generator operations from generated target-file guidance, require clarification for missing vertical SSOT, add explicit Zed target mapping, and bound repository-local skill projection. -- Collapse generated target content into SSOT index entries and remove expanded repository evidence, repository area, skill capability, and development command lists from active targets. -- Keep SSOT index source refs complete while narrowing Agent Harness SSOT refs to explicit governance entrypoints and treating ordinary `SKILL.md` files as evidence only. -- Bound route evidence scanning to cached project files and text candidates, narrow Directory Structure fallback wording, and treat extension metadata as Engineering SSOT only for this extension source repository. - -## [3.0.0] - 2026-06-25 - -### Changed - -- Reposition generated output as project-governance projections for Spec Kit supported agent platform targets while keeping the `repository-governance` extension identity and runtime filenames. -- Replace generated `SPECKIT GOVERNANCE` managed markers with `PROJECT GOVERNANCE` markers and treat legacy Speckit markers as migration cleanup targets. -- Normalize legacy initialization cache wording during refresh so old cache sections cannot reintroduce `SPECKIT GOVERNANCE` markers or weaker protected-write rules. -- Resolve missing or unknown integration metadata through the explicit `generic` entry in `CONTEXT_FILES`. - -## [2.0.6] - 2026-06-24 - -### Fixed - -- Keep spec-kit integration PR validation aligned with current spec-kit test node names. - -## [2.0.5] - 2026-06-24 - -### Fixed - -- Update the release smoke install command to use Spec Kit's current `--integration codex` option. - -## [2.0.4] - 2026-06-24 - -### Changed - -- Lock local GitHub Actions verification commands with `uv run --locked` and keep the extension install smoke test on the GitHub runner. -- Broaden repository fact detection for extension assets, Spec Kit metadata, project policy files, feature specs, build/runtime config, and Python/uv test commands. -- Replace the local `zip` shell command with a cross-platform Python package builder shared by docs and the release workflow. - -### Fixed - -- Keep spec-kit integration PR sync aligned with the runtime extension package boundary and current spec-kit test node ids. - -## [2.0.2] - 2026-06-03 - -### Fixed - -- Fix release workflow smoke installation after `v2.0.1` failed before artifact publication when Spec Kit already installs the bundled repository-governance extension and projects Codex commands as `.agents/skills`. -- Allow tag release artifact publication to complete when the optional spec-kit integration PR token is unavailable. - -## [2.0.1] - 2026-06-03 - -### Added - -- Add GitHub Actions contract CI for repository governance validation. -- Add a release artifact workflow that builds a deterministic runtime extension zip, smoke-installs the extension, and can open the bundled `bigsmartben/spec-kit` integration PR. -- Document the local extension package build command for `dist/repository-governance.zip`. -- Add repository extension governance documentation for command, template, script, path-safety, package-boundary, and verification rules. -- Add a Spec Kit Agent Adapter layer and scenario capability index for repository-local skills and MCP-backed external tool evidence, with MCP config candidates treated as evidence only and runtime enumeration required before use. - -## [2.0.0] - 2026-05-25 - -### Changed - -- Reposition the extension as a Repository Governance Framework generator. -- Rename runtime command, template, and helper files to repository-governance while preserving the Spec Kit command/template/script separation. -- Add a top-level vertical SSOT registry and missing-SSOT handling rules without embedding architecture methodology in the top-level governance layer. -- Capture repository facts as vertical SSOT evidence for Architecture, Engineering, Code Style, Directory Structure, Toolchain, and Agent Harness coverage. - -## [1.2.0] - 2026-05-19 - -### Changed - -- Initialize the missing governance evidence cache from repository evidence instead of copying the bundled template verbatim. -- Document captured repository evidence and manifest-backed commands in the initial governance evidence cache. -- Clarify that the extension generates active agent platform repository governance files from Spec Kit metadata. -- Simplify command semantics to one generate/update command: missing target governance files are generated, existing target governance files are updated. -- Include repository evidence and development commands in generated agent platform governance files. -- Add `uv`/`pytest` development configuration so the repository test suite has a reproducible runner. -- Treat the generated active agent platform section as the repository governance SSOT and `.specify/memory/agent-governance.md` as a first-run evidence cache. -- Preserve reviewed governance content from an existing active generated section during refresh instead of re-reading rules from the memory cache. -- Clarify that users review only the resolved active agent governance file; no separate memory review or second refresh is required. -- Distill detected repository areas into generated action rules with depth limited to two directory levels, including hidden and cache directories. -- Add generic directory responsibility governance without platform or project examples. -- Tighten governance template wording to preserve managed markers, scope broad linked-file updates, and limit skill-spec field requirements to repository-local skills. - -## [1.1.0] - 2026-05-15 - -### Changed - -- Decoupled the agent governance domain from specific project-governance source files. -- Updated generated projections to describe project governance as an independent domain. - -### Added - -- Tests for template and projection domain boundaries. - -## [1.0.0] - 2026-05-14 - -### Added - -- Initial `speckit.agent-governance.refresh` command. -- Agent governance memory template. -- Python helper to project managed governance sections into active agent context files. -- Optional hooks after constitution, plan, and tasks workflows. diff --git a/extensions/repository-governance/LICENSE b/extensions/repository-governance/LICENSE deleted file mode 100644 index 1fb116dbeb..0000000000 --- a/extensions/repository-governance/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 bigben - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/extensions/repository-governance/README.md b/extensions/repository-governance/README.md deleted file mode 100644 index b2cc9a5c47..0000000000 --- a/extensions/repository-governance/README.md +++ /dev/null @@ -1,113 +0,0 @@ -# Spec Kit Repository Governance - -Generate project-governance projections for the active Spec Kit agent platform target. - -## Output - -- Active agent platform target from safe `context_file` override or Spec Kit integration metadata. -- Generated `PROJECT GOVERNANCE` projection file. -- Target file content is a runtime project-governance entrypoint for coding agents, not a generator operations manual. - -## Scope - -- Generate the resolved active agent platform target when missing. -- Update existing active target project-governance projections. -- Scan repository areas as bounded evidence for missing-SSOT fallback. -- Capture repository facts as bounded evidence for gap handling and CLI reporting. -- Structure generated instructions as repository-wide, directory-tree scope, and `agent-runtime/` guidance. -- Project agent platform adapter rules from Spec Kit integration metadata. -- Index explicit fixed directory-tree SSOT entrypoints in the generated SSOT index. -- Report repository-local skills and MCP config files as evidence candidates only unless an explicit `agent-runtime/` SSOT source names them. -- Analyze repository areas to depth 2 only for evidence and CLI summaries. -- Use Directory Tree fallback only when no fixed directory-tree SSOT matches, or the matched tree has no source_refs and task scope is explicit. -- Overwrite the active agent platform target on generation. -- Do not generate Copilot `.github/instructions/*.instructions.md` companion files. -- Generate repository evidence from the current repository state on every run. -- Review only the active agent platform target. -- Remove legacy managed sections only from non-active context files enumerated by `CONTEXT_FILES`. -- Missing fixed directory-tree SSOT is reported as a clarification need for governed changes; generated output must not invent repository policy from descriptive repository evidence. -- Do not project full repository fact inventories, repository area lists, skill capability lists, or development command lists into the active target. - -## Install - -```bash -specify extension add repository-governance --from https://github.com/bigsmartben/spec-kit-agent-governance/archive/refs/tags/v3.0.2.zip -``` - -Local development: - -```bash -specify extension add --dev /path/to/spec-kit-agent-governance -``` - -## Run - -```text -/speckit.repository-governance.generate -``` - -Helper: - -```bash -uv run python .specify/extensions/repository-governance/scripts/generate_repository_governance.py -``` - -## Build - -```bash -uv run python tools/build_repository_governance_zip.py -``` - -## Files - -- `extension.yml` -- `commands/speckit.repository-governance.generate.md` -- `scripts/generate_repository_governance.py` -- `templates/repository-governance-template.md` - -## SSOT Index - -- `agent-runtime/` SSOT index: status, source_refs, and gap code. -- `engineering-runtime/` SSOT index: status, source_refs, and gap code. -- `poc/` SSOT index: status, source_refs, and gap code. -- `source-code/` SSOT index: status, source_refs, and gap code. -- `test-code/` SSOT index: status, source_refs, and gap code. -- `other-tools/` SSOT index: status, source_refs, and gap code. -- Repository-level and nested project evidence is scanned through fixed directory trees with bounded parent depth, and matched tree entrypoints become SSOT index source_refs. -- `agent-runtime/` examples include `.codex/`, `.claude/`, `AGENTS.md`, `CLAUDE.md`, and other agent rule files. -- `engineering-runtime/` examples include `.github/workflows/`, environment files, DevOps, CI/CD, Docker, manifests, lockfiles, permissions, password, key, and secrets surfaces. -- `poc/` examples include technical spikes, architecture drafts, UC designs, prototypes, research notes, validation reports, and validation conclusions before formal iteration. -- `source-code/` examples include client, server, API, route, library, script, command, and template code. -- `test-code/` examples include test code, fixtures, prepared data, resources, reports, and test conclusions. -- `other-tools/` examples include `.codegraph/`, editor tooling, MCP configs, and `tools/`. - -## Instruction Layers - -- Repository-wide instructions summarize authority, active-target scope, write boundaries, validation commands, and handoff expectations. -- SSOT index maps `agent-runtime/`, `engineering-runtime/`, `poc/`, `source-code/`, `test-code/`, and `other-tools/` to source_refs and gap codes. -- SSOT routing maps task types and repository paths to fixed directory-tree SSOT entries. -- Path and task scope rules keep generated guidance deterministic without expanding the write surface. -- `poc/` routing keeps exploration out of formal implementation: before editing `source-code/` from POC work, create or cite a formal design or task; POC conclusions need validation record source_refs under `poc/validation-reports/` or `NEEDS_CLARIFICATION:poc-validation-record`. -- Agent runtime instructions cover adapter behavior, repository-local skills, MCP discovery, external tools, permissions, and failure handling. -- The extension emits one active target file and does not generate platform companion instruction files. - -## Evidence Discovery - -- Repository facts are scanned from README files, project docs, repository policy files, feature specs, source/test paths, nested manifests, and runtime/build configuration. -- Development command sources are scanned from package scripts or Python/uv test conventions. -- Scanned facts feed missing-SSOT handling and CLI evidence summaries; only matched fixed directory-tree entrypoints are projected as SSOT source_refs. - -## Agent Adapter - -- Repository capability layer: source-backed repository-local skills and MCP candidates only. -- Agent adapter layer: use explicit integration support when available; otherwise use generic fallback rules. -- Platform projection layer: apply only rules supported by the active target file. - -Repository-local `SKILL.md` files are reported as evidence and read when they match the task; they are not `agent-runtime/` SSOT source_refs unless an explicit `agent-runtime/` SSOT source names them. MCP config files are reported as candidates and evidence only; active servers, resources, and tools must be enumerated from the agent platform runtime before use. - -## Verify - -```bash -uv run --locked python -m py_compile scripts/generate_repository_governance.py tools/build_repository_governance_zip.py tests/test_governance_domains.py -uv run --locked pytest -q -``` diff --git a/extensions/repository-governance/commands/speckit.repository-governance.generate.md b/extensions/repository-governance/commands/speckit.repository-governance.generate.md deleted file mode 100644 index 9d473daf14..0000000000 --- a/extensions/repository-governance/commands/speckit.repository-governance.generate.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -description: "Generate the active project-governance projection" ---- - -# Project Governance Projection Generation - -## Input - -$ARGUMENTS - -## Output - -- Active agent platform target. -- Generated `PROJECT GOVERNANCE` projection file. -- Project-governance instructions for the active coding-agent platform. - -## Procedure - -1. Require `.specify/`. -2. Run the deterministic projection helper: - - ```bash - uv run python .specify/extensions/repository-governance/scripts/generate_repository_governance.py - ``` -3. Review only the reported active agent platform target. -4. Treat repository evidence in the report as descriptive source-backed context, not as generated implementation work. - -## Report - -- active agent platform target -- generated or updated -- review target -- fixed directory-tree SSOT index summary -- descriptive evidence scan summary -- unresolved risks or clarification needs diff --git a/extensions/repository-governance/extension.yml b/extensions/repository-governance/extension.yml deleted file mode 100644 index 8437ea2acc..0000000000 --- a/extensions/repository-governance/extension.yml +++ /dev/null @@ -1,46 +0,0 @@ -schema_version: "1.0" - -extension: - id: repository-governance - name: "Repository Governance" - version: "3.0.2" - description: "Generate project-governance projections from Spec Kit metadata" - author: "bigben" - repository: "https://github.com/bigsmartben/spec-kit-agent-governance" - homepage: "https://github.com/bigsmartben/spec-kit-agent-governance" - license: "MIT" - -requires: - speckit_version: ">=0.8.0" - tools: - - name: "uv" - required: true - -provides: - commands: - - name: "speckit.repository-governance.generate" - file: "commands/speckit.repository-governance.generate.md" - description: "Generate the active project-governance projection" - -hooks: - after_constitution: - command: "speckit.repository-governance.generate" - optional: true - prompt: "Generate project-governance projection after constitution changes?" - description: "Generates the active agent platform target" - after_plan: - command: "speckit.repository-governance.generate" - optional: true - prompt: "Generate project-governance projection after planning?" - description: "Keeps the active agent platform target aligned" - after_tasks: - command: "speckit.repository-governance.generate" - optional: true - prompt: "Generate project-governance projection after task generation?" - description: "Keeps the active agent platform target aligned" - -tags: - - "governance" - - "agents" - - "memory" - - "context" diff --git a/extensions/repository-governance/scripts/generate_repository_governance.py b/extensions/repository-governance/scripts/generate_repository_governance.py deleted file mode 100755 index f14abcb3ca..0000000000 --- a/extensions/repository-governance/scripts/generate_repository_governance.py +++ /dev/null @@ -1,1178 +0,0 @@ -#!/usr/bin/env python3 -"""Generate Spec Kit project-governance projections.""" - -from __future__ import annotations - -import json -import re -import sys -from functools import lru_cache -from pathlib import Path -from typing import Any - - -INTEGRATION_JSON = Path(".specify/integration.json") -INIT_OPTIONS_JSON = Path(".specify/init-options.json") - -MARKER_START = "" -MARKER_END = "" -LEGACY_MARKER_START = "" -LEGACY_MARKER_END = "" -GENERIC_INTEGRATION = "generic" -CONTEXT_FILE_SUFFIXES = {".md", ".mdc", ".txt"} -CONTEXT_FILE_NAME_PATTERN = re.compile(r"(agent|agents|rule|rules|instruction|instructions|governance)", re.IGNORECASE) -PROTECTED_CUSTOM_CONTEXT_ROOTS = { - ".github", - ".git", - "scripts", - "src", - "tests", - "test", - "spec", - "specs", -} -MCP_CONFIG_NAMES = {".mcp.json", "mcp.json", "mcp.yml", "mcp.yaml", "mcp.config.json"} - -CONTEXT_FILES = { - "agy": "AGENTS.md", - "amp": "AGENTS.md", - "auggie": ".augment/rules/specify-rules.md", - "bob": "AGENTS.md", - "claude": "CLAUDE.md", - "codebuddy": "CODEBUDDY.md", - "codex": "AGENTS.md", - "copilot": ".github/copilot-instructions.md", - "cursor-agent": ".cursor/rules/specify-rules.mdc", - "devin": "AGENTS.md", - "forge": "AGENTS.md", - "gemini": "GEMINI.md", - "generic": "AGENTS.md", - "goose": "AGENTS.md", - "iflow": "IFLOW.md", - "junie": ".junie/AGENTS.md", - "kilocode": ".kilocode/rules/specify-rules.md", - "kimi": "KIMI.md", - "kiro-cli": "AGENTS.md", - "lingma": ".lingma/rules/specify-rules.md", - "opencode": "AGENTS.md", - "pi": "AGENTS.md", - "qodercli": "QODER.md", - "qwen": "QWEN.md", - "roo": ".roo/rules/specify-rules.md", - "shai": "SHAI.md", - "tabnine": "TABNINE.md", - "trae": ".trae/rules/project_rules.md", - "vibe": "AGENTS.md", - "windsurf": ".windsurf/rules/specify-rules.md", - "zed": "AGENTS.md", -} - -README_FILES = ["README.md", "README.markdown", "README.txt"] -PROJECT_DOC_FILES = [ - "CONTRIBUTING.md", - "SECURITY.md", - "SUPPORT.md", - "ARCHITECTURE.md", - "ROADMAP.md", -] -PROJECT_DOC_DIRS = ["docs", "adr", "adrs", "specs"] -PACKAGE_MANIFESTS = [ - "package.json", - "pyproject.toml", - "Cargo.toml", - "go.mod", - "Gemfile", - "pom.xml", - "build.gradle", - "build.gradle.kts", - "settings.gradle", - "settings.gradle.kts", - "composer.json", - "requirements.txt", - "setup.py", - "setup.cfg", -] -LOCKFILES = [ - "package-lock.json", - "pnpm-lock.yaml", - "yarn.lock", - "bun.lockb", - "uv.lock", - "poetry.lock", - "Pipfile.lock", - "Cargo.lock", - "go.sum", - "Gemfile.lock", - "composer.lock", -] -TASK_RUNNERS = ["Makefile", "Taskfile.yml", "Taskfile.yaml", "justfile", "Brewfile"] -SOURCE_DIRS = ["src", "app", "lib", "services", "packages", "apps", "cmd", "internal", "scripts", "commands", "templates"] -TEST_DIRS = ["test", "tests", "spec", "specs", "e2e"] -REPOSITORY_POLICY_FILES = [ - "LICENSE", - "CODEOWNERS", - ".github/CODEOWNERS", - "CONTRIBUTING.md", - "SECURITY.md", - "SUPPORT.md", -] -EXTENSION_ASSET_FILES = ["extension.yml", ".extensionignore"] -EXTENSION_ASSET_DIRS = ["commands", "templates"] -EXTENSION_CONTRACT_FILES = [ - "commands/speckit.repository-governance.generate.md", - "templates/repository-governance-template.md", - "docs/extension-governance.md", -] -API_CONTRACT_FILES = [ - "openapi.yaml", - "openapi.yml", - "openapi.json", - "api.yaml", - "api.yml", - "schema.graphql", - "buf.yaml", -] -ARCHITECTURE_DIRS = ["infra", "deploy", "deployment", "k8s", "helm", "terraform"] -BUILD_CONFIG_FILES = [ - "tsconfig.json", - "jsconfig.json", - "vite.config.js", - "vite.config.ts", - "next.config.js", - "next.config.mjs", - "webpack.config.js", - "rollup.config.js", - "turbo.json", - "nx.json", - "pnpm-workspace.yaml", - "lerna.json", -] -CODE_STYLE_FILES = [ - ".editorconfig", - ".prettierrc", - ".prettierrc.json", - ".prettierrc.yml", - "prettier.config.js", - "prettier.config.cjs", - "eslint.config.js", - "eslint.config.mjs", - ".eslintrc", - ".eslintrc.json", - "biome.json", - "ruff.toml", - ".ruff.toml", - "mypy.ini", - "pytest.ini", - "tox.ini", - "vitest.config.js", - "vitest.config.ts", - "jest.config.js", - "playwright.config.ts", -] -RUNTIME_CONFIG_FILES = [ - ".env.example", - ".env.sample", - "Dockerfile", - "docker-compose.yml", - "docker-compose.yaml", - "compose.yml", - "compose.yaml", - "Procfile", - "vercel.json", - "netlify.toml", - "fly.toml", -] -ROUTE_BASE_DIRS = ("src", "app", "lib", "services", "packages") -ROUTE_TEXT_SUFFIXES = { - ".cjs", - ".cs", - ".go", - ".java", - ".js", - ".jsx", - ".kt", - ".mjs", - ".php", - ".py", - ".rb", - ".rs", - ".scala", - ".ts", - ".tsx", -} -MAX_ROUTE_FILE_BYTES = 512_000 -SPEC_KIT_METADATA = [".specify/integration.json", ".specify/init-options.json", ".specify/extensions.yml"] -EVIDENCE_SCAN_MAX_PARENT_DEPTH = 3 -SUPPORTED_ADAPTERS = {"codex", "zed"} -AGENT_RUNTIME_DIRS = [ - ".codex", - ".claude", - ".cursor", - ".augment", - ".junie", - ".kilocode", - ".lingma", - ".roo", - ".trae", - ".windsurf", -] -ENGINEERING_RUNTIME_FILES = [ - ".editorconfig", - ".extensionignore", - ".gitignore", - "Dockerfile", - "compose.yml", - "compose.yaml", - "docker-compose.yml", - "docker-compose.yaml", - "extension.yml", - *PACKAGE_MANIFESTS, - *LOCKFILES, - *TASK_RUNNERS, - *BUILD_CONFIG_FILES, - *CODE_STYLE_FILES, - *RUNTIME_CONFIG_FILES, - *EXTENSION_CONTRACT_FILES, -] -ENGINEERING_RUNTIME_DIRS = [ - ".devcontainer", - ".github/workflows", - "ci", - "devops", - "deploy", - "deployment", - "docker", - "env", - "environments", - "helm", - "infra", - "k8s", - "terraform", -] -POC_DIRS = [ - "poc", - "poc/technical-spikes", - "poc/architecture-drafts", - "poc/uc-designs", - "poc/prototypes", - "poc/research-notes", - "poc/validation-reports", -] -SOURCE_CODE_DIRS = ["src", "app", "client", "clients", "frontend", "web", "server", "backend", "api", "lib", "services", "packages", "apps", "cmd", "internal", "scripts", "commands", "templates"] -TEST_CODE_DIRS = ["test", "tests", "spec", "specs", "e2e", "fixtures", "testdata", "test-results", "coverage"] -TOOLING_DIRS = ["tools", ".codegraph", ".vscode", ".idea"] -TOOLING_FILES = ["codegraph.json", "codegraph.yml", ".codegraph.json", ".codegraph.yml", *MCP_CONFIG_NAMES] - - -def main() -> int: - root = Path.cwd() - if not (root / ".specify").is_dir(): - print("Error: .specify/ not found. Run from a Spec Kit project root.", file=sys.stderr) - return 1 - - state = read_json(root / INTEGRATION_JSON) - init_options = read_json(root / INIT_OPTIONS_JSON) - target = resolve_target(root, state, init_options) - projection = render_projection(root, target, state, init_options) - index_summary = ssot_index_summary(root, state, init_options) - evidence_summary = repository_evidence_summary(root, state, init_options) - action = write_projection(target, projection) - remove_stale_sections(root, target, init_options) - - print(f"Target agent platform file: {rel(root, target)}") - print(f"Project-governance projection: {action}") - print(f"Review target: {rel(root, target)}") - print(f"SSOT index summary: {index_summary}") - print(f"Evidence scan summary: {evidence_summary}") - return 0 - - -def ssot_index_summary(root: Path, state: dict[str, Any], init_options: dict[str, Any]) -> str: - entries = ssot_index_data(root, state, init_options) - parts = [] - for name, refs in entries: - status = "indexed" if refs else "missing" - gap = "none" if refs else ssot_gap_code(name) - parts.append(f"{name}: {status} ({len(refs)} refs, gap: {gap})") - return "; ".join(parts) - - -def repository_evidence_summary(root: Path, state: dict[str, Any], init_options: dict[str, Any]) -> str: - evidence = repository_evidence_lines(root, state, init_options) - detected = [line for line in evidence if "none detected" not in line and "`unknown`" not in line] - return "; ".join(line.removeprefix("- ") for line in detected) or "none detected" - - -def repository_evidence_lines(root: Path, state: dict[str, Any], init_options: dict[str, Any]) -> list[str]: - lines = [ - evidence_line("README", existing_paths(root, README_FILES)), - evidence_line("Project docs", unique_ordered([*existing_paths(root, PROJECT_DOC_FILES), *existing_dirs(root, PROJECT_DOC_DIRS)])), - evidence_line("Repository policy", existing_paths(root, REPOSITORY_POLICY_FILES)), - evidence_line("Spec Kit metadata", existing_paths(root, SPEC_KIT_METADATA)), - evidence_line("Extension assets", extension_asset_paths(root)), - evidence_line("Package manifest", package_manifest_paths(root)), - evidence_line("Lockfiles", lockfile_paths(root)), - evidence_line("Task runners", existing_paths(root, TASK_RUNNERS)), - evidence_line("CI workflows", directory_files(root, ".github/workflows")), - evidence_line("Source paths", source_paths(root)), - evidence_line("Test paths", test_paths(root)), - evidence_line("Feature specs", scan_feature_specs(root)), - evidence_line("API contracts", api_contract_paths(root)), - evidence_line("Build config", build_config_paths(root)), - evidence_line("Runtime config", runtime_config_paths(root)), - evidence_line("Repository areas", repository_area_paths(root)), - evidence_line("Existing agent context files", existing_context_files(root, init_options, state)), - evidence_line("Repository-local skills", scan_skills(root)), - evidence_line("MCP configs", scan_mcp_configs(root)), - f"- Active integration: `{default_integration(state) or 'unknown'}`", - f"- Resolved agent platform target: `{rel(root, resolve_target(root, state, init_options))}`", - ] - return lines - - -def agent_runtime_ssot_refs(root: Path, init_options: dict[str, Any], state: dict[str, Any]) -> list[str]: - return unique_ordered( - [ - *existing_context_files(root, init_options, state), - *existing_dirs(root, AGENT_RUNTIME_DIRS), - ] - ) - - -def engineering_runtime_ssot_refs(root: Path) -> list[str]: - return unique_ordered( - [ - *existing_paths(root, ENGINEERING_RUNTIME_FILES), - *existing_dirs(root, ENGINEERING_RUNTIME_DIRS), - *directory_files(root, ".github/workflows"), - ] - ) - - -def source_code_ssot_refs(root: Path) -> list[str]: - return unique_ordered( - [ - *existing_dirs(root, SOURCE_CODE_DIRS), - *api_contract_paths(root), - *route_files(root), - ] - ) - - -def poc_ssot_refs(root: Path) -> list[str]: - return unique_ordered( - [ - *existing_dirs(root, POC_DIRS), - *directory_files(root, "poc/validation-reports"), - ] - ) - - -def test_code_ssot_refs(root: Path) -> list[str]: - return unique_ordered( - [ - *existing_dirs(root, TEST_CODE_DIRS), - *existing_paths(root, ["pytest.ini", "tox.ini", "vitest.config.js", "vitest.config.ts", "jest.config.js", "playwright.config.ts"]), - ] - ) - - -def tooling_ssot_refs(root: Path) -> list[str]: - return unique_ordered( - [ - *existing_dirs(root, TOOLING_DIRS), - *existing_paths(root, TOOLING_FILES), - ] - ) - - -def evidence_line(label: str, values: list[str]) -> str: - return f"- {label}: {format_values(values)}" - - -def format_values(values: list[str]) -> str: - return ", ".join(f"`{value}`" for value in values) if values else "none detected" - - -def format_index_refs(values: list[str]) -> str: - if not values: - return "none detected" - return ", ".join(f"`{value}`" for value in values) - - -def unique_ordered(values: list[str]) -> list[str]: - seen: set[str] = set() - result: list[str] = [] - for value in values: - if value in seen: - continue - seen.add(value) - result.append(value) - return result - - -def existing_paths(root: Path, names: list[str]) -> list[str]: - return bounded_named_files(root, names) - - -def existing_dirs(root: Path, names: list[str]) -> list[str]: - return bounded_named_dirs(root, names) - - -def bounded_named_files(root: Path, names: list[str], max_parent_depth: int = EVIDENCE_SCAN_MAX_PARENT_DEPTH) -> list[str]: - matches: list[str] = [] - files = project_files(root) - for name in names: - for path in files: - relative = path.relative_to(root) - path_text = relative.as_posix() - if name.startswith(".") and "/" not in name and path_text != name: - continue - if (path_text == name or path.name == name) and len(relative.parts) - 1 <= max_parent_depth: - matches.append(rel(root, path)) - return unique_ordered(matches) - - -def bounded_named_dirs(root: Path, names: list[str], max_parent_depth: int = EVIDENCE_SCAN_MAX_PARENT_DEPTH) -> list[str]: - matches: list[str] = [] - dirs = project_dirs(root) - for name in names: - for path in dirs: - relative = path.relative_to(root) - path_text = relative.as_posix() - if path_text != name and path.name != name: - continue - if len(relative.parts) - 1 <= max_parent_depth: - matches.append(f"{rel(root, path)}/") - return unique_ordered(matches) - - -def bounded_glob_files(root: Path, patterns: list[str], max_parent_depth: int = EVIDENCE_SCAN_MAX_PARENT_DEPTH) -> list[str]: - matches: list[str] = [] - for path in project_files(root): - relative = path.relative_to(root) - if len(relative.parts) - 1 > max_parent_depth: - continue - if any(relative.match(pattern) for pattern in patterns): - matches.append(rel(root, path)) - return unique_ordered(matches) - - -def project_files(root: Path) -> tuple[Path, ...]: - files, _dirs = project_walk(root) - return files - - -def project_dirs(root: Path) -> tuple[Path, ...]: - _files, dirs = project_walk(root) - return dirs - - -@lru_cache(maxsize=16) -def project_walk(root: Path) -> tuple[tuple[Path, ...], tuple[Path, ...]]: - files: list[Path] = [] - dirs: list[Path] = [] - stack = [root] - while stack: - base = stack.pop() - try: - children = sorted(base.iterdir(), key=lambda path: path.as_posix()) - except OSError: - continue - next_dirs: list[Path] = [] - for child in children: - if ignored(child): - continue - if child.is_dir(): - dirs.append(child) - next_dirs.append(child) - elif child.is_file(): - files.append(child) - stack.extend(reversed(next_dirs)) - return tuple(files), tuple(dirs) - - -def existing_top_level_globs(root: Path, patterns: list[str]) -> list[str]: - matches: list[str] = [] - for pattern in patterns: - for path in sorted(root.glob(pattern)): - if path.is_file(): - matches.append(rel(root, path)) - return unique_ordered(matches) - - -def package_manifest_paths(root: Path) -> list[str]: - return unique_ordered( - [ - *existing_paths(root, PACKAGE_MANIFESTS), - *existing_top_level_globs(root, ["requirements*.txt"]), - ] - ) - - -def lockfile_paths(root: Path) -> list[str]: - return existing_paths(root, LOCKFILES) - - -def source_paths(root: Path) -> list[str]: - return existing_dirs(root, SOURCE_DIRS) - - -def test_paths(root: Path) -> list[str]: - return existing_dirs(root, TEST_DIRS) - - -def api_contract_paths(root: Path) -> list[str]: - return unique_ordered( - [ - *existing_paths(root, API_CONTRACT_FILES), - *existing_top_level_globs(root, ["*.proto", "*.graphql"]), - ] - ) - - -def build_config_paths(root: Path) -> list[str]: - return unique_ordered( - [ - *existing_paths(root, BUILD_CONFIG_FILES), - *existing_top_level_globs(root, ["tsconfig*.json", "vite.config.*", "next.config.*", "webpack.config.*"]), - ] - ) - - -def extension_asset_paths(root: Path) -> list[str]: - return unique_ordered([*existing_paths(root, EXTENSION_ASSET_FILES), *existing_dirs(root, EXTENSION_ASSET_DIRS)]) - - -def is_repository_governance_extension_source(root: Path) -> bool: - return all((root / path).is_file() for path in ("extension.yml", *EXTENSION_CONTRACT_FILES)) - - -def runtime_config_paths(root: Path) -> list[str]: - return unique_ordered( - [ - *existing_paths(root, RUNTIME_CONFIG_FILES), - *existing_dirs(root, ARCHITECTURE_DIRS), - ] - ) - - -def directory_files(root: Path, directory: str) -> list[str]: - base = root / directory - if not base.is_dir(): - return [] - return sorted(rel(root, path) for path in base.iterdir() if path.is_file()) - - -def route_files(root: Path) -> list[str]: - result: list[str] = [] - route_name_pattern = re.compile(r"(route|routes|router|api|endpoint)", re.IGNORECASE) - route_content_pattern = re.compile( - r"(@app\.route|APIRouter|router\.|Route::|express\(|fastify\(|app\.(get|post|put|delete|patch)\()", - re.IGNORECASE, - ) - for path in project_files(root): - if not route_file_candidate(root, path): - continue - if route_name_pattern.search(path.name): - result.append(rel(root, path)) - continue - try: - text = path.read_text(encoding="utf-8", errors="ignore") - except OSError: - continue - if route_content_pattern.search(text): - result.append(rel(root, path)) - return unique_ordered(result) - - -def route_file_candidate(root: Path, path: Path) -> bool: - relative = path.relative_to(root) - if not relative.parts or relative.parts[0] not in ROUTE_BASE_DIRS: - return False - if len(relative.parts) - 1 > EVIDENCE_SCAN_MAX_PARENT_DEPTH: - return False - if path.suffix.lower() not in ROUTE_TEXT_SUFFIXES: - return False - try: - return path.stat().st_size <= MAX_ROUTE_FILE_BYTES - except OSError: - return False - - -def repository_area_lines(root: Path) -> list[str]: - lines = [] - for area in repository_area_paths(root): - if "/" in area.rstrip("/"): - parent = area.rstrip("/").split("/", 1)[0] + "/" - lines.append(f"- `{area}`: change with parent area `{parent}`.") - else: - lines.append(f"- `{area}`: review before changing linked areas.") - return lines or ["- none detected"] - - -def repository_area_paths(root: Path) -> list[str]: - areas: list[str] = [] - for path in sorted(root.iterdir()): - if not path.is_dir(): - continue - areas.append(f"{path.name}/") - for child in sorted(path.iterdir()): - if child.is_dir(): - areas.append(f"{path.name}/{child.name}/") - return areas - - -def existing_context_files(root: Path, init_options: dict[str, Any], state: dict[str, Any]) -> list[str]: - paths = known_context_paths(root, init_options) - resolved = resolve_target(root, state, init_options) - paths.add(resolved) - return sorted(rel(root, path) for path in paths if path.exists()) - - -def known_context_paths(root: Path, init_options: dict[str, Any]) -> set[Path]: - paths: set[Path] = set() - for value in CONTEXT_FILES.values(): - target = safe_project_path(root, value) - if target is not None: - paths.add(target) - return paths - - -def development_command_lines(root: Path) -> list[str]: - commands = package_script_lines(root) - if commands: - commands.append("- manifest commands over ad hoc equivalents") - return commands - commands = python_project_command_lines(root) - if commands: - commands.append("- project commands over ad hoc equivalents") - return commands - return ["- none detected"] - - -def package_script_lines(root: Path) -> list[str]: - package_json = root / "package.json" - data = read_json(package_json) - scripts = data.get("scripts") - if not isinstance(scripts, dict): - return [] - result: list[str] = [] - for name in sorted(scripts): - value = scripts[name] - if not isinstance(value, str) or not value.strip(): - continue - command = f"npm {name}" if name in {"start", "stop", "test", "restart"} else f"npm run {name}" - result.append(f"- `{command}` -> `{value.strip()}`") - return result - - -def python_project_command_lines(root: Path) -> list[str]: - if not (root / "pyproject.toml").is_file(): - return [] - if not (test_paths(root) or existing_paths(root, ["pytest.ini", "tox.ini"])): - return [] - prefix = "uv run --locked" if (root / "uv.lock").is_file() else "uv run" - return [f"- `{prefix} pytest -q` -> pytest suite"] - - -def read_json(path: Path) -> dict[str, Any]: - if not path.exists(): - return {} - try: - data = json.loads(path.read_text(encoding="utf-8-sig")) - except (json.JSONDecodeError, OSError, UnicodeDecodeError): - return {} - return data if isinstance(data, dict) else {} - - -def resolve_target(root: Path, state: dict[str, Any], init_options: dict[str, Any]) -> Path: - init_target = safe_context_file_path(root, init_options.get("context_file")) - if init_target is not None: - return init_target - for value in (CONTEXT_FILES.get(default_integration(state) or ""), CONTEXT_FILES.get(GENERIC_INTEGRATION)): - target = safe_project_path(root, value) - if target is not None: - return target - return root / CONTEXT_FILES[GENERIC_INTEGRATION] - - -def safe_project_path(root: Path, value: Any) -> Path | None: - if not isinstance(value, str) or not value.strip(): - return None - raw = Path(value.strip()) - if raw.is_absolute(): - return None - candidate = raw if raw.is_absolute() else root / raw - try: - candidate.resolve(strict=False).relative_to(root.resolve()) - except (OSError, ValueError): - return None - return candidate - - -def safe_context_file_path(root: Path, value: Any) -> Path | None: - target = safe_project_path(root, value) - if target is None: - return None - if is_known_context_target(root, target): - return target - if target.suffix.lower() not in CONTEXT_FILE_SUFFIXES: - return None - if not CONTEXT_FILE_NAME_PATTERN.search(target.name): - return None - if is_protected_custom_context_target(root, target): - return None - return target - - -def is_known_context_target(root: Path, target: Path) -> bool: - target_text = rel(root, target) - return target_text in set(CONTEXT_FILES.values()) - - -def is_protected_custom_context_target(root: Path, target: Path) -> bool: - target_text = rel(root, target) - parts = Path(target_text).parts - if not parts: - return True - if parts[0] in PROTECTED_CUSTOM_CONTEXT_ROOTS: - return True - if target.name in MCP_CONFIG_NAMES: - return True - if target.name.startswith(".env"): - return True - return any(part.lower() in {"secret", "secrets", "permission", "permissions"} for part in parts) - - -def default_integration(state: dict[str, Any]) -> str | None: - for key in ("default_integration", "integration"): - value = state.get(key) - if isinstance(value, str) and value.strip(): - return value.strip() - installed = state.get("installed_integrations") - if isinstance(installed, list): - for value in installed: - if isinstance(value, str) and value.strip(): - return value.strip() - return None - - -def installed_integrations(state: dict[str, Any]) -> list[str]: - seen: set[str] = set() - values = state.get("installed_integrations") - if not isinstance(values, list): - values = [] - default = default_integration(state) - ordered = ([default] if default else []) + values - result: list[str] = [] - for value in ordered: - if not isinstance(value, str) or not value.strip(): - continue - clean = value.strip() - if clean not in seen: - seen.add(clean) - result.append(clean) - return result - - -def render_projection(root: Path, target: Path, state: dict[str, Any], init_options: dict[str, Any]) -> str: - default_key = default_integration(state) or "unknown" - installed = installed_integrations(state) - style = projection_style(target) - lines = [ - "# Project Governance Projection", - "- Projection: active agent platform target file.", - "- Domain: project-governance.", - "- Framework: Project Governance Projection Framework.", - "- Extension identity: repository-governance.", - f"- Target: {rel(root, target)}", - f"- Active integration: {default_key}", - "- Projection source: current repository scan.", - "", - "## Repository-Wide Instructions", - f"- {style_lead(style)}", - "- Treat this file as the active project-governance entrypoint for coding-agent work in this repository.", - "- Keep task reasoning grounded in source-backed repository facts, matched directory-tree routes, and explicit user instructions.", - "- Keep edits scoped to the active task and matched fixed directory tree.", - "- Fixed directory-tree SSOT: when a path matches `agent-runtime/`, `engineering-runtime/`, `poc/`, `source-code/`, `test-code/`, or `other-tools/`, route it to that SSOT.", - "", - "### Context", - f"- Installed integrations: {', '.join(installed) if installed else 'none'}", - f"- Repository-local skill evidence: {evidence_count(scan_skills(root))}", - f"- MCP config candidates: {evidence_count(scan_mcp_configs(root))}", - f"- Extensions config: .specify/extensions.yml ({extensions_status(root)})", - "", - "### Authority", - *authority_default(), - "", - "## SSOT Index", - *directory_tree_ssot_registry_default(), - "", - *ssot_index_lines(root, state, init_options), - "", - "### Missing SSOT Handling", - *missing_ssot_handling_default(), - "", - "## Directory Tree And Task Scope Rules", - *task_scope_rules_default(), - "", - "### Directory Tree Fallback", - *directory_tree_fallback_default(), - "", - "## agent-runtime", - *agent_adapter_lines(root, target, default_key), - "", - *mcp_default(style), - "", - *skill_default(style), - "", - *repository_workflow_default(), - "", - "## Write Boundaries", - *write_boundary_default(style), - "", - "## Handoff", - *handoff_default(style), - "", - ] - return "\n".join(lines) - - -def write_projection(target: Path, projection: str) -> str: - existed = target.exists() - updated = projection - if target.suffix == ".mdc": - updated = ensure_mdc_frontmatter(updated) - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(normalize_newlines(updated), encoding="utf-8") - return "updated" if existed else "generated" - - -def remove_stale_sections(root: Path, active: Path, init_options: dict[str, Any]) -> None: - paths = known_context_paths(root, init_options) - for path in paths: - if same_path(path, active): - continue - remove_section(path) - - -def remove_section(path: Path) -> None: - if not path.exists(): - return - content = path.read_text(encoding="utf-8-sig") - managed_range = find_managed_range(content) - if managed_range is None: - return - removal_start, removal_end = managed_range - if removal_start > 1 and content[removal_start - 1] == "\n" and content[removal_start - 2] == "\n": - removal_start -= 1 - updated = normalize_newlines(content[:removal_start] + content[removal_end:]) - if not updated.strip() or (path.suffix == ".mdc" and re.match(r"^---\n.*?\n---\s*$", updated, re.DOTALL)): - path.unlink() - else: - path.write_text(updated, encoding="utf-8") - - -def directory_tree_fallback_default() -> list[str]: - return [ - "- Use only when no fixed directory-tree SSOT matches, or the matched tree has no source_refs and the task scope is explicit.", - "- Treat scanned repository areas as descriptive context, not as approved path policy.", - "- Keep new or moved files aligned with existing nearby conventions unless the user supplies a different target.", - "- Record the matched `NEEDS_CLARIFICATION:` gap in handoff when placement is ambiguous.", - ] - - -def task_scope_rules_default() -> list[str]: - return [ - "- Routing rule: seeing a file in one fixed directory tree classifies it as that SSOT; no match falls through to Directory Tree Fallback.", - "- `agent-runtime/`: `.codex/`, `.claude/`, `AGENTS.md`, `CLAUDE.md`, and other agent rule files; read `agent-runtime/` SSOT before edits.", - "- `engineering-runtime/`: `.github/workflows/`, Docker files, environment files, manifests, lockfiles, DevOps, infra, secrets, key, password, and permission surfaces; read `engineering-runtime/` SSOT before edits.", - "- `poc/`: `technical-spikes/`, `architecture-drafts/`, `uc-designs/`, `prototypes/`, `research-notes/`, and `validation-reports/`; read `poc/` SSOT before edits.", - "- `poc/` content is not formal implementation.", - "- Before editing `source-code/` from POC work, create or cite a formal design or task; implement from that artifact, not by copying experimental code directly.", - "- POC conclusions must include validation record source_refs under `poc/validation-reports/`; otherwise record `NEEDS_CLARIFICATION:poc-validation-record` in handoff.", - "- `source-code/`: `src/`, `app/`, `client/`, `server/`, `api/`, `lib/`, `services/`, `scripts/`, `commands/`, and `templates/`; read `source-code/` SSOT before edits.", - "- `test-code/`: `tests/`, `test/`, `e2e/`, `fixtures/`, `testdata/`, `test-results/`, and `coverage/`; read `test-code/` SSOT before edits.", - "- `other-tools/`: `tools/`, `.codegraph/`, MCP config files, `.vscode/`, and `.idea/`; read `other-tools/` SSOT before edits.", - "- If multiple trees match, read every matched SSOT and apply the highest authority non-conflicting rule.", - ] - - -def development_commands_default() -> list[str]: - return ["- none recorded"] - - -def directory_tree_ssot_registry_default() -> list[str]: - return [ - "- `agent-runtime/`: `.codex/`, `.claude/`, `AGENTS.md`, `CLAUDE.md`, and other agent rule files.", - "- `engineering-runtime/`: `.github/workflows/`, Docker files, environment files, manifests, lockfiles, DevOps, infra, and secrets/key/password configuration surfaces.", - "- `poc/`: pre-iteration exploration for technical spikes, architecture drafts, UC designs, prototypes, research notes, validation reports, and validation conclusions.", - "- `source-code/`: client, server, API, route, library, service, script, command, and template code.", - "- `test-code/`: test code, fixtures, prepared data, test resources, reports, and test conclusions.", - "- `other-tools/`: `tools/`, `.codegraph/`, MCP config files, `.vscode/`, `.idea/`, and auxiliary tool settings.", - ] - - -def ssot_index_lines(root: Path, state: dict[str, Any], init_options: dict[str, Any]) -> list[str]: - lines: list[str] = [] - for name, refs in ssot_index_data(root, state, init_options): - lines.extend(ssot_index_entry(name, refs)) - return lines - - -def ssot_index_data(root: Path, state: dict[str, Any], init_options: dict[str, Any]) -> list[tuple[str, list[str]]]: - return [ - ("agent-runtime", agent_runtime_ssot_refs(root, init_options, state)), - ("engineering-runtime", engineering_runtime_ssot_refs(root)), - ("poc", poc_ssot_refs(root)), - ("source-code", source_code_ssot_refs(root)), - ("test-code", test_code_ssot_refs(root)), - ("other-tools", tooling_ssot_refs(root)), - ] - - -def ssot_index_entry(name: str, refs: list[str]) -> list[str]: - status = "indexed" if refs else "missing" - gap = "none" if refs else ssot_gap_code(name) - return [ - f"- `{name}/` SSOT index:", - f" - status: {status}", - f" - source_refs: {format_index_refs(refs)}", - f" - gap: {gap}", - ] - - -def ssot_gap_code(name: str) -> str: - return f"NEEDS_CLARIFICATION:{name}" - - -def missing_ssot_handling_default() -> list[str]: - return [ - "- If a fixed directory-tree SSOT is missing or incomplete, treat repository evidence as descriptive context only.", - "- Before changing a surface governed by missing SSOT, ask for clarification or record `NEEDS_CLARIFICATION:` in handoff.", - "- Use existing code and config facts for narrow edits only when task scope and validation are explicit.", - "- Do not invent repository policy from descriptive repository evidence.", - ] - - -def authority_default() -> list[str]: - return [ - "1. Current user instruction", - "2. Safety and permission constraints", - "3. Fixed directory-tree SSOT documents", - "4. Current repository code and configuration facts", - "5. Active `PROJECT GOVERNANCE` projection", - "6. Tests and CI results", - "7. Historical documents", - "8. Explicit assumptions for reversible local edits", - "- Active projection is generated routing guidance and is subordinate to explicit fixed directory-tree SSOT documents or source-backed repository facts on substantive conflicts.", - ] - - -def repository_workflow_default() -> list[str]: - return [ - "- Classify task type and fixed directory tree before changing files.", - "- Read every SSOT route matched by Directory Tree And Task Scope Rules.", - "- Use SSOT Index source_refs as entrypoints, not as replacement content for the referenced sources.", - "- Run validation commands from explicit `engineering-runtime/` SSOT instructions or user direction when they match the changed surface.", - "- Scope: active task only.", - "- Preserve: user-authored edits.", - "- Protected files: implementation paths, CI configuration, MCP configuration, secrets, permissions, tool settings, and arbitrary repository paths outside the resolved write surface.", - "- Protected-file writes: explicit user request, named matching contract or regression test, and passing validation commands.", - "- External writes: authorized target and action only.", - "- Handoff: changed files, commands, validation, risks.", - ] - - -def find_managed_range(content: str) -> tuple[int, int] | None: - for start_marker, end_marker in ( - (MARKER_START, MARKER_END), - (LEGACY_MARKER_START, LEGACY_MARKER_END), - ): - start = content.find(start_marker) - end = content.find(end_marker, start if start != -1 else 0) - if start == -1 or end == -1 or end <= start: - continue - removal_end = end + len(end_marker) - if removal_end < len(content) and content[removal_end] == "\r": - removal_end += 1 - if removal_end < len(content) and content[removal_end] == "\n": - removal_end += 1 - return start, removal_end - return None - - -def projection_style(path: Path) -> str: - rel_path = path.as_posix() - if rel_path.endswith(".github/copilot-instructions.md"): - return "copilot" - if path.suffix == ".mdc" or "/rules/" in rel_path: - return "rule" - return "agent" - - -def style_lead(style: str) -> str: - if style == "copilot": - return "Use these as concise Copilot custom instructions for this repository." - if style == "rule": - return "Apply these repository rules before planning, editing, or using tools." - return "Follow these repository instructions when working in this project." - - -def write_boundary_default(style: str) -> list[str]: - if style == "rule": - return [ - "- Stay inside the active task scope.", - "- Edit agent context files only when the user explicitly asks for instruction changes.", - "- Protected-file writes require explicit user request, a named matching contract or regression test, and passing validation commands.", - ] - return [ - "- Keep edits inside the active task scope.", - "- Edit agent context files only when the user explicitly asks for instruction changes.", - "- Protected-file writes require explicit user request, a named matching contract or regression test, and passing validation commands.", - ] - - -def mcp_default(style: str) -> list[str]: - return [ - "- Read-only unless the user authorizes mutation.", - "- Mutation: explicit user intent with target, action, and expected effect.", - "- External writes: target, action, expected effect.", - ] - - -def skill_default(style: str) -> list[str]: - return [ - "- Repository-local skills are evidence only unless an explicit `agent-runtime/` SSOT source names them.", - "- Read matching repository-local `SKILL.md` before planning or editing.", - "- Treat skill scope declarations as task-local constraints.", - "- If a matching skill lacks scope or validation guidance, ask for clarification before expanding writes.", - ] - - -def handoff_default(style: str) -> list[str]: - return ["- changed files", "- commands run", "- validation result", "- unresolved risks"] - - -def scan_feature_specs(root: Path) -> list[str]: - specs = root / "specs" - if not specs.is_dir(): - return [] - entries: list[str] = [] - for feature in sorted(path for path in specs.iterdir() if path.is_dir()): - statuses = [f"{name}:{'present' if (feature / name).exists() else 'missing'}" for name in ("spec.md", "plan.md", "tasks.md")] - entries.append(f"{rel(root, feature)} ({', '.join(statuses)})") - return entries - - -def scan_skills(root: Path) -> list[str]: - return sorted(rel(root, path) for path in project_files(root) if path.name == "SKILL.md") - - -def agent_adapter_lines(root: Path, target: Path, integration: str) -> list[str]: - adapter_support = integration if integration in SUPPORTED_ADAPTERS else "generic fallback" - lines = [ - "- Repository capability layer: source-backed repository-local skills and MCP candidates only.", - "- Agent adapter layer: use explicit integration support when available; otherwise use generic fallback rules.", - "- Platform projection layer: apply only rules supported by the active target file.", - f"- Active integration: {integration}", - f"- Context target: {rel(root, target)}", - f"- Adapter support: {adapter_support}", - ] - if integration == "codex": - lines.extend( - [ - "- Skill discovery: repository-local `SKILL.md` capability specs, sorted by path.", - "- MCP discovery: platform runtime enumeration first; repository config candidates are evidence only unless supported by this adapter.", - ] - ) - elif integration == "zed": - lines.extend( - [ - "- Instruction discovery: Zed project instructions from the active context target.", - "- Skill discovery: evidence-only repository scan; activate skills through the Zed runtime when available.", - "- MCP discovery: enumerate Zed runtime tools before use; repository config candidates are evidence only.", - ] - ) - else: - lines.extend( - [ - "- Skill discovery: evidence-only repository scan; no platform activation is assumed.", - "- MCP discovery: no platform support is assumed; repository config candidates are evidence only.", - ] - ) - return lines - - -def evidence_count(values: list[str]) -> str: - if not values: - return "none detected" - return f"{len(values)} detected" - - -def scan_mcp_configs(root: Path) -> list[str]: - return sorted( - rel(root, path) - for path in project_files(root) - if path.name in MCP_CONFIG_NAMES - ) - - -def ignored(path: Path) -> bool: - parts = path.parts - if any(part in {".git", "__pycache__", ".venv", "node_modules"} for part in parts): - return True - return any(parts[index : index + 2] == (".specify", "extensions") for index in range(len(parts) - 1)) - - -def extensions_status(root: Path) -> str: - path = root / ".specify/extensions.yml" - if not path.exists(): - return "missing" - return "present" - - -def exists(root: Path, value: str) -> str: - return "present" if (root / value).exists() else "missing" - - -def ensure_mdc_frontmatter(content: str) -> str: - stripped = content.lstrip() - if not stripped.startswith("---"): - return "---\nalwaysApply: true\n---\n\n" + content - match = re.match(r"^(---[ \t]*\n)(.*?)(\n---[ \t]*)(\n|$)(.*)", stripped, re.DOTALL) - if not match: - return "---\nalwaysApply: true\n---\n\n" + content - opening, frontmatter, closing, sep, rest = match.groups() - if re.search(r"(?m)^[ \t]*alwaysApply[ \t]*:[ \t]*true[ \t]*$", frontmatter): - return content - if re.search(r"(?m)^[ \t]*alwaysApply[ \t]*:", frontmatter): - frontmatter = re.sub(r"(?m)^([ \t]*)alwaysApply[ \t]*:.*$", r"\1alwaysApply: true", frontmatter, count=1) - elif frontmatter.strip(): - frontmatter += "\nalwaysApply: true" - else: - frontmatter = "alwaysApply: true" - return f"{opening}{frontmatter}{closing}{sep}{rest}" - - -def same_path(left: Path, right: Path) -> bool: - try: - return left.resolve(strict=False) == right.resolve(strict=False) - except OSError: - return left.absolute() == right.absolute() - - -def rel(root: Path, path: Path) -> str: - try: - return path.relative_to(root).as_posix() - except ValueError: - return path.as_posix() - - -def normalize_newlines(content: str) -> str: - return content.replace("\r\n", "\n").replace("\r", "\n") - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/extensions/repository-governance/templates/repository-governance-template.md b/extensions/repository-governance/templates/repository-governance-template.md deleted file mode 100644 index d22d812656..0000000000 --- a/extensions/repository-governance/templates/repository-governance-template.md +++ /dev/null @@ -1,119 +0,0 @@ -# Project Governance Projection Template - - - -## Final Output - -- active agent platform project-governance projection -- generated active agent platform target file - -## Repository-Wide Instructions - -- Framework: Project Governance Projection Framework. -- Treat this file as the active project-governance entrypoint for coding-agent work in this repository. -- Keep task reasoning grounded in source-backed repository facts, matched directory-tree routes, and explicit user instructions. -- Keep edits scoped to the active task and matched fixed directory tree. -- Fixed directory-tree SSOT: when a path matches `agent-runtime/`, `engineering-runtime/`, `poc/`, `source-code/`, `test-code/`, or `other-tools/`, route it to that SSOT. - -### Authority - -1. Current user instruction -2. Safety and permission constraints -3. Fixed directory-tree SSOT documents -4. Current repository code and configuration facts -5. Active `PROJECT GOVERNANCE` projection -6. Tests and CI results -7. Historical documents -8. Explicit assumptions for reversible local edits - -- Active projection is generated routing guidance and is subordinate to explicit fixed directory-tree SSOT documents or source-backed repository facts on substantive conflicts. - -## SSOT Index - -- `agent-runtime/`: `.codex/`, `.claude/`, `AGENTS.md`, `CLAUDE.md`, and other agent rule files. -- `engineering-runtime/`: `.github/workflows/`, Docker files, environment files, manifests, lockfiles, DevOps, infra, and secrets/key/password configuration surfaces. -- `poc/`: pre-iteration exploration for technical spikes, architecture drafts, UC designs, prototypes, research notes, validation reports, and validation conclusions. -- `source-code/`: client, server, API, route, library, service, script, command, and template code. -- `test-code/`: test code, fixtures, prepared data, test resources, reports, and test conclusions. -- `other-tools/`: `tools/`, `.codegraph/`, MCP config files, `.vscode/`, `.idea/`, and auxiliary tool settings. -- `agent-runtime/` SSOT index: - - status: missing - - source_refs: none detected - - gap: NEEDS_CLARIFICATION:agent-runtime -- `engineering-runtime/` SSOT index: - - status: missing - - source_refs: none detected - - gap: NEEDS_CLARIFICATION:engineering-runtime -- `poc/` SSOT index: - - status: missing - - source_refs: none detected - - gap: NEEDS_CLARIFICATION:poc -- `source-code/` SSOT index: - - status: missing - - source_refs: none detected - - gap: NEEDS_CLARIFICATION:source-code -- `test-code/` SSOT index: - - status: missing - - source_refs: none detected - - gap: NEEDS_CLARIFICATION:test-code -- `other-tools/` SSOT index: - - status: missing - - source_refs: none detected - - gap: NEEDS_CLARIFICATION:other-tools - -### Missing SSOT Handling - -- If a fixed directory-tree SSOT is missing or incomplete, treat repository evidence as descriptive context only. -- Before changing a surface governed by missing SSOT, ask for clarification or record `NEEDS_CLARIFICATION:` in handoff. -- Use existing code and config facts for narrow edits only when task scope and validation are explicit. -- Do not invent repository policy from descriptive repository evidence. - -## Directory Tree And Task Scope Rules - -- Routing rule: seeing a file in one fixed directory tree classifies it as that SSOT; no match falls through to Directory Tree Fallback. -- `agent-runtime/`: `.codex/`, `.claude/`, `AGENTS.md`, `CLAUDE.md`, and other agent rule files; read `agent-runtime/` SSOT before edits. -- `engineering-runtime/`: `.github/workflows/`, Docker files, environment files, manifests, lockfiles, DevOps, infra, secrets, key, password, and permission surfaces; read `engineering-runtime/` SSOT before edits. -- `poc/`: `technical-spikes/`, `architecture-drafts/`, `uc-designs/`, `prototypes/`, `research-notes/`, and `validation-reports/`; read `poc/` SSOT before edits. -- `poc/` content is not formal implementation. -- Before editing `source-code/` from POC work, create or cite a formal design or task; implement from that artifact, not by copying experimental code directly. -- POC conclusions must include validation record source_refs under `poc/validation-reports/`; otherwise record `NEEDS_CLARIFICATION:poc-validation-record` in handoff. -- `source-code/`: `src/`, `app/`, `client/`, `server/`, `api/`, `lib/`, `services/`, `scripts/`, `commands/`, and `templates/`; read `source-code/` SSOT before edits. -- `test-code/`: `tests/`, `test/`, `e2e/`, `fixtures/`, `testdata/`, `test-results/`, and `coverage/`; read `test-code/` SSOT before edits. -- `other-tools/`: `tools/`, `.codegraph/`, MCP config files, `.vscode/`, and `.idea/`; read `other-tools/` SSOT before edits. -- If multiple trees match, read every matched SSOT and apply the highest authority non-conflicting rule. - -### Directory Tree Fallback - -- Use only when no fixed directory-tree SSOT matches, or the matched tree has no source_refs and the task scope is explicit. -- Treat scanned repository areas as descriptive context, not as approved path policy. -- Keep new or moved files aligned with existing nearby conventions unless the user supplies a different target. -- Record the matched `NEEDS_CLARIFICATION:` gap in handoff when placement is ambiguous. - -## agent-runtime - -- Repository capability layer: source-backed repository-local skills and MCP candidates only. -- Agent adapter layer: use explicit integration support when available; otherwise use generic fallback rules. -- Platform projection layer: apply only rules supported by the active target file. -- Repository-local skills: evidence only unless an explicit `agent-runtime/` SSOT source names them; read matching `SKILL.md` before planning or editing. -- MCP-backed external tools: indexed as MCP config candidates only; enumerate runtime tools before use. -- Repository config candidates are evidence only unless the active adapter supports them. -- If a matching skill lacks scope or validation guidance, ask for clarification before expanding writes. -- MCP default: read-only. -- MCP mutation: explicit user intent with target, action, and expected effect. -- Secrets: never log, never write. - -## Write Boundaries - -- Scope: active task only. -- Agent context files: edit only when the user explicitly asks for instruction changes. -- Protected files: implementation paths, CI configuration, MCP configuration, secrets, permissions, tool settings, and arbitrary repository paths outside the resolved write surface. -- Protected-file writes: explicit user request, named matching contract or regression test, and passing validation commands. - -## Handoff - -- changed files -- commands run -- validation result -- unresolved risks diff --git a/pyproject.toml b/pyproject.toml index fb7d50c8a5..1829093e34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,10 +44,7 @@ packages = ["src/specify_cli"] "extensions/arch" = "specify_cli/core_pack/extensions/arch" "extensions/discovery" = "specify_cli/core_pack/extensions/discovery" "extensions/git" = "specify_cli/core_pack/extensions/git" -"extensions/inception" = "specify_cli/core_pack/extensions/inception" -"extensions/intake" = "specify_cli/core_pack/extensions/intake" "extensions/preview" = "specify_cli/core_pack/extensions/preview" -"extensions/repository-governance" = "specify_cli/core_pack/extensions/repository-governance" "extensions/agent-context" = "specify_cli/core_pack/extensions/agent-context" "extensions/bug" = "specify_cli/core_pack/extensions/bug" # Bundled workflows (auto-installed during `specify init`) diff --git a/src/specify_cli/commands/init.py b/src/specify_cli/commands/init.py index 761f0add90..a4fef8a182 100644 --- a/src/specify_cli/commands/init.py +++ b/src/specify_cli/commands/init.py @@ -27,12 +27,7 @@ from .._utils import check_tool DEFAULT_BUNDLED_EXTENSIONS = ( - "discovery", "git", - "inception", - "intake", - "preview", - "repository-governance", ) DEFAULT_BUNDLED_PRESETS = ("workflow-preset",) diff --git a/src/specify_cli/component_standard.py b/src/specify_cli/component_standard.py index 2aa1c97645..45d2a441c4 100644 --- a/src/specify_cli/component_standard.py +++ b/src/specify_cli/component_standard.py @@ -1,7 +1,7 @@ """Executable coding-standard checks for Preset and Extension packages. The runtime manifest loaders answer whether a package can be installed. This -module adds repository-governance checks that answer whether a touched package +module adds repository component checks that answer whether a touched package follows the component coding standard. It deliberately has no network access and does not mutate the package being checked. """ diff --git a/templates/commands/checklist.md b/templates/commands/checklist.md index ee81933c1e..334bde61b4 100644 --- a/templates/commands/checklist.md +++ b/templates/commands/checklist.md @@ -91,12 +91,12 @@ Blocking items must declare their routing: ```markdown - [ ] CHK-SEC-003 [blocker:product-decision] [spec:FR-012] Is the guest export policy specified? -- [ ] CHK-VIS-004 [blocker:provider-evidence] [return:intake] Is the approved design revision referenced? +- [ ] CHK-VIS-004 [blocker:provider-evidence] [return:source-evidence] Is the approved design revision referenced? ``` - `product-decision` means clarify may ask the user and update `spec.md`. - `provider-evidence` means the item remains blocked and returns to the - responsible intake/provider workflow. + responsible source-evidence/provider workflow. - Checked items are satisfied requirement-quality assertions. Unchecked planning-gate items make that checklist `BLOCKED`. @@ -131,7 +131,7 @@ creating another file: When BLOCKED, list each blocker ID and route: - product decisions → `__SPECKIT_COMMAND_CLARIFY__` -- provider evidence → intake/provider workflow +- provider evidence → source-evidence/provider workflow ## Post-Execution Hooks diff --git a/templates/commands/clarify.md b/templates/commands/clarify.md index b4c508019b..2854a28414 100644 --- a/templates/commands/clarify.md +++ b/templates/commands/clarify.md @@ -79,7 +79,7 @@ Execution steps: - Prioritize unchecked `[blocker:product-decision]` items as clarification candidates. - Never turn `[blocker:provider-evidence]` items into product questions. - Keep them BLOCKED and report their `[return:intake]` route. + Keep them BLOCKED and report their `[return:source-evidence]` route. - Treat the legacy `checklists/behavior-testability.md` as non-authoritative even if it contains similar wording. @@ -245,7 +245,7 @@ Execution steps: atomically; do not append duplicate IDs, old blockers, or repeated Gate Status sections. - Provider-evidence blockers remain unchecked and keep the aggregate - BLOCKED until the provider/intake workflow supplies evidence. + BLOCKED until the provider/source-evidence workflow supplies evidence. - PASS requires every standard domain to be evaluated, every gate to carry the current spec revision, every applicable planning gate to be PASS, and every NOT_APPLICABLE result to include a reason. @@ -317,7 +317,7 @@ Report completion (after questioning loop ends or early termination): - [ ] Spec ambiguities identified and clarifications integrated into spec file - [ ] Spec quality checklist re-validated against updated spec (if `FEATURE_DIR/checklists/requirements.md` exists) - [ ] Affected requirement gates recomputed with stable IDs and current spec revision -- [ ] Provider-evidence blockers preserved and routed to intake +- [ ] Provider-evidence blockers preserved and routed to source evidence - [ ] Planning Readiness aggregated without creating a summary file - [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above - [ ] Completion reported to user with questions answered, sections touched, checklist status, and coverage summary diff --git a/templates/commands/plan.md b/templates/commands/plan.md index e855479ab1..d9d2e19010 100644 --- a/templates/commands/plan.md +++ b/templates/commands/plan.md @@ -43,7 +43,7 @@ Complete this read-only preflight before extension hooks or any planning write: Do not run hooks, create directories, copy/touch `plan.md`, or write any planning artifact. Route product-decision blockers to `__SPECKIT_COMMAND_CLARIFY__` and provider-evidence blockers to their - intake/provider workflow. + source-evidence/provider workflow. Planning Readiness is an in-memory aggregate. Never create `planning-readiness.md`. diff --git a/tests/extensions/inception/__init__.py b/tests/extensions/inception/__init__.py deleted file mode 100644 index 8b13789179..0000000000 --- a/tests/extensions/inception/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/extensions/inception/test_inception_extension.py b/tests/extensions/inception/test_inception_extension.py deleted file mode 100644 index 52c02294dd..0000000000 --- a/tests/extensions/inception/test_inception_extension.py +++ /dev/null @@ -1,94 +0,0 @@ -from pathlib import Path - -from specify_cli.extensions import ExtensionManifest - - -REPO_ROOT = Path(__file__).resolve().parents[3] -EXTENSION_DIR = REPO_ROOT / "extensions" / "inception" - - -def _read(rel_path: str) -> str: - return (EXTENSION_DIR / rel_path).read_text(encoding="utf-8") - - -def test_manifest_declares_product_and_architecture_migration_commands(): - manifest = ExtensionManifest(EXTENSION_DIR / "extension.yml") - - assert manifest.id == "inception" - assert manifest.name == "SDD Inception" - assert manifest.version == "2.0.0" - assert [command["name"] for command in manifest.commands] == [ - "speckit.inception.product", - "speckit.inception.arch", - ] - - -def test_product_command_keeps_product_only_outputs(): - product = _read("commands/speckit.inception.product.md") - - for expected in ( - "templates/product/uc-template.md", - "templates/product/wireflow-medium-template.html", - "templates/product/wireflow-high-template.html", - "inception/product/uc.md", - "inception/product/wireflow-medium.html", - "inception/product/wireflow-high.html", - ): - assert expected in product - - -def test_wireflow_uses_full_fidelity_names_not_abbreviated_files(): - combined = "\n".join( - [ - _read("extension.yml"), - _read("README.md"), - _read("commands/speckit.inception.product.md"), - _read("templates/product/wireflow-medium-template.html"), - _read("templates/product/wireflow-high-template.html"), - ] - ) - - assert "wireflow-medium.html" in combined - assert "wireflow-high.html" in combined - assert "wireflow-m.html" not in combined - assert "wireflow-h.html" not in combined - - -def test_architecture_command_is_write_free_migration_entrypoint(): - command = _read("commands/speckit.inception.arch.md") - readme = _read("README.md") - - for expected in ( - "INCEPTION_ARCH_COMMAND_RETIRED", - "__SPECKIT_COMMAND_CONSTITUTION__", - "Do not read `inception/product/uc.md` by default", - "Do not create `inception/arch/`", - "not a prerequisite or automatic authority", - ): - assert expected in command - - assert "write-free compatibility" in readme - assert "No conventional path is mandatory" in readme - - -def test_architecture_generation_templates_are_removed(): - arch_templates = EXTENSION_DIR / "templates" / "arch" - assert not list(arch_templates.glob("*")) - - combined = "\n".join( - [ - _read("extension.yml"), - _read("README.md"), - _read("commands/speckit.inception.arch.md"), - ] - ) - for retired in ( - "api-capability-template.md", - "api-poc-template.md", - "system-boundary-template.md", - "domain-model-template.md", - "arch-template.md", - "POC_CONFIRMATION_MISSING", - "POC_RUN_EVIDENCE_MISSING", - ): - assert retired not in combined diff --git a/tests/integrations/community_defaults.py b/tests/integrations/community_defaults.py index f0dbb8abfb..0db8d55c52 100644 --- a/tests/integrations/community_defaults.py +++ b/tests/integrations/community_defaults.py @@ -10,27 +10,14 @@ REPO_ROOT = Path(__file__).resolve().parents[2] DEFAULT_EXTENSION_IDS = ( - "discovery", "git", - "inception", - "intake", - "preview", - "repository-governance", ) DEFAULT_EXTENSION_COMMANDS = ( - "speckit.discovery.contract", "speckit.git.feature", "speckit.git.validate", "speckit.git.remote", "speckit.git.initialize", "speckit.git.commit", - "speckit.inception.product", - "speckit.inception.arch", - "speckit.intake.visual-design", - "speckit.intake.prd", - "speckit.intake.test-cases", - "speckit.preview.wireflow", - "speckit.repository-governance.generate", ) DEFAULT_PRESET_ID = "workflow-preset" DEFAULT_PRESET_COMMANDS = ( diff --git a/tests/integrations/test_cli.py b/tests/integrations/test_cli.py index 71c905ed41..749105c6bf 100644 --- a/tests/integrations/test_cli.py +++ b/tests/integrations/test_cli.py @@ -1001,8 +1001,8 @@ def test_git_extension_commands_registered_by_default(self, tmp_path): } assert expected_git_skills <= actual_git_skills - def test_community_extensions_and_workflow_preset_auto_installed(self, tmp_path): - """specify init installs default community extensions and the workflow preset.""" + def test_minimal_extensions_and_workflow_preset_auto_installed(self, tmp_path): + """specify init installs only the core git extension plus the workflow preset.""" from typer.testing import CliRunner from specify_cli import app @@ -1021,20 +1021,13 @@ def test_community_extensions_and_workflow_preset_auto_installed(self, tmp_path) assert result.exit_code == 0, f"init failed: {result.output}" - for extension_id in ("discovery", "git", "inception", "intake", "preview", "repository-governance"): - ext_dir = project / ".specify" / "extensions" / extension_id - assert (ext_dir / "extension.yml").exists(), f"{extension_id} was not installed" + assert (project / ".specify" / "extensions" / "git" / "extension.yml").exists() + for extension_id in ("discovery", "inception", "intake", "preview", "repository-governance"): + assert not (project / ".specify" / "extensions" / extension_id).exists() extensions_yml = project / ".specify" / "extensions.yml" hooks_data = yaml.safe_load(extensions_yml.read_text(encoding="utf-8")) - assert hooks_data["installed"] == [ - "discovery", - "git", - "inception", - "intake", - "preview", - "repository-governance", - ] + assert hooks_data["installed"] == ["git"] preset_dir = project / ".specify" / "presets" / "workflow-preset" assert (preset_dir / "preset.yml").exists(), "workflow-preset was not installed" @@ -1193,7 +1186,7 @@ def test_repeated_init_reapplies_active_workflow_preset( assert registry["presets"]["workflow-preset"]["enabled"] is True def test_git_default_keeps_community_defaults(self, tmp_path): - """Bundled community defaults install alongside the default git extension.""" + """Optional and retired community extensions are absent from fresh init.""" from typer.testing import CliRunner from specify_cli import app @@ -1214,8 +1207,7 @@ def test_git_default_keeps_community_defaults(self, tmp_path): assert (project / ".specify" / "extensions" / "git" / "extension.yml").exists() for extension_id in ("discovery", "inception", "intake", "preview", "repository-governance"): - ext_dir = project / ".specify" / "extensions" / extension_id - assert (ext_dir / "extension.yml").exists(), f"{extension_id} was not installed" + assert not (project / ".specify" / "extensions" / extension_id).exists() assert (project / ".specify" / "presets" / "workflow-preset" / "preset.yml").exists() diff --git a/tests/integrations/test_integration_catalog.py b/tests/integrations/test_integration_catalog.py index 0ac3f0fa9d..e05a327941 100644 --- a/tests/integrations/test_integration_catalog.py +++ b/tests/integrations/test_integration_catalog.py @@ -623,6 +623,33 @@ def test_upgrade_succeeds(self, tmp_path): assert result.exit_code == 0, result.output assert "upgraded successfully" in result.output + def test_upgrade_preserves_files_from_retired_extensions(self, tmp_path): + """Integration upgrades must not delete extension-owned project files.""" + from typer.testing import CliRunner + from specify_cli import app + + runner = CliRunner() + project = self._init_project(tmp_path, "copilot") + legacy_file = ( + project + / ".specify" + / "extensions" + / "intake" + / "user-maintained-evidence.md" + ) + legacy_file.parent.mkdir(parents=True, exist_ok=True) + legacy_file.write_text("preserve me\n", encoding="utf-8") + + old = os.getcwd() + try: + os.chdir(project) + result = runner.invoke(app, ["integration", "upgrade"], catch_exceptions=False) + finally: + os.chdir(old) + + assert result.exit_code == 0, result.output + assert legacy_file.read_text(encoding="utf-8") == "preserve me\n" + def test_upgrade_allows_preset_managed_copilot_prompt_rewrite(self, tmp_path): from typer.testing import CliRunner from specify_cli import app diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 827ee83aed..fcf3f4315f 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -229,9 +229,9 @@ def test_core_command_names_match_bundled_templates(self): assert CORE_COMMAND_NAMES == expected - @pytest.mark.parametrize("extension_id", ("arch", "preview", "repository-governance")) - def test_bundled_default_catalogs_match_manifest(self, extension_id): - """Bundled extension packages, catalogs, and default command lists stay aligned.""" + @pytest.mark.parametrize("extension_id", ("arch", "discovery", "preview")) + def test_bundled_opt_in_catalogs_match_manifest(self, extension_id): + """Bundled opt-in packages and catalogs stay aligned without becoming defaults.""" from tests.integrations.community_defaults import DEFAULT_EXTENSION_COMMANDS repo_root = Path(__file__).resolve().parent.parent @@ -245,7 +245,6 @@ def test_bundled_default_catalogs_match_manifest(self, extension_id): bundled_entry = bundled_catalog["extensions"][extension_id] community_entry = community_catalog["extensions"][extension_id] manifest_repository = manifest.data["extension"]["repository"] - manifest_commands = tuple(command["name"] for command in manifest.commands) default_commands = tuple( command for command in DEFAULT_EXTENSION_COMMANDS @@ -259,39 +258,8 @@ def test_bundled_default_catalogs_match_manifest(self, extension_id): assert entry["repository"] == manifest_repository assert entry["provides"]["commands"] == len(manifest.commands) - assert community_entry["download_url"].endswith(f"/refs/tags/v{manifest.version}.zip") - if extension_id == "arch": - # Architecture remains an opt-in extension; catalog parity does not - # imply that init registers its commands by default. - assert default_commands == () - else: - assert default_commands == manifest_commands - - def test_bundled_inception_catalog_matches_manifest(self): - """Inception is bundled locally but not mirrored into the community catalog.""" - from tests.integrations.community_defaults import DEFAULT_EXTENSION_COMMANDS - - repo_root = Path(__file__).resolve().parent.parent - manifest = ExtensionManifest(repo_root / "extensions" / "inception" / "extension.yml") - bundled_catalog = json.loads( - (repo_root / "extensions" / "catalog.json").read_text(encoding="utf-8") - ) - bundled_entry = bundled_catalog["extensions"]["inception"] - manifest_repository = manifest.data["extension"]["repository"] - manifest_commands = tuple(command["name"] for command in manifest.commands) - default_commands = tuple( - command - for command in DEFAULT_EXTENSION_COMMANDS - if command.startswith("speckit.inception.") - ) - - assert bundled_entry["name"] == manifest.name - assert bundled_entry["version"] == manifest.version - assert bundled_entry["description"] == manifest.description - assert bundled_entry["repository"] == manifest_repository - assert bundled_entry["bundled"] is True - assert bundled_entry["provides"]["commands"] == len(manifest.commands) - assert default_commands == manifest_commands + assert community_entry["download_url"].endswith(".zip") + assert default_commands == () def test_missing_required_field(self, temp_dir): """Test manifest missing required field.""" diff --git a/tests/test_presets.py b/tests/test_presets.py index 40754b148e..b232f5935a 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -4879,29 +4879,24 @@ def test_community_smoke_checks_wheel_assets_and_extension_dev_reinstall(self): "PLAN_OUTPUT_READY", ): assert marker in verify_run - assert ( - 'for extension_id in arch discovery inception intake preview repository-governance; do' - in verify_run - ) + assert "test -f .specify/extensions/git/extension.yml" in verify_run + for extension_id in ( + "discovery", + "inception", + "intake", + "preview", + "repository-governance", + ): + assert f"test ! -e .specify/extensions/{extension_id}" in verify_run + assert "for extension_id in arch discovery preview; do" in verify_run assert "test -f .specify/extensions/arch/extension.yml" not in verify_run - assert ( - '/tmp/specify-community-smoke-venv/bin/specify extension remove "$extension_id" --force' - in verify_run - ) - assert "find .claude/skills -maxdepth 1 -type d -name 'speckit-discovery-*'" in verify_run - assert "find .claude/skills -maxdepth 1 -type d -name 'speckit-inception-*'" in verify_run - assert "find .claude/skills -maxdepth 1 -type d -name 'speckit-intake-*'" in verify_run assert "speckit-preview-wireflow" in verify_run - assert "test ! -d .claude/skills/speckit-preview-wireflow" in verify_run + assert "test ! -e .claude/skills/speckit-preview-wireflow" in verify_run assert "test -f .claude/skills/speckit-preview-wireflow/SKILL.md" in verify_run assert ( 'grep -q "Input-to-Design Synthesis Pass" .claude/skills/speckit-preview-wireflow/SKILL.md' in verify_run ) - assert ( - "test ! -d .claude/skills/speckit-repository-governance-generate" - in verify_run - ) assert ( '/tmp/specify-community-smoke-venv/bin/specify extension add "$GITHUB_WORKSPACE/extensions/$extension_id" --dev' in verify_run @@ -4922,35 +4917,11 @@ def test_community_smoke_checks_wheel_assets_and_extension_dev_reinstall(self): 'test -f .claude/skills/speckit-discovery-contract/SKILL.md' in verify_run ) - assert ( - 'test -f .claude/skills/speckit-inception-product/SKILL.md' - in verify_run - ) - assert ( - 'test -f .claude/skills/speckit-inception-arch/SKILL.md' - in verify_run - ) - assert ( - 'grep -q "INCEPTION_ARCH_COMMAND_RETIRED" .claude/skills/speckit-inception-arch/SKILL.md' - in verify_run - ) - assert ( - 'test -f .claude/skills/speckit-intake-visual-design/SKILL.md' - in verify_run - ) - assert ( - 'grep -q "visual design intake" .claude/skills/speckit-intake-visual-design/SKILL.md' - in verify_run - ) assert "specify extension add discovery" in verify_run assert ( 'test -f .claude/skills/speckit-discovery-contract/SKILL.md' in verify_run ) - assert ( - 'test -f .claude/skills/speckit-repository-governance-generate/SKILL.md' - in verify_run - ) def test_bundled_preset_download_raises_error(self, project_dir): """download_pack raises PresetError for bundled presets without download_url.""" diff --git a/tests/test_requirement_gate_workflow_contract.py b/tests/test_requirement_gate_workflow_contract.py index 039d862486..1a12d81029 100644 --- a/tests/test_requirement_gate_workflow_contract.py +++ b/tests/test_requirement_gate_workflow_contract.py @@ -38,7 +38,7 @@ def test_requirement_gate_metadata_and_blocker_routes_are_stable() -> None: assert field in checklist assert "[blocker:product-decision]" in command assert "[blocker:provider-evidence]" in command - assert "[return:intake]" in command + assert "[return:source-evidence]" in command def test_specify_hands_off_to_checklist_instead_of_plan() -> None: