diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 68c4aeb255..901a5ff1e3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -10,6 +10,18 @@ - [ ] Ran existing tests with `uv sync && uv run pytest` - [ ] Tested with a sample project (if applicable) +## Community Catalog Submission + + + +- Submission route: +- Catalog type: +- Source repository: +- Source version: +- Source commit: +- Download URL: +- Related issue: + ## AI Disclosure @@ -19,4 +31,3 @@ - [ ] I **did** use AI assistance (describe below) - diff --git a/.github/workflows/community-integration.yml b/.github/workflows/community-integration.yml new file mode 100644 index 0000000000..b29c2b27d2 --- /dev/null +++ b/.github/workflows/community-integration.yml @@ -0,0 +1,43 @@ +name: Community Integration Validation + +permissions: + contents: read + +on: + pull_request: + workflow_dispatch: + +jobs: + validate-community-integration: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + with: + python-version: "3.11" + + - name: Write PR body + if: github.event_name == 'pull_request' + shell: pwsh + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: | + Set-Content -LiteralPath .pr-body.md -Value $env:PR_BODY -Encoding utf8 + + - name: Validate community integration workflow + shell: pwsh + run: | + $argsList = @( + 'scripts/community/validate_integration.py', + '--branch', + '${{ github.head_ref }}' + ) + if (Test-Path -LiteralPath .pr-body.md) { + $argsList += @('--pr-body-file', '.pr-body.md') + } + python @argsList diff --git a/.github/workflows/community-smoke.yml b/.github/workflows/community-smoke.yml index bbae56f1df..a50296c44f 100644 --- a/.github/workflows/community-smoke.yml +++ b/.github/workflows/community-smoke.yml @@ -48,6 +48,7 @@ jobs: test -f .specify/extensions/arch/extension.yml test -f .specify/extensions/preview/extension.yml test -f .specify/extensions/discovery/extension.yml + test -f .specify/extensions/intake/extension.yml test -f .specify/extensions/repository-governance/extension.yml test -f .specify/presets/workflow-preset/preset.yml /tmp/specify-community-smoke-venv/bin/python - <<'PY' @@ -92,7 +93,19 @@ jobs: test -f .specify/presets/workflow-preset/.composed/speckit.plan.md test -f .specify/presets/workflow-preset/.composed/speckit.tasks.md - test -f .claude/skills/speckit-arch-generate/SKILL.md + for arch_skill in \ + speckit-arch-scenario-generate \ + speckit-arch-logical-generate \ + speckit-arch-process-generate \ + speckit-arch-development-generate \ + speckit-arch-physical-generate \ + speckit-arch-scenario-reverse \ + speckit-arch-logical-reverse \ + speckit-arch-process-reverse \ + speckit-arch-development-reverse \ + speckit-arch-physical-reverse; do + test -f ".claude/skills/$arch_skill/SKILL.md" + done for preview_skill in \ speckit-preview-low-md \ speckit-preview-low-html \ @@ -103,6 +116,9 @@ jobs: test -f ".claude/skills/$preview_skill/SKILL.md" done test -f .claude/skills/speckit-discovery-feasibility/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-refresh/SKILL.md test -f .claude/skills/speckit-specify/SKILL.md test -f .claude/skills/speckit-clarify/SKILL.md @@ -120,13 +136,12 @@ jobs: grep -q "Vertical Planner Agent" .claude/skills/speckit-implement/SKILL.md grep -q "Worker Agent" .claude/skills/speckit-implement/SKILL.md - for extension_id in arch discovery preview repository-governance; do + for extension_id in arch discovery 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 arch) - test ! -d .claude/skills/speckit-arch-generate - test ! -d .claude/skills/speckit-arch-reverse + test "$(find .claude/skills -maxdepth 1 -type d -name 'speckit-arch-*' | wc -l)" -eq 0 ;; preview) for preview_skill in \ @@ -142,19 +157,22 @@ jobs: discovery) test "$(find .claude/skills -maxdepth 1 -type d -name 'speckit-discovery-*' | 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-refresh ;; esac done - for extension_id in arch discovery preview repository-governance; do + for extension_id in arch discovery intake preview repository-governance; do /tmp/specify-community-smoke-venv/bin/specify extension add "$GITHUB_WORKSPACE/extensions/$extension_id" --dev case "$extension_id" in arch) - test -f .claude/skills/speckit-arch-generate/SKILL.md - test -f .claude/skills/speckit-arch-reverse/SKILL.md - grep -q "4+1 architecture workflow" .claude/skills/speckit-arch-generate/SKILL.md + test -f .claude/skills/speckit-arch-scenario-generate/SKILL.md + test -f .claude/skills/speckit-arch-scenario-reverse/SKILL.md + grep -q "scenario view" .claude/skills/speckit-arch-scenario-generate/SKILL.md ;; preview) for preview_skill in \ @@ -172,6 +190,12 @@ jobs: test -f .claude/skills/speckit-discovery-feasibility/SKILL.md grep -q "evidence-backed feasibility study" .claude/skills/speckit-discovery-feasibility/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-refresh/SKILL.md grep -q "Repository Governance Generate/Update" .claude/skills/speckit-repository-governance-refresh/SKILL.md diff --git a/.gitignore b/.gitignore index 8977cd0cb6..ad5dae4e94 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,11 @@ __pycache__/ *.py[cod] *$py.class *.so +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ .Python build/ develop-eggs/ @@ -27,6 +32,12 @@ env/ .venv .venv*/ +# Local caches and generated artifacts +.tmp/ +tmp/ +temp/ +.worktrees/ + # IDE .vscode/ .idea/ diff --git a/AGENTS.md b/AGENTS.md index 4f0c9912a8..9c82ff1f39 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,70 @@ The toolkit supports multiple AI coding assistants, allowing teams to use their --- +## Local Community Integration Workflow + +This local checkout uses `spec-kit` as the fork of `github/spec-kit` that +integrates community extensions and presets. Sibling directories named +`spec-kit-*` are treated as source projects for community extensions or presets, +not as extra checkouts of this repository unless `git worktree list` proves they +are linked worktrees. + +### Repository roles + +- `C:\Users\24598\Documents\github\spec-kit` is the integration repository. + Keep its main working directory on `main`. +- `C:\Users\24598\Documents\github\spec-kit-*` directories are source + repositories for community extensions or presets. Develop, release, and tag + source packages there. +- Use temporary linked worktrees for concurrent integration PRs. Remove them + with `git worktree remove` and `git worktree prune` after the PR is merged, + closed, or abandoned. + +### Community submission routes + +Spec Kit supports two community submission routes: + +1. **PR template route**: the contributor opens a direct PR. The PR body must + identify `Submission route: pr-template` and include source repository, + source version, source commit, changed catalog type, and validation evidence. +2. **Issue template route**: the contributor files an extension or preset issue + using `.github/ISSUE_TEMPLATE/extension_submission.yml` or + `.github/ISSUE_TEMPLATE/preset_submission.yml`. The catalog workflow creates + the integration PR from the issue. The PR body must identify + `Submission route: issue-template` and include `Closes #`. + +Agents must preserve the route. Do not convert an issue-template submission into +a direct PR-template submission unless the maintainer explicitly asks for that +reroute. + +### Required local steps for agents + +When integrating a community extension or preset: + +1. Start from `spec-kit` on `main`, fetch the upstream default branch, and make a + short-lived `community/*` branch or linked worktree. +2. Record source-backed metadata: source repository URL, release version, + source commit SHA, download URL, catalog type (`extension` or `preset`), and + submission route (`pr-template` or `issue-template`). +3. Update only the catalog, bundled extension or preset snapshot, docs, and + tests required by the integration. Do not develop the source project inside + this repository unless the integration requires a bundled snapshot change. +4. Run `scripts/community/validate-integration.ps1` before opening or updating + the PR. +5. Run targeted tests for touched surfaces, and run `uv run pytest` when shared + catalog, extension, preset, or init behavior changes. +6. Clean temporary linked worktrees with + `scripts/community/cleanup-worktrees.ps1` after they are no longer needed. + +Community integration branches follow: + +``` +community/- # issue-template route +community/ # pr-template route without an issue +``` + +--- + ## Integration Architecture Each AI agent is a self-contained **integration subpackage** under `src/specify_cli/integrations//`. The subpackage exposes a single class that declares all metadata and inherits setup/teardown logic from a base class. Built-in integrations are then instantiated and added to the global `INTEGRATION_REGISTRY` by `src/specify_cli/integrations/__init__.py` via `_register_builtins()`. diff --git a/README.md b/README.md index 9e5fa63446..31626fcd30 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,8 @@ specify integration list | 能力 | 来源 | 关键命令或产物 | 解决什么问题 | | --- | --- | --- | --- | -| ARCH SSOT | `arch` 扩展 | `/speckit.arch.generate`、`/speckit.arch.reverse`、`.specify/memory/architecture*.md` | 把项目级 4+1 架构视图沉淀为稳定记忆,避免每个 feature 重新猜边界、约束和部署假设。 | +| ARCH SSOT | `arch` 扩展 | `/speckit.arch.scenario-generate`、`/speckit.arch.logical-generate`、`/speckit.arch.*-reverse`、`.specify/memory/architecture*.md` | 把项目级 4+1 架构视图沉淀为稳定记忆,避免每个 feature 重新猜边界、约束和部署假设。 | +| Intake 证据归一化 | `intake` 扩展 | `/speckit.intake.visual-design`、`/speckit.intake.prd`、`/speckit.intake.test-cases` | 把 PRD、设计和测试用例证据整理成 SDD 可消费的 intake artifact,再进入 specify/plan。 | | 仓库治理规范 | `repository-governance` 扩展 | `/speckit.repository-governance.refresh`、受管 `SPECKIT GOVERNANCE` 段 | 统一 agent 的 SSOT 读取顺序、目录责任、平台适配和仓库事实证据。 | | UI/UX 需求规格 | `workflow-preset` + `preview` 扩展 | `spec.md` 用户旅程、`behavior/uif.intent.json`、`contracts/uif/`、`/speckit.preview.mid-html` | 先把界面状态、断点、用户路径和交互假设写成规格/契约,再生成可打开的预览产物评审。 | | BDD 引入 | `workflow-preset` | `/speckit.checklist`、`checklists/behavior-testability.md`、`contracts/bdd/` | 在规划前检查 Given/When/Then、可观察结果、边界和 NFR 声明,避免不可验证需求进入实现。 | @@ -168,6 +169,7 @@ specify integration list | 扩展 | 默认状态 | 你会用它做什么 | | --- | --- | --- | | `arch` | `specify init` 默认安装 | 生成或反向生成项目级 4+1 架构视图,形成 `.specify/memory/architecture*.md` 架构记忆。 | +| `intake` | `specify init` 默认安装 | 归一化 PRD、视觉设计和测试用例证据,生成下游 SDD 命令可读取的 intake artifact。 | | `preview` | `specify init` 默认安装 | 根据当前 feature 的规格和计划生成低/中/高保真 Markdown wireflow 或自包含 HTML 预览,用于实现前验证 UI 和交互假设。 | | `repository-governance` | `specify init` 默认安装 | 生成 Repository Governance Framework 治理说明,包含垂直 SSOT 注册、读取顺序、缺失 SSOT 处理和仓库事实证据。 | | `git` | 初始化时默认安装,传 `--no-git` 可跳过 | 初始化 Git、创建 feature branch、校验分支、检测 remote,并可配置自动提交。 | @@ -178,6 +180,7 @@ specify integration list ```bash specify extension add arch +specify extension add intake specify extension add preview specify extension add repository-governance specify extension add git @@ -193,8 +196,11 @@ specify extension info arch 扩展命令示例: ```text -/speckit.arch.generate -/speckit.arch.reverse +/speckit.arch.scenario-generate +/speckit.arch.logical-generate +/speckit.arch.scenario-reverse +/speckit.intake.visual-design +/speckit.intake.prd /speckit.preview.low-md /speckit.preview.mid-html /speckit.preview.high-html @@ -226,8 +232,9 @@ specify preset info workflow-preset ### 什么时候用这些能力 -- 新项目或架构正在变化:先跑 `/speckit.arch.generate`,让后续计划有稳定架构上下文。 -- 接手旧仓库:跑 `/speckit.arch.reverse`,先从仓库事实反推架构记忆。 +- 新项目或架构正在变化:按 scenario/logical/process/development/physical 顺序跑 `/speckit.arch.*-generate`,让后续计划有稳定架构上下文。 +- 接手旧仓库:按 scenario/logical/process/development/physical 顺序跑 `/speckit.arch.*-reverse`,先从仓库事实反推架构记忆。 +- 已有 PRD、设计稿或测试用例:先跑 `/speckit.intake.prd`、`/speckit.intake.visual-design` 或 `/speckit.intake.test-cases`,把外部证据归一化后再进入 `/speckit.specify`。 - 团队使用多个 agent 或新人频繁接手:跑 `/speckit.repository-governance.refresh`,把目录责任、SSOT 边界和 agent 执行规则写入上下文。 - 希望引入 BDD:保留默认 `workflow-preset`,让 `/speckit.checklist` 在规划前检查 BDD readiness,让 `/speckit.plan` 生成 BDD/UIF/fixture 行为草稿和正式契约。 - 做前端或交互功能:在 `/speckit.specify` 或 `/speckit.plan` 后跑 `/speckit.preview.mid-html` 或对应保真的 `*-md`/`*-html` 命令,先看预览再实现。 @@ -311,7 +318,11 @@ AGENTS.md ```text /speckit.constitution -/speckit.arch.generate +/speckit.arch.scenario-generate +/speckit.arch.logical-generate +/speckit.arch.process-generate +/speckit.arch.development-generate +/speckit.arch.physical-generate /speckit.specify /speckit.clarify /speckit.preview.mid-html @@ -325,7 +336,11 @@ AGENTS.md ```text /speckit.constitution -/speckit.arch.reverse +/speckit.arch.scenario-reverse +/speckit.arch.logical-reverse +/speckit.arch.process-reverse +/speckit.arch.development-reverse +/speckit.arch.physical-reverse /speckit.repository-governance.refresh /speckit.specify /speckit.clarify diff --git a/docs/.gitignore b/docs/.gitignore index 68fec76e37..77b1e80873 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -6,4 +6,19 @@ obj/ # Temporary files *.tmp *.log +.tmp/ +tmp/ +temp/ +# Local caches +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ +.venv/ +venv/ +.DS_Store +Thumbs.db diff --git a/docs/quickstart.md b/docs/quickstart.md index ade342b7ec..e9fb5b061b 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -14,7 +14,11 @@ ```text /speckit.constitution -/speckit.arch.generate +/speckit.arch.scenario-generate +/speckit.arch.logical-generate +/speckit.arch.process-generate +/speckit.arch.development-generate +/speckit.arch.physical-generate /speckit.repository-governance.refresh /speckit.specify /speckit.clarify @@ -26,7 +30,7 @@ /speckit.implement ``` -接手旧仓库时,把 `/speckit.arch.generate` 换成 `/speckit.arch.reverse`,先从仓库事实反向生成架构 SSOT。 +接手旧仓库时,把五个 `/speckit.arch.*-generate` 换成对应的 `/speckit.arch.*-reverse`,先从仓库事实反向生成架构 SSOT。 小实验可以安装 `lean` 预设后走轻量路径: @@ -125,13 +129,21 @@ specify integration list 新项目或架构正在重塑时: ```text -/speckit.arch.generate +/speckit.arch.scenario-generate +/speckit.arch.logical-generate +/speckit.arch.process-generate +/speckit.arch.development-generate +/speckit.arch.physical-generate ``` 接手已有仓库时: ```text -/speckit.arch.reverse +/speckit.arch.scenario-reverse +/speckit.arch.logical-reverse +/speckit.arch.process-reverse +/speckit.arch.development-reverse +/speckit.arch.physical-reverse ``` 主要产物: diff --git a/extensions/arch/.gitattributes b/extensions/arch/.gitattributes new file mode 100644 index 0000000000..dfdb8b771c --- /dev/null +++ b/extensions/arch/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/extensions/arch/.gitignore b/extensions/arch/.gitignore index 97255ff7f9..8a1814e127 100644 --- a/extensions/arch/.gitignore +++ b/extensions/arch/.gitignore @@ -1,5 +1,23 @@ .DS_Store +Thumbs.db .pytest_cache/ __pycache__/ *.py[cod] +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ +.venv/ +venv/ +dist/ +build/ +*.egg-info/ +*.zip +*.log +.env +.env.* +!.env.example +.tmp/ +tmp/ +temp/ .worktrees/ diff --git a/extensions/arch/AGENTS.md b/extensions/arch/AGENTS.md new file mode 100644 index 0000000000..b059ed99a1 --- /dev/null +++ b/extensions/arch/AGENTS.md @@ -0,0 +1,35 @@ +# Architecture Workflow Extension + +This repository is a Spec Kit community extension source project for `arch`. + +## Project Shape + +- `extension.yml` is the extension manifest and must stay aligned with declared files. +- `commands/` contains Spec Kit command templates. +- `templates/` contains architecture artifact templates. +- `schemas/` contains architecture artifact schemas. +- `scripts/` contains setup helpers. +- `tests/repository-first-contract.sh` is the main contract test. +- `presets/arch-governance/` is a nested source preset owned by this repository. + +## Source Workflow + +- Develop extension and nested preset source changes in this repository. +- Release versioned source artifacts from this repository. +- Keep `README.md`, `CHANGELOG.md`, `CATALOG-SUBMISSION.md`, manifests, and tests aligned. +- Run `bash tests/repository-first-contract.sh` after behavior, command, template, schema, or setup-script 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/arch/CATALOG-SUBMISSION.md b/extensions/arch/CATALOG-SUBMISSION.md index 634053c0ca..8674f141ba 100644 --- a/extensions/arch/CATALOG-SUBMISSION.md +++ b/extensions/arch/CATALOG-SUBMISSION.md @@ -2,27 +2,29 @@ Extension ID: arch Name: Architecture Workflow -Version: 1.1.0 -Description: Generate or reverse project-level 4+1 architecture view artifacts and synthesis +Version: 1.2.1 +Description: Generate or reverse project-level 4+1 architecture views as separate commands Author: bigsmartben Repository URL: https://github.com/bigsmartben/spec-kit-arch -Download URL: https://github.com/bigsmartben/spec-kit-arch/archive/refs/tags/v1.1.0.zip +Download URL: https://github.com/bigsmartben/spec-kit-arch/archive/refs/tags/v1.2.1.zip Documentation URL: https://github.com/bigsmartben/spec-kit-arch#readme License: MIT Required Spec Kit version: >=0.8.10.dev0 -Commands count: 2 +Commands count: 10 Hooks count: 0 Tags: architecture, 4plus1, workflow, design Key features: -- Generates or refreshes six architecture memory artifacts. -- Provides `/speckit.arch.reverse` for evidence-backed architecture reconstruction from ordinary historical repositories. +- Provides `.arch` namespaced commands for each 4+1 view: scenario, logical, process, development, and physical. +- Provides separate forward-generation and reverse-generation commands for every view. - Records reverse workflow evidence in `.specify/memory/architecture-repo-facts.md`. - Consumes optional repository-first dependency matrices and module invocation specs as reverse workflow evidence. -- Guides scenario, logical, process, development, physical, and synthesis views. +- Allows teams to update one architecture view at a time while preserving the cross-view synthesis boundary. +- Defines synthesis readiness, repo-facts merge behavior, and source traceability expectations for repeatable command runs. +- Ships a schema-backed artifact contract for structured validation before Markdown rendering. - Restricts writes to documented `.specify/memory/architecture*.md` files, including repo facts for reverse generation. Testing performed: - Local development install with `specify extension add --dev /home/administrator/github/spec-kit-arch`. - Bash setup helper verified with `.specify/extensions/arch/scripts/bash/setup-arch.sh --json`. -- Release ZIP install to verify after publishing `v1.1.0`: `specify extension add arch --from https://github.com/bigsmartben/spec-kit-arch/archive/refs/tags/v1.1.0.zip`. +- Release ZIP install to verify after publishing `v1.2.1`: `specify extension add arch --from https://github.com/bigsmartben/spec-kit-arch/archive/refs/tags/v1.2.1.zip`. diff --git a/extensions/arch/CHANGELOG.md b/extensions/arch/CHANGELOG.md index 7a4b25a0fa..6837633cff 100644 --- a/extensions/arch/CHANGELOG.md +++ b/extensions/arch/CHANGELOG.md @@ -2,10 +2,16 @@ ## Unreleased -- Add `/speckit.arch.reverse` for repo-facts-first reverse generation of architecture artifacts from ordinary historical repositories. -- Teach `/speckit.arch.reverse` to consume optional `.specify/memory/repository-first/` dependency matrices and module invocation specs as evidence for development-view governance. +- Use CLI-compatible command names such as `/speckit.arch.scenario-generate` and `/speckit.arch.scenario-reverse`. +- Clarify command bootstrap boundaries, synthesis readiness checks, reverse repo-facts merge behavior, and source traceability expectations. +- Replace the two broad architecture commands with ten `.arch`-namespaced commands: forward and reverse generation for scenario, logical, process, development, and physical views. +- Keep synthesis refresh optional for per-view commands so each command owns one primary 4+1 artifact. +- Add reverse commands for repo-facts-first generation of architecture artifacts from ordinary historical repositories. +- Teach reverse commands to consume optional `.specify/memory/repository-first/` dependency matrices and module invocation specs as evidence for development-view governance. +- Add a schema-backed architecture artifact contract and require commands to validate JSON-compatible working models before Markdown rendering. +- Separate command and template responsibilities: commands now own extraction, classification, validation, merge policy, and write boundaries; templates only define Markdown layout. - Add the `arch-governance` preset to wrap only the core `/speckit.plan` workflow with architecture SSOT grounding. -- Keep `/speckit.arch.generate` independent from core workflow commands by removing downstream planning language from architecture artifacts. +- Keep architecture generation commands independent from core workflow commands by removing downstream planning language from architecture artifacts. ## v1.0.0 diff --git a/extensions/arch/README.md b/extensions/arch/README.md index 8f6207159d..fd610c3334 100644 --- a/extensions/arch/README.md +++ b/extensions/arch/README.md @@ -29,7 +29,7 @@ specify extension info arch Community catalog entries may be discovery-only. Install the published release directly from GitHub: ```bash -specify extension add arch --from https://github.com/bigsmartben/spec-kit-arch/archive/refs/tags/v1.1.0.zip +specify extension add arch --from https://github.com/bigsmartben/spec-kit-arch/archive/refs/tags/v1.2.1.zip ``` Install from a local development checkout: @@ -46,33 +46,47 @@ After installation, the extension is copied under: ## Commands -The extension provides two commands: +The extension id is `arch`, and each command uses `.arch` as the command namespace. The extension provides ten commands: one forward-generation command and one reverse-generation command for each 4+1 architecture view. ```text -/speckit.arch.generate -/speckit.arch.reverse +/speckit.arch.scenario-generate +/speckit.arch.logical-generate +/speckit.arch.process-generate +/speckit.arch.development-generate +/speckit.arch.physical-generate +/speckit.arch.scenario-reverse +/speckit.arch.logical-reverse +/speckit.arch.process-reverse +/speckit.arch.development-reverse +/speckit.arch.physical-reverse ``` -Choose the command based on where your architecture knowledge should come from. +Choose the direction based on where architecture knowledge should come from, then choose the view you want to update. -| Situation | Use | What it does | -| --- | --- | --- | -| You are working in a Spec Kit project with intended product/use-case context | `/speckit.arch.generate` | Generates or refreshes the architecture SSOT from scenario semantics and existing architecture memory. It may read `.specify/memory/uc.md` as optional background. | -| You are onboarding an ordinary historical repository without reliable Spec Kit feature context | `/speckit.arch.reverse` | First records observable repository evidence, then derives the architecture SSOT from that evidence. | +| Direction | Use when | Evidence source | Writes | +| --- | --- | --- | --- | +| `generate` | You are working from intended product/use-case context or existing architecture memory | User input, current architecture views, optional `.specify/memory/uc.md` for scenario only | The selected architecture view, with synthesis refreshed only when all views are coherent | +| `reverse` | You are onboarding or documenting an existing repository | Observable repository facts recorded in `.specify/memory/architecture-repo-facts.md` | Repo facts plus the selected architecture view, with synthesis refreshed only when all views are coherent | -### `/speckit.arch.generate` +| View | Forward command | Reverse command | Main artifact | +| --- | --- | --- | --- | +| Scenario | `/speckit.arch.scenario-generate` | `/speckit.arch.scenario-reverse` | `.specify/memory/architecture-scenario-view.md` | +| Logical | `/speckit.arch.logical-generate` | `/speckit.arch.logical-reverse` | `.specify/memory/architecture-logical-view.md` | +| Process | `/speckit.arch.process-generate` | `/speckit.arch.process-reverse` | `.specify/memory/architecture-process-view.md` | +| Development | `/speckit.arch.development-generate` | `/speckit.arch.development-reverse` | `.specify/memory/architecture-development-view.md` | +| Physical | `/speckit.arch.physical-generate` | `/speckit.arch.physical-reverse` | `.specify/memory/architecture-physical-view.md` | -Run this when you already know what the project is meant to become, or when Spec Kit use-case context exists and you want to make architecture intent explicit before planning. +### Forward Generation -```text -/speckit.arch.generate -``` +Run `*.generate` commands when you already know what the project is meant to become, or when Spec Kit use-case context exists and you want to make architecture intent explicit before planning. -The command works from the scenario view outward: +Recommended order: -1. Establishes the architecture framing for the current pass. -2. Creates or refreshes the scenario, logical, process, development, and physical views. -3. Updates `.specify/memory/architecture.md` as the cross-view synthesis. +1. `/speckit.arch.scenario-generate` +2. `/speckit.arch.logical-generate` +3. `/speckit.arch.process-generate` +4. `/speckit.arch.development-generate` +5. `/speckit.arch.physical-generate` Use it to answer questions like: @@ -81,15 +95,25 @@ Use it to answer questions like: - Which runtime, development, or deployment constraints are already architecture decisions? - Which architecture gaps must stay explicit until the team supplies more information? -### `/speckit.arch.reverse` +Each forward command populates only its selected view. Its setup bootstrap may create missing placeholder files for the architecture memory set, but the command must not populate non-target views or use `.specify/memory/architecture-repo-facts.md` as input. It may refresh `.specify/memory/architecture.md` only after all five views contain coherent, non-placeholder architecture content. -Run this when the repository already exists but the architecture SSOT does not. +Each command also loads `.specify/extensions/arch/schemas/architecture-artifacts.schema.json` as the structural contract for architecture artifacts. Commands own evidence extraction, classification, validation, and write boundaries; templates own only the Markdown rendering layout. -```text -/speckit.arch.reverse -``` +### Reverse Generation + +Run `*.reverse` commands when the repository already exists but the architecture SSOT does not. -The command inspects repository evidence first, writes `.specify/memory/architecture-repo-facts.md`, then derives the five 4+1 views and synthesis from those facts. +Recommended order: + +1. `/speckit.arch.scenario-reverse` +2. `/speckit.arch.logical-reverse` +3. `/speckit.arch.process-reverse` +4. `/speckit.arch.development-reverse` +5. `/speckit.arch.physical-reverse` + +Each reverse command inspects repository evidence first, writes or refreshes `.specify/memory/architecture-repo-facts.md`, then derives the selected 4+1 view from those facts. The repo facts file is cumulative: reverse commands preserve existing non-placeholder facts outside their evidence focus unless cited evidence is removed, contradicted, or superseded, and they record the reason for any replacement or downgrade. A reverse command may refresh `.specify/memory/architecture.md` after all five views contain coherent, evidence-backed architecture content. + +Reverse commands validate both the repo-facts working model and the target-view working model against the same schema contract before rendering Markdown. It is useful for: @@ -97,7 +121,19 @@ It is useful for: - Reconstructing architecture intent from README files, tests, entry points, package layout, routes, workers, CI/CD, configuration, and deployment clues. - Making uncertainty visible when the repository does not prove actors, runtime ownership, deployment topology, or business meaning. -If `.specify/memory/repository-first/` exists, the reverse command also treats those files as evidence inputs. It summarizes dependency matrices and module invocation specs into architecture-level constraints, dependency rules, gaps, and review triggers. +If `.specify/memory/repository-first/` exists, reverse commands also treat those files as evidence inputs. They summarize dependency matrices and module invocation specs into architecture-level constraints, dependency rules, gaps, and review triggers. + +### Synthesis Refresh + +Commands refresh `.specify/memory/architecture.md` only when every 4+1 view is synthesis-ready: + +- The view file exists. +- It no longer contains `NEEDS ARCH UPDATE`. +- It contains concrete architecture conclusions or explicit gaps in required sections, not only template headings. +- Explicit gaps may remain, but gaps alone do not make a view synthesis-ready. +- Cross-view terminology is coherent enough to synthesize without inventing content. + +If any view is missing, placeholder-only, stale, or inconsistent, commands leave `.specify/memory/architecture.md` unchanged and report the missing or not-ready views. ## Files Written @@ -112,13 +148,21 @@ The architecture SSOT files are: .specify/memory/architecture-physical-view.md ``` -`/speckit.arch.reverse` also writes its evidence layer: +Reverse commands also write their evidence layer: ```text .specify/memory/architecture-repo-facts.md ``` -`/speckit.arch.generate` writes the six architecture SSOT files. Its setup may create an empty `.specify/memory/architecture-repo-facts.md` placeholder for reverse workflow compatibility, but generate does not use that file as input. +Forward `*.generate` commands populate only the selected view. Their setup may create placeholder architecture memory files, including an empty `.specify/memory/architecture-repo-facts.md` for reverse workflow compatibility, but generate commands do not use that file as input. + +Reverse `*.reverse` commands populate `.specify/memory/architecture-repo-facts.md` plus the selected view, preserving unrelated existing repo facts unless evidence has changed and the reason is recorded. + +The extension also ships the artifact contract used by the prompts: + +```text +.specify/extensions/arch/schemas/architecture-artifacts.schema.json +``` The commands do not edit: @@ -173,4 +217,4 @@ specify preset add --dev /home/administrator/github/spec-kit-arch/presets/arch-g rg -n "Architecture SSOT Injection|Architecture Grounding Summary" .agents/skills/speckit-plan/SKILL.md ``` -The extension intentionally does not provide the legacy `/speckit.arch` command. +The extension intentionally does not provide the legacy `/speckit.arch`, `/speckit.arch.generate`, or `/speckit.arch.reverse` commands. diff --git a/extensions/arch/commands/speckit.arch.development-generate.md b/extensions/arch/commands/speckit.arch.development-generate.md new file mode 100644 index 0000000000..19f9e6b721 --- /dev/null +++ b/extensions/arch/commands/speckit.arch.development-generate.md @@ -0,0 +1,68 @@ +--- +description: Generate the 4+1 development view from logical and process architecture views. +scripts: + sh: .specify/extensions/arch/scripts/bash/setup-arch.sh --json + ps: .specify/extensions/arch/scripts/powershell/setup-arch.ps1 -Json +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Goal + +Generate or refresh the development view: + +- Target view: `.specify/memory/architecture-development-view.md` +- Primary inputs: `.specify/memory/architecture-logical-view.md`, `.specify/memory/architecture-process-view.md` +- Optional synthesis refresh: `.specify/memory/architecture.md` + +The development view derives architecture-level components, package boundary intent, contract/artifact semantics, and dependency rules from logical and process views. + +## Operating Boundaries + +- Write only `DEVELOPMENT_VIEW`; update `ARCH_FILE` only if the five architecture views are already coherent enough to refresh synthesis without inventing content. +- Do not read, populate, or update `REPO_FACTS_FILE`. +- Do not modify logical or process views, source code, specs, plans, tasks, docs, tests, package files, deployment files, or runbooks. +- Stay at architecture-level development structure, not concrete implementation design. +- If prerequisite views are insufficient, record development gaps instead of fabricating components, packages, or contracts. + +## Setup Bootstrap + +`{SCRIPT}` may create missing architecture memory files from templates, including non-target view placeholders and `REPO_FACTS_FILE`. Treat that as bootstrap scaffolding only. After setup, this command must populate only `DEVELOPMENT_VIEW`, and may refresh `ARCH_FILE` only under the synthesis readiness rule below. + +If setup creates `REPO_FACTS_FILE`, leave it as-is. Do not read it as input and do not add facts to it from this generate command. + +## Structured Contract + +`ARCH_SCHEMA_FILE` is the authoritative structural contract for architecture artifacts. Build a JSON-compatible working model for every file you update, validate required sections, traceability records, gaps, and `High` / `Medium` / `Low` confidence values against the schema, then render Markdown with the corresponding template. The command owns extraction, classification, validation, and write policy; templates own Markdown layout only. + +## Synthesis Readiness + +Parse all five view paths from setup JSON: `SCENARIO_VIEW`, `LOGICAL_VIEW`, `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, and `PHYSICAL_VIEW`. + +A view is ready for synthesis only when it exists, no longer contains `NEEDS ARCH UPDATE`, and contains at least one source-backed non-gap architecture conclusion in its required sections. Unsupported or unknown items must be recorded in that view's gaps and do not make the view ready. Refresh `ARCH_FILE` only when all five views are ready and cross-view terminology is coherent. Otherwise leave `ARCH_FILE` unchanged and report the missing or not-ready views. + +## Outline + +1. Run `{SCRIPT}` from repo root and parse JSON for `ARCH_FILE`, `ARCH_SCHEMA_FILE`, `REPO_FACTS_FILE`, and all five view paths. +2. Load `LOGICAL_VIEW`, `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, `ARCH_SCHEMA_FILE`, `architecture-development-template.md`, and `architecture-template.md`. +3. Extract source-backed logical boundaries, process collaborations, contracts, and invariants. +4. Normalize component, package, contract, and dependency terminology. +5. Derive architecture-level development boundaries and dependency rules. +6. Classify each candidate as a supported development conclusion or a development gap under the schema and quality gates. +7. Render the schema-compliant development working model with `architecture-development-template.md`. +8. Apply the synthesis readiness rule. If all five view files are ready, refresh `ARCH_FILE`; otherwise leave synthesis untouched and report the missing or not-ready views. +9. Report updated paths and explicit development gaps. + +## Quality Gates + +- ERROR if the development view contains source file paths, concrete package trees, classes, functions, framework wiring, or implementation tasks. +- ERROR if a target-view conclusion is not grounded in logical/process architecture or a stated constraint. +- Unsupported target-view conclusions MUST appear only in `Development View Gaps`, not in conclusion tables. +- ERROR if a boundary has responsibilities but no explicit non-responsibility. +- Record gaps instead of inventing development structure. diff --git a/extensions/arch/commands/speckit.arch.development-reverse.md b/extensions/arch/commands/speckit.arch.development-reverse.md new file mode 100644 index 0000000000..35631c83bc --- /dev/null +++ b/extensions/arch/commands/speckit.arch.development-reverse.md @@ -0,0 +1,87 @@ +--- +description: Reverse-generate the 4+1 development view from observable repository facts. +scripts: + sh: .specify/extensions/arch/scripts/bash/setup-arch.sh --json + ps: .specify/extensions/arch/scripts/powershell/setup-arch.ps1 -Json +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Goal + +Reverse-generate or refresh the development view from observable repository evidence: + +- Evidence layer: `.specify/memory/architecture-repo-facts.md` +- Target view: `.specify/memory/architecture-development-view.md` +- Supporting inputs: logical and process views if available under Supporting View Availability +- Optional synthesis refresh: `.specify/memory/architecture.md` + +## Operating Boundaries + +- Write only `REPO_FACTS_FILE` and `DEVELOPMENT_VIEW`; update `ARCH_FILE` only if the five architecture views are already coherent enough to refresh synthesis without inventing content. +- Do not modify logical/process views, source code, README files, docs, package files, configuration, tests, deployment files, specs, plans, tasks, or runbooks. +- Stay at architecture-level development structure, not implementation design. +- If repository evidence is insufficient, record a specific evidence gap and development gap instead of inventing components, packages, contracts, or dependency rules. + +## Setup Bootstrap + +`{SCRIPT}` may create missing architecture memory files from templates, including non-target view placeholders. Treat that as bootstrap scaffolding only. After setup, this command must populate only `REPO_FACTS_FILE` and `DEVELOPMENT_VIEW`, and may refresh `ARCH_FILE` only under the synthesis readiness rule below. + +## Evidence Sources + +Inspect package directories, module boundaries, imports, manifests, build scripts, configuration ownership, test grouping, examples, and CI structure. + +Repository-First Inputs: if `.specify/memory/repository-first/` exists, inspect its markdown artifacts before deriving the view. Summarize build manifest detection, first-party module edges, allowed/forbidden invocation rules, dependency matrix signals, and governance actions in `REPO_FACTS_FILE`. Repository-first content must not be copied verbatim into architecture views. + +## Supporting View Availability + +`LOGICAL_VIEW` and `PROCESS_VIEW` are available only when each file exists, does not contain `NEEDS ARCH UPDATE`, and contains at least one source-backed non-gap architecture conclusion. Placeholder content and view gaps may identify missing context but must not support new development conclusions. + +## Repo Facts Merge Rule + +Treat `REPO_FACTS_FILE` as a cumulative evidence layer. Preserve existing non-placeholder facts from sections outside this command's evidence focus unless the cited evidence no longer exists or is contradicted by stronger evidence. When replacing, removing, or downgrading an existing fact, record the reason in `Evidence Gaps` or a review trigger instead of silently dropping it. + +## Repo Facts Evidence Rules + +- Every non-placeholder fact must name an evidence source such as a file, directory, configuration, test, script, manifest, command output, or commit signal. +- Confidence values are `High`, `Medium`, or `Low`: `High` means multiple independent sources agree; `Medium` means one strong source is present; `Low` means naming, directory structure, isolated code, or Git history suggests a fact but lacks behavior evidence. +- Git history is an auxiliary signal for change axes and boundary risks. It cannot independently prove architecture conclusions. +- Repository-first outputs are fact inputs. Summarize only architecture constraints, governance signals, gaps, or review triggers; do not copy full dependency inventories into architecture views. +- Concrete classes, functions, fields, endpoints, database tables, and implementation data structures may appear only as evidence sources, not architecture conclusions. +- Architecture conclusions must trace to repo facts, available supporting-view conclusions, user input, or stated external constraints. Unsupported items MUST appear only in `Evidence Gaps` or the target view gaps. + +## Structured Contract + +`ARCH_SCHEMA_FILE` is the authoritative structural contract for architecture artifacts. Build JSON-compatible working models for `REPO_FACTS_FILE` and the target view, validate sections, traceability records, gaps, and `High` / `Medium` / `Low` confidence values against the schema, then render Markdown with the corresponding templates. The command owns evidence collection, classification, merge policy, validation, and write policy; templates own Markdown layout only. + +## Synthesis Readiness + +Parse all five view paths from setup JSON: `SCENARIO_VIEW`, `LOGICAL_VIEW`, `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, and `PHYSICAL_VIEW`. + +A view is ready for synthesis only when it exists, no longer contains `NEEDS ARCH UPDATE`, and contains at least one source-backed non-gap architecture conclusion in its required sections. Unsupported or unknown items must be recorded in that view's gaps and do not make the view ready. Refresh `ARCH_FILE` only when all five views are ready and cross-view terminology is coherent. Otherwise leave `ARCH_FILE` unchanged and report the missing or not-ready views. + +## Outline + +1. Run `{SCRIPT}` from repo root and parse JSON for `ARCH_FILE`, `ARCH_SCHEMA_FILE`, `REPO_FACTS_FILE`, and all five view paths. +2. Load `REPO_FACTS_FILE`, `LOGICAL_VIEW`, `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, `ARCH_SCHEMA_FILE`, `architecture-repo-facts-template.md`, `architecture-development-template.md`, and `architecture-template.md`. +3. Refresh repo facts for this command's evidence focus and classify unsupported observations as evidence gaps. +4. Populate a schema-compliant development working model from eligible repo facts plus `LOGICAL_VIEW` and `PROCESS_VIEW` when they satisfy Supporting View Availability. +5. Summarize repository-first dependency matrices only as architecture constraints, dependency rules, gaps, or review triggers. +6. Apply the repo facts merge rule before writing `REPO_FACTS_FILE`. +7. Apply the synthesis readiness rule. If all five view files are ready, refresh `ARCH_FILE`; otherwise leave synthesis untouched and report the missing or not-ready views. +8. Report updated paths and explicit unresolved gaps. + +## Quality Gates + +- ERROR if the development view promotes concrete source paths, package trees, classes, functions, framework wiring, or implementation tasks into architecture conclusions. +- ERROR if repository-first dependency matrices are copied wholesale into a 4+1 view instead of summarized as architecture constraints. +- ERROR if a target-view conclusion lacks a repo fact, available `LOGICAL_VIEW` or `PROCESS_VIEW` source, or stated constraint. +- Unsupported target-view conclusions MUST appear only in `Evidence Gaps` or `Development View Gaps`, not in conclusion tables. +- ERROR if Git history is used alone as a development conclusion. +- GAP if development ownership or dependency governance cannot be supported by repository evidence. diff --git a/extensions/arch/commands/speckit.arch.generate.md b/extensions/arch/commands/speckit.arch.generate.md deleted file mode 100644 index a594134482..0000000000 --- a/extensions/arch/commands/speckit.arch.generate.md +++ /dev/null @@ -1,157 +0,0 @@ ---- -description: Execute the 4+1 architecture workflow and generate architecture view artifacts. -scripts: - sh: .specify/extensions/arch/scripts/bash/setup-arch.sh --json - ps: .specify/extensions/arch/scripts/powershell/setup-arch.ps1 -Json ---- - -## User Input - -```text -$ARGUMENTS -``` - -You **MUST** consider the user input before proceeding (if not empty). - -## Goal - -Generate or update the project-level architecture SSOT as 4+1 architecture artifacts: - -- Main synthesis: `.specify/memory/architecture.md` -- Scenario view: `.specify/memory/architecture-scenario-view.md` -- Logical view: `.specify/memory/architecture-logical-view.md` -- Process view: `.specify/memory/architecture-process-view.md` -- Development view: `.specify/memory/architecture-development-view.md` -- Physical view: `.specify/memory/architecture-physical-view.md` - -The scenario view is the entry point. It produces the UC semantics for this architecture pass: actors, goals, use cases, scenario paths, branches, and acceptance meaning. The other four views are derived from the scenario view. - -The six artifacts are the authoritative architecture design source. They preserve project-level architecture intent, boundaries, tradeoffs, constraints, and unresolved gaps as 4+1 architecture reasoning. - -## Operating Boundaries - -- Write only the six architecture artifacts listed above. -- Setup may create `.specify/memory/architecture-repo-facts.md` as a placeholder for reverse workflow compatibility, but generate must not load, populate, or update it. -- If `.specify/memory/uc.md` exists, read it only as supporting reference, not as a hard prerequisite or sole source of truth. -- Do not modify `.specify/memory/uc.md`, `.specify/memory/constitution.md`, feature specs, plans, tasks, source code, tests, or root `docs/`. -- Stay at abstract architecture-design level. -- Do not write concrete classes, files, functions, endpoints, DTO fields, database tables, framework selections, library choices, UI component details, deployment manifests, task breakdowns, test strategy, validation anchors, code notes, deployment scripts, or runbooks. -- If evidence is insufficient, record a specific gap in the affected view instead of inventing business facts, components, interfaces, modules, deployment units, or numeric metrics. - -## Architecture Layers - -### Architecture Reasoning Layer - -Reason only in the 4+1 views. Use each view template as the source of truth for that view's reasoning contract and artifact structure. Produce architecture design inference, not tracking, audit, or implementation planning. Maintain a cross-view architecture model that normalizes architecture meaning for synthesis while preserving each view's distinct concept type. Every conclusion must still be grounded in a scenario, object, state, collaboration, boundary, deployment constraint, or stated external constraint. Do not translate, rename, or equate concepts across views through notation-specific terms. - -### Representation Layer - -Markdown tables are the default artifact structure. Optional diagrams are renderings, not reasoning inputs. - -- Add optional diagrams only after the relevant view's reasoning is complete. -- Render only facts already present in that view. -- Do not introduce concepts, boundaries, relationships, deployment units, or cross-view concept alignments. -- C4, UML, Mermaid, and PlantUML are notation choices only; they must not change 4+1 view responsibilities. - -## Outline - -1. **Setup**: Run `{SCRIPT}` from repo root and parse JSON for `ARCH_FILE`, `ARCH_DIR`, `SCENARIO_VIEW`, `LOGICAL_VIEW`, `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, and `PHYSICAL_VIEW`. The setup JSON may also include `REPO_FACTS_FILE` for reverse workflow compatibility; acknowledge it and ignore it for this generate workflow. - -2. **Load context**: - - Read all six architecture artifacts created by setup. - - Do not read `REPO_FACTS_FILE` or `.specify/memory/architecture-repo-facts.md`. - - Read `.specify/memory/uc.md` if present as optional scenario background. - - Read the six architecture templates under `.specify/extensions/arch/templates/`. - -3. **Execute architecture workflow**: - - Phase -1: Establish architecture framing before writing any view. - - Phase 0: Fill `SCENARIO_VIEW` using its template. - - Phase 1: Fill `LOGICAL_VIEW` using its template and `SCENARIO_VIEW`. - - Phase 2: Fill `PROCESS_VIEW` using its template, `SCENARIO_VIEW`, and `LOGICAL_VIEW`. - - Phase 3: Fill `DEVELOPMENT_VIEW` using its template, `LOGICAL_VIEW`, and `PROCESS_VIEW`. - - Phase 4: Fill `PHYSICAL_VIEW` using its template, `PROCESS_VIEW`, and `DEVELOPMENT_VIEW`. - - Phase 5: Update `ARCH_FILE` using its synthesis template and the five completed views. - -4. **Stop and report**: Report the six updated paths and any explicit unresolved architecture gaps. - -## Phases - -### Phase -1: Architecture Framing - -**Output**: Working framing used to constrain all five views and the synthesis. Do not create an additional file. - -Before filling any view, identify the architecture judgment this pass must preserve: - -- View dimensions: scenario, logical, process, development, and physical -- Architecture intent: what this architecture pass is trying to make stable or explicit -- Core tensions: the main design forces in conflict, and the current tradeoff direction -- Stable boundaries: responsibilities or authority lines that should remain stable across iterations -- Change axes: business, workflow, operational, or integration areas expected to vary and therefore needing isolation -- Responsibility collision risks: responsibilities that agents or teams are likely to merge incorrectly -- Invariants: architecture rules later iterations must not violate -- Non-goals / anti-patterns: concerns this architecture pass does not solve, plus designs that would drift from the intent -- Implementation details to exclude: concrete classes, files, APIs, data schemas, frameworks, infrastructure manifests, or task plans that must stay out of the architecture layer - -Use this framing as the decision filter for every later phase. If a view cannot support a framing claim with scenario, boundary, lifecycle, collaboration, component, deployment, or stated-constraint evidence, record a specific gap instead of inventing facts. -Defer any optional diagram or notation-specific rendering until the affected view's 4+1 reasoning is complete. - -### Phase 0: Scenario View - -**Output**: `.specify/memory/architecture-scenario-view.md` - -Create or update the UC-producing scenario view by following the scenario view template. This phase is authoritative for scenario semantics inside the architecture workflow. Do not defer UC creation to a separate command. - -### Phase 1: Logical View - -**Input**: `.specify/memory/architecture-scenario-view.md` -**Output**: `.specify/memory/architecture-logical-view.md` - -Derive the logical view from the scenario view by following the logical view template. - -### Phase 2: Process View - -**Input**: `.specify/memory/architecture-scenario-view.md`, `.specify/memory/architecture-logical-view.md` -**Output**: `.specify/memory/architecture-process-view.md` - -Derive the process view from the scenario and logical views by following the process view template. - -### Phase 3: Development View - -**Input**: `.specify/memory/architecture-logical-view.md`, `.specify/memory/architecture-process-view.md` -**Output**: `.specify/memory/architecture-development-view.md` - -Derive the development view from the logical and process views by following the development view template. - -### Phase 4: Physical View - -**Input**: `.specify/memory/architecture-process-view.md`, `.specify/memory/architecture-development-view.md` -**Output**: `.specify/memory/architecture-physical-view.md` - -Derive the physical view from the process and development views by following the physical view template. - -### Phase 5: Architecture Synthesis - -**Input**: all five view files -**Output**: `architecture.md` - -Update the main synthesis file by following the synthesis template. Do not copy every detail from the view files. Summarize the architecture conclusions that connect multiple views. - -## Architecture Gates - -- ERROR if any view contains implementation details prohibited by the Operating Boundaries. -- ERROR if a boundary has responsibilities but no explicit non-responsibility or forbidden crossing. -- ERROR if a major architecture decision lacks consequence, tradeoff, or affected view. -- ERROR if an invariant is not tied to a scenario, state, boundary, collaboration, deployment constraint, or stated external constraint. -- ERROR if default tables or optional diagrams introduce concepts, relationships, or deployment units not justified by the framing or source views. -- ERROR if notation-specific output changes 4+1 view responsibilities or introduces architecture conclusions. -- ERROR if the cross-view architecture model erases the distinct meaning of a view-specific concept. -- ERROR if the workflow maps Use Case, Domain Object, Component, Container, or Deployment Unit as equivalent concepts across views. -- Record a specific gap instead of inventing business facts, authority boundaries, lifecycle rules, components, interfaces, deployment units, or numeric metrics. - -## Quality Bar - -- Scenario view must contain enough UC semantics for the other four views to derive from it. -- Every non-placeholder conclusion must be grounded in a scenario, object, runtime link, component boundary, deployment boundary, or stated constraint. -- Use stable names consistently across all five views and the synthesis file. -- Keep uncertainty specific: record what is unknown, which view it affects, and which architecture conclusion cannot yet be made. -- Remove generic statements such as "scalable", "secure", "observable", or "modular" unless they name owner, affected view, scope, and architecture consequence. diff --git a/extensions/arch/commands/speckit.arch.logical-generate.md b/extensions/arch/commands/speckit.arch.logical-generate.md new file mode 100644 index 0000000000..5eb6037474 --- /dev/null +++ b/extensions/arch/commands/speckit.arch.logical-generate.md @@ -0,0 +1,68 @@ +--- +description: Generate the 4+1 logical view from scenario architecture semantics. +scripts: + sh: .specify/extensions/arch/scripts/bash/setup-arch.sh --json + ps: .specify/extensions/arch/scripts/powershell/setup-arch.ps1 -Json +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Goal + +Generate or refresh the logical view: + +- Target view: `.specify/memory/architecture-logical-view.md` +- Primary input: `.specify/memory/architecture-scenario-view.md` +- Optional synthesis refresh: `.specify/memory/architecture.md` + +The logical view derives capability boundaries, domain objects, states, relationships, and invariants from scenario semantics. + +## Operating Boundaries + +- Write only `LOGICAL_VIEW`; update `ARCH_FILE` only if the five architecture views are already coherent enough to refresh synthesis without inventing content. +- Do not read, populate, or update `REPO_FACTS_FILE`. +- Do not modify scenario view, source code, specs, plans, tasks, docs, tests, deployment files, or runbooks. +- Stay at abstract logical architecture level. +- If the scenario view is placeholder, stale, or insufficient, record logical gaps instead of fabricating capabilities or domain objects. + +## Setup Bootstrap + +`{SCRIPT}` may create missing architecture memory files from templates, including non-target view placeholders and `REPO_FACTS_FILE`. Treat that as bootstrap scaffolding only. After setup, this command must populate only `LOGICAL_VIEW`, and may refresh `ARCH_FILE` only under the synthesis readiness rule below. + +If setup creates `REPO_FACTS_FILE`, leave it as-is. Do not read it as input and do not add facts to it from this generate command. + +## Structured Contract + +`ARCH_SCHEMA_FILE` is the authoritative structural contract for architecture artifacts. Build a JSON-compatible working model for every file you update, validate required sections, traceability records, gaps, and `High` / `Medium` / `Low` confidence values against the schema, then render Markdown with the corresponding template. The command owns extraction, classification, validation, and write policy; templates own Markdown layout only. + +## Synthesis Readiness + +Parse all five view paths from setup JSON: `SCENARIO_VIEW`, `LOGICAL_VIEW`, `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, and `PHYSICAL_VIEW`. + +A view is ready for synthesis only when it exists, no longer contains `NEEDS ARCH UPDATE`, and contains at least one source-backed non-gap architecture conclusion in its required sections. Unsupported or unknown items must be recorded in that view's gaps and do not make the view ready. Refresh `ARCH_FILE` only when all five views are ready and cross-view terminology is coherent. Otherwise leave `ARCH_FILE` unchanged and report the missing or not-ready views. + +## Outline + +1. Run `{SCRIPT}` from repo root and parse JSON for `ARCH_FILE`, `ARCH_SCHEMA_FILE`, `REPO_FACTS_FILE`, and all five view paths. +2. Load `SCENARIO_VIEW`, `LOGICAL_VIEW`, `ARCH_FILE`, `ARCH_SCHEMA_FILE`, `architecture-logical-template.md`, and `architecture-template.md`. +3. Extract source-backed scenario conclusions and scenario terminology. +4. Normalize capability, object, state, and boundary names. +5. Derive logical authority boundaries, domain relationships, and lifecycle constraints. +6. Classify each candidate as a supported logical conclusion or a logical gap under the schema and quality gates. +7. Render the schema-compliant logical working model with `architecture-logical-template.md`. +8. Apply the synthesis readiness rule. If all five view files are ready, refresh `ARCH_FILE`; otherwise leave synthesis untouched and report the missing or not-ready views. +9. Report updated paths and explicit logical gaps. + +## Quality Gates + +- ERROR if the logical view introduces classes, DTOs, database tables, fields, endpoints, schemas, or implementation data structures. +- ERROR if a target-view conclusion is not grounded in a scenario, boundary, lifecycle constraint, or stated user constraint. +- Unsupported target-view conclusions MUST appear only in `Logical Gaps`, not in conclusion tables. +- ERROR if a boundary has responsibilities but no explicit non-responsibility. +- Record gaps instead of inventing authority, lifecycle, or object facts. diff --git a/extensions/arch/commands/speckit.arch.logical-reverse.md b/extensions/arch/commands/speckit.arch.logical-reverse.md new file mode 100644 index 0000000000..4d6012ed13 --- /dev/null +++ b/extensions/arch/commands/speckit.arch.logical-reverse.md @@ -0,0 +1,86 @@ +--- +description: Reverse-generate the 4+1 logical view from observable repository facts. +scripts: + sh: .specify/extensions/arch/scripts/bash/setup-arch.sh --json + ps: .specify/extensions/arch/scripts/powershell/setup-arch.ps1 -Json +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Goal + +Reverse-generate or refresh the logical view from observable repository evidence: + +- Evidence layer: `.specify/memory/architecture-repo-facts.md` +- Target view: `.specify/memory/architecture-logical-view.md` +- Supporting input: `.specify/memory/architecture-scenario-view.md` if available under Supporting View Availability +- Optional synthesis refresh: `.specify/memory/architecture.md` + +## Operating Boundaries + +- Write only `REPO_FACTS_FILE` and `LOGICAL_VIEW`; update `ARCH_FILE` only if the five architecture views are already coherent enough to refresh synthesis without inventing content. +- Do not modify scenario view, source code, README files, docs, package files, configuration, tests, deployment files, specs, plans, tasks, or runbooks. +- Stay at abstract logical architecture level. +- If repository evidence is insufficient, record a specific evidence gap and logical gap instead of inventing capabilities, objects, states, or invariants. + +## Setup Bootstrap + +`{SCRIPT}` may create missing architecture memory files from templates, including non-target view placeholders. Treat that as bootstrap scaffolding only. After setup, this command must populate only `REPO_FACTS_FILE` and `LOGICAL_VIEW`, and may refresh `ARCH_FILE` only under the synthesis readiness rule below. + +## Evidence Sources + +Inspect README/docs, entry points, tests, examples, route declarations, state clues, persistence clues, domain naming, and configuration ownership. + +Repository-First Inputs: if `.specify/memory/repository-first/` exists, inspect its markdown artifacts and summarize build manifest detection, first-party module edges, invocation governance, and dependency governance signals that affect logical boundaries in `REPO_FACTS_FILE`. Repository-first content must not be copied verbatim into architecture views. + +## Supporting View Availability + +`SCENARIO_VIEW` is available only when it exists, does not contain `NEEDS ARCH UPDATE`, and contains at least one source-backed non-gap architecture conclusion. Placeholder content and scenario gaps may identify missing context but must not support new logical conclusions. + +## Repo Facts Merge Rule + +Treat `REPO_FACTS_FILE` as a cumulative evidence layer. Preserve existing non-placeholder facts from sections outside this command's evidence focus unless the cited evidence no longer exists or is contradicted by stronger evidence. When replacing, removing, or downgrading an existing fact, record the reason in `Evidence Gaps` or a review trigger instead of silently dropping it. + +## Repo Facts Evidence Rules + +- Every non-placeholder fact must name an evidence source such as a file, directory, configuration, test, script, manifest, command output, or commit signal. +- Confidence values are `High`, `Medium`, or `Low`: `High` means multiple independent sources agree; `Medium` means one strong source is present; `Low` means naming, directory structure, isolated code, or Git history suggests a fact but lacks behavior evidence. +- Git history is an auxiliary signal for change axes and boundary risks. It cannot independently prove architecture conclusions. +- Repository-first outputs are fact inputs. Summarize only architecture constraints, governance signals, gaps, or review triggers; do not copy full dependency inventories into architecture views. +- Concrete classes, functions, fields, endpoints, database tables, and implementation data structures may appear only as evidence sources, not architecture conclusions. +- Architecture conclusions must trace to repo facts, available supporting-view conclusions, user input, or stated external constraints. Unsupported items MUST appear only in `Evidence Gaps` or the target view gaps. + +## Structured Contract + +`ARCH_SCHEMA_FILE` is the authoritative structural contract for architecture artifacts. Build JSON-compatible working models for `REPO_FACTS_FILE` and the target view, validate sections, traceability records, gaps, and `High` / `Medium` / `Low` confidence values against the schema, then render Markdown with the corresponding templates. The command owns evidence collection, classification, merge policy, validation, and write policy; templates own Markdown layout only. + +## Synthesis Readiness + +Parse all five view paths from setup JSON: `SCENARIO_VIEW`, `LOGICAL_VIEW`, `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, and `PHYSICAL_VIEW`. + +A view is ready for synthesis only when it exists, no longer contains `NEEDS ARCH UPDATE`, and contains at least one source-backed non-gap architecture conclusion in its required sections. Unsupported or unknown items must be recorded in that view's gaps and do not make the view ready. Refresh `ARCH_FILE` only when all five views are ready and cross-view terminology is coherent. Otherwise leave `ARCH_FILE` unchanged and report the missing or not-ready views. + +## Outline + +1. Run `{SCRIPT}` from repo root and parse JSON for `ARCH_FILE`, `ARCH_SCHEMA_FILE`, `REPO_FACTS_FILE`, and all five view paths. +2. Load `REPO_FACTS_FILE`, `SCENARIO_VIEW`, `LOGICAL_VIEW`, `ARCH_SCHEMA_FILE`, `architecture-repo-facts-template.md`, `architecture-logical-template.md`, and `architecture-template.md`. +3. Refresh repo facts for this command's evidence focus and classify unsupported observations as evidence gaps. +4. Populate a schema-compliant logical working model from eligible repo facts and `SCENARIO_VIEW` when it satisfies Supporting View Availability. +5. Put low-confidence facts in gaps rather than promoting them into architecture conclusions. +6. Apply the repo facts merge rule before writing `REPO_FACTS_FILE`. +7. Apply the synthesis readiness rule. If all five view files are ready, refresh `ARCH_FILE`; otherwise leave synthesis untouched and report the missing or not-ready views. +8. Report updated paths and explicit unresolved gaps. + +## Quality Gates + +- ERROR if the logical view promotes classes, functions, fields, endpoints, database structures, or implementation data structures into architecture conclusions. +- ERROR if a target-view conclusion lacks a repo fact, available `SCENARIO_VIEW` source, or stated constraint. +- Unsupported target-view conclusions MUST appear only in `Evidence Gaps` or `Logical Gaps`, not in conclusion tables. +- ERROR if Git history is used alone as a logical conclusion. +- GAP if authority boundaries or domain lifecycle cannot be supported by repository evidence. diff --git a/extensions/arch/commands/speckit.arch.physical-generate.md b/extensions/arch/commands/speckit.arch.physical-generate.md new file mode 100644 index 0000000000..4b321ccbef --- /dev/null +++ b/extensions/arch/commands/speckit.arch.physical-generate.md @@ -0,0 +1,68 @@ +--- +description: Generate the 4+1 physical view from process and development architecture views. +scripts: + sh: .specify/extensions/arch/scripts/bash/setup-arch.sh --json + ps: .specify/extensions/arch/scripts/powershell/setup-arch.ps1 -Json +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Goal + +Generate or refresh the physical view: + +- Target view: `.specify/memory/architecture-physical-view.md` +- Primary inputs: `.specify/memory/architecture-process-view.md`, `.specify/memory/architecture-development-view.md` +- Optional synthesis refresh: `.specify/memory/architecture.md` + +The physical view derives deployment, hosting, external system, fact-source, observability, and operational boundaries from process and development architecture. + +## Operating Boundaries + +- Write only `PHYSICAL_VIEW`; update `ARCH_FILE` only if the five architecture views are already coherent enough to refresh synthesis without inventing content. +- Do not read, populate, or update `REPO_FACTS_FILE`. +- Do not modify process or development views, source code, specs, plans, tasks, docs, tests, deployment manifests, infrastructure files, or runbooks. +- Stay at abstract physical architecture level. +- If prerequisite views are insufficient, record physical gaps instead of fabricating deployment units, external systems, or operational constraints. + +## Setup Bootstrap + +`{SCRIPT}` may create missing architecture memory files from templates, including non-target view placeholders and `REPO_FACTS_FILE`. Treat that as bootstrap scaffolding only. After setup, this command must populate only `PHYSICAL_VIEW`, and may refresh `ARCH_FILE` only under the synthesis readiness rule below. + +If setup creates `REPO_FACTS_FILE`, leave it as-is. Do not read it as input and do not add facts to it from this generate command. + +## Structured Contract + +`ARCH_SCHEMA_FILE` is the authoritative structural contract for architecture artifacts. Build a JSON-compatible working model for every file you update, validate required sections, traceability records, gaps, and `High` / `Medium` / `Low` confidence values against the schema, then render Markdown with the corresponding template. The command owns extraction, classification, validation, and write policy; templates own Markdown layout only. + +## Synthesis Readiness + +Parse all five view paths from setup JSON: `SCENARIO_VIEW`, `LOGICAL_VIEW`, `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, and `PHYSICAL_VIEW`. + +A view is ready for synthesis only when it exists, no longer contains `NEEDS ARCH UPDATE`, and contains at least one source-backed non-gap architecture conclusion in its required sections. Unsupported or unknown items must be recorded in that view's gaps and do not make the view ready. Refresh `ARCH_FILE` only when all five views are ready and cross-view terminology is coherent. Otherwise leave `ARCH_FILE` unchanged and report the missing or not-ready views. + +## Outline + +1. Run `{SCRIPT}` from repo root and parse JSON for `ARCH_FILE`, `ARCH_SCHEMA_FILE`, `REPO_FACTS_FILE`, and all five view paths. +2. Load `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, `PHYSICAL_VIEW`, `ARCH_SCHEMA_FILE`, `architecture-physical-template.md`, and `architecture-template.md`. +3. Extract source-backed process collaborations, development boundaries, release constraints, and operational constraints. +4. Normalize deployment, external-system, fact-source, observability, and operations terminology. +5. Derive physical architecture boundaries and ownership constraints. +6. Classify each candidate as a supported physical conclusion or a physical gap under the schema and quality gates. +7. Render the schema-compliant physical working model with `architecture-physical-template.md`. +8. Apply the synthesis readiness rule. If all five view files are ready, refresh `ARCH_FILE`; otherwise leave synthesis untouched and report the missing or not-ready views. +9. Report updated paths and explicit physical gaps. + +## Quality Gates + +- ERROR if the physical view contains Kubernetes YAML, cloud resource manifests, machine sizes, service SKUs, deployment scripts, runbooks, or concrete infrastructure configuration. +- ERROR if a target-view conclusion is not grounded in `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, or a stated constraint. +- Unsupported target-view conclusions MUST appear only in `Physical View Gaps`, not in conclusion tables. +- ERROR if a boundary has responsibilities but no explicit non-responsibility. +- Record gaps instead of inventing topology or operations facts. diff --git a/extensions/arch/commands/speckit.arch.physical-reverse.md b/extensions/arch/commands/speckit.arch.physical-reverse.md new file mode 100644 index 0000000000..d7824b3fac --- /dev/null +++ b/extensions/arch/commands/speckit.arch.physical-reverse.md @@ -0,0 +1,86 @@ +--- +description: Reverse-generate the 4+1 physical view from observable repository facts. +scripts: + sh: .specify/extensions/arch/scripts/bash/setup-arch.sh --json + ps: .specify/extensions/arch/scripts/powershell/setup-arch.ps1 -Json +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Goal + +Reverse-generate or refresh the physical view from observable repository evidence: + +- Evidence layer: `.specify/memory/architecture-repo-facts.md` +- Target view: `.specify/memory/architecture-physical-view.md` +- Supporting inputs: process and development views if available under Supporting View Availability +- Optional synthesis refresh: `.specify/memory/architecture.md` + +## Operating Boundaries + +- Write only `REPO_FACTS_FILE` and `PHYSICAL_VIEW`; update `ARCH_FILE` only if the five architecture views are already coherent enough to refresh synthesis without inventing content. +- Do not modify process/development views, source code, README files, docs, package files, configuration, tests, deployment files, specs, plans, tasks, infrastructure files, or runbooks. +- Stay at abstract physical architecture level. +- If repository evidence is insufficient, record a specific evidence gap and physical gap instead of inventing deployment units, external systems, fact sources, or operational constraints. + +## Setup Bootstrap + +`{SCRIPT}` may create missing architecture memory files from templates, including non-target view placeholders. Treat that as bootstrap scaffolding only. After setup, this command must populate only `REPO_FACTS_FILE` and `PHYSICAL_VIEW`, and may refresh `ARCH_FILE` only under the synthesis readiness rule below. + +## Evidence Sources + +Inspect Dockerfiles, Compose files, Kubernetes manifests, Terraform, CI/CD, environment examples, service configuration, deployment documentation, scripts, and observable external-system configuration. + +Repository-First Inputs: if `.specify/memory/repository-first/` exists, inspect its markdown artifacts and summarize build manifest detection, first-party module edges, invocation governance, and dependency governance signals that affect deployment or external boundaries in `REPO_FACTS_FILE`. Repository-first content must not be copied verbatim into architecture views. + +## Supporting View Availability + +`PROCESS_VIEW` and `DEVELOPMENT_VIEW` are available only when each file exists, does not contain `NEEDS ARCH UPDATE`, and contains at least one source-backed non-gap architecture conclusion. Placeholder content and view gaps may identify missing context but must not support new physical conclusions. + +## Repo Facts Merge Rule + +Treat `REPO_FACTS_FILE` as a cumulative evidence layer. Preserve existing non-placeholder facts from sections outside this command's evidence focus unless the cited evidence no longer exists or is contradicted by stronger evidence. When replacing, removing, or downgrading an existing fact, record the reason in `Evidence Gaps` or a review trigger instead of silently dropping it. + +## Repo Facts Evidence Rules + +- Every non-placeholder fact must name an evidence source such as a file, directory, configuration, test, script, manifest, command output, or commit signal. +- Confidence values are `High`, `Medium`, or `Low`: `High` means multiple independent sources agree; `Medium` means one strong source is present; `Low` means naming, directory structure, isolated code, or Git history suggests a fact but lacks behavior evidence. +- Git history is an auxiliary signal for change axes and boundary risks. It cannot independently prove architecture conclusions. +- Repository-first outputs are fact inputs. Summarize only architecture constraints, governance signals, gaps, or review triggers; do not copy full dependency inventories into architecture views. +- Concrete classes, functions, fields, endpoints, database tables, and implementation data structures may appear only as evidence sources, not architecture conclusions. +- Architecture conclusions must trace to repo facts, available supporting-view conclusions, user input, or stated external constraints. Unsupported items MUST appear only in `Evidence Gaps` or the target view gaps. + +## Structured Contract + +`ARCH_SCHEMA_FILE` is the authoritative structural contract for architecture artifacts. Build JSON-compatible working models for `REPO_FACTS_FILE` and the target view, validate sections, traceability records, gaps, and `High` / `Medium` / `Low` confidence values against the schema, then render Markdown with the corresponding templates. The command owns evidence collection, classification, merge policy, validation, and write policy; templates own Markdown layout only. + +## Synthesis Readiness + +Parse all five view paths from setup JSON: `SCENARIO_VIEW`, `LOGICAL_VIEW`, `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, and `PHYSICAL_VIEW`. + +A view is ready for synthesis only when it exists, no longer contains `NEEDS ARCH UPDATE`, and contains at least one source-backed non-gap architecture conclusion in its required sections. Unsupported or unknown items must be recorded in that view's gaps and do not make the view ready. Refresh `ARCH_FILE` only when all five views are ready and cross-view terminology is coherent. Otherwise leave `ARCH_FILE` unchanged and report the missing or not-ready views. + +## Outline + +1. Run `{SCRIPT}` from repo root and parse JSON for `ARCH_FILE`, `ARCH_SCHEMA_FILE`, `REPO_FACTS_FILE`, and all five view paths. +2. Load `REPO_FACTS_FILE`, `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, `PHYSICAL_VIEW`, `ARCH_SCHEMA_FILE`, `architecture-repo-facts-template.md`, `architecture-physical-template.md`, and `architecture-template.md`. +3. Refresh repo facts for this command's evidence focus and classify unsupported observations as evidence gaps. +4. Populate a schema-compliant physical working model from eligible repo facts plus `PROCESS_VIEW` and `DEVELOPMENT_VIEW` when they satisfy Supporting View Availability. +5. Put low-confidence facts in gaps rather than promoting them into physical conclusions. +6. Apply the repo facts merge rule before writing `REPO_FACTS_FILE`. +7. Apply the synthesis readiness rule. If all five view files are ready, refresh `ARCH_FILE`; otherwise leave synthesis untouched and report the missing or not-ready views. +8. Report updated paths and explicit unresolved gaps. + +## Quality Gates + +- ERROR if the physical view promotes Kubernetes YAML, cloud resource manifests, machine sizes, service SKUs, deployment scripts, runbooks, or concrete infrastructure configuration into architecture conclusions. +- ERROR if a target-view conclusion lacks repo facts, available `PROCESS_VIEW`, available `DEVELOPMENT_VIEW`, or stated constraints. +- Unsupported target-view conclusions MUST appear only in `Evidence Gaps` or `Physical View Gaps`, not in conclusion tables. +- ERROR if Git history is used alone as a physical conclusion. +- GAP if deployment topology, external ownership, or operations cannot be supported by repository evidence. diff --git a/extensions/arch/commands/speckit.arch.process-generate.md b/extensions/arch/commands/speckit.arch.process-generate.md new file mode 100644 index 0000000000..e2819dd314 --- /dev/null +++ b/extensions/arch/commands/speckit.arch.process-generate.md @@ -0,0 +1,68 @@ +--- +description: Generate the 4+1 process view from scenario and logical architecture views. +scripts: + sh: .specify/extensions/arch/scripts/bash/setup-arch.sh --json + ps: .specify/extensions/arch/scripts/powershell/setup-arch.ps1 -Json +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Goal + +Generate or refresh the process view: + +- Target view: `.specify/memory/architecture-process-view.md` +- Primary inputs: `.specify/memory/architecture-scenario-view.md`, `.specify/memory/architecture-logical-view.md` +- Optional synthesis refresh: `.specify/memory/architecture.md` + +The process view derives runtime collaboration, handoffs, approvals, receipts, state advancement, and failure closure from scenario paths and logical boundaries. + +## Operating Boundaries + +- Write only `PROCESS_VIEW`; update `ARCH_FILE` only if the five architecture views are already coherent enough to refresh synthesis without inventing content. +- Do not read, populate, or update `REPO_FACTS_FILE`. +- Do not modify scenario or logical views, source code, specs, plans, tasks, docs, tests, deployment files, or runbooks. +- Stay at abstract runtime-collaboration architecture level. +- If prerequisite views are insufficient, record process gaps instead of fabricating runtime links or failure paths. + +## Setup Bootstrap + +`{SCRIPT}` may create missing architecture memory files from templates, including non-target view placeholders and `REPO_FACTS_FILE`. Treat that as bootstrap scaffolding only. After setup, this command must populate only `PROCESS_VIEW`, and may refresh `ARCH_FILE` only under the synthesis readiness rule below. + +If setup creates `REPO_FACTS_FILE`, leave it as-is. Do not read it as input and do not add facts to it from this generate command. + +## Structured Contract + +`ARCH_SCHEMA_FILE` is the authoritative structural contract for architecture artifacts. Build a JSON-compatible working model for every file you update, validate required sections, traceability records, gaps, and `High` / `Medium` / `Low` confidence values against the schema, then render Markdown with the corresponding template. The command owns extraction, classification, validation, and write policy; templates own Markdown layout only. + +## Synthesis Readiness + +Parse all five view paths from setup JSON: `SCENARIO_VIEW`, `LOGICAL_VIEW`, `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, and `PHYSICAL_VIEW`. + +A view is ready for synthesis only when it exists, no longer contains `NEEDS ARCH UPDATE`, and contains at least one source-backed non-gap architecture conclusion in its required sections. Unsupported or unknown items must be recorded in that view's gaps and do not make the view ready. Refresh `ARCH_FILE` only when all five views are ready and cross-view terminology is coherent. Otherwise leave `ARCH_FILE` unchanged and report the missing or not-ready views. + +## Outline + +1. Run `{SCRIPT}` from repo root and parse JSON for `ARCH_FILE`, `ARCH_SCHEMA_FILE`, `REPO_FACTS_FILE`, and all five view paths. +2. Load `SCENARIO_VIEW`, `LOGICAL_VIEW`, `PROCESS_VIEW`, `ARCH_SCHEMA_FILE`, `architecture-process-template.md`, and `architecture-template.md`. +3. Extract source-backed scenario paths, logical boundaries, states, and invariants. +4. Normalize runtime participant, handoff, receipt, and failure-closure terminology. +5. Derive runtime collaboration boundaries and completion conditions. +6. Classify each candidate as a supported process conclusion or a process gap under the schema and quality gates. +7. Render the schema-compliant process working model with `architecture-process-template.md`. +8. Apply the synthesis readiness rule. If all five view files are ready, refresh `ARCH_FILE`; otherwise leave synthesis untouched and report the missing or not-ready views. +9. Report updated paths and explicit process gaps. + +## Quality Gates + +- ERROR if the process view contains call stacks, queue names, retry counts, endpoint sequences, workflow engine configuration, or orchestration code. +- ERROR if a target-view conclusion is not grounded in a scenario path, logical boundary, or stated constraint. +- Unsupported target-view conclusions MUST appear only in `Process Gaps`, not in conclusion tables. +- ERROR if a boundary has responsibilities but no explicit non-responsibility. +- Record gaps instead of inventing runtime ownership or operational behavior. diff --git a/extensions/arch/commands/speckit.arch.process-reverse.md b/extensions/arch/commands/speckit.arch.process-reverse.md new file mode 100644 index 0000000000..ecd04e64d2 --- /dev/null +++ b/extensions/arch/commands/speckit.arch.process-reverse.md @@ -0,0 +1,86 @@ +--- +description: Reverse-generate the 4+1 process view from observable repository facts. +scripts: + sh: .specify/extensions/arch/scripts/bash/setup-arch.sh --json + ps: .specify/extensions/arch/scripts/powershell/setup-arch.ps1 -Json +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Goal + +Reverse-generate or refresh the process view from observable repository evidence: + +- Evidence layer: `.specify/memory/architecture-repo-facts.md` +- Target view: `.specify/memory/architecture-process-view.md` +- Supporting inputs: scenario and logical views if available under Supporting View Availability +- Optional synthesis refresh: `.specify/memory/architecture.md` + +## Operating Boundaries + +- Write only `REPO_FACTS_FILE` and `PROCESS_VIEW`; update `ARCH_FILE` only if the five architecture views are already coherent enough to refresh synthesis without inventing content. +- Do not modify scenario/logical views, source code, README files, docs, package files, configuration, tests, deployment files, specs, plans, tasks, or runbooks. +- Stay at abstract runtime-collaboration architecture level. +- If repository evidence is insufficient, record a specific evidence gap and process gap instead of inventing runtime links, handoffs, retries, or failure closure. + +## Setup Bootstrap + +`{SCRIPT}` may create missing architecture memory files from templates, including non-target view placeholders. Treat that as bootstrap scaffolding only. After setup, this command must populate only `REPO_FACTS_FILE` and `PROCESS_VIEW`, and may refresh `ARCH_FILE` only under the synthesis readiness rule below. + +## Evidence Sources + +Inspect workers, schedulers, middleware, async jobs, event handlers, transactions, retries, locks, route behavior, tests, examples, configuration, and CI/runtime scripts. + +Repository-First Inputs: if `.specify/memory/repository-first/` exists, inspect its markdown artifacts and summarize build manifest detection, first-party module edges, invocation governance, and dependency governance signals that affect runtime boundaries in `REPO_FACTS_FILE`. Repository-first content must not be copied verbatim into architecture views. + +## Supporting View Availability + +`SCENARIO_VIEW` and `LOGICAL_VIEW` are available only when each file exists, does not contain `NEEDS ARCH UPDATE`, and contains at least one source-backed non-gap architecture conclusion. Placeholder content and view gaps may identify missing context but must not support new process conclusions. + +## Repo Facts Merge Rule + +Treat `REPO_FACTS_FILE` as a cumulative evidence layer. Preserve existing non-placeholder facts from sections outside this command's evidence focus unless the cited evidence no longer exists or is contradicted by stronger evidence. When replacing, removing, or downgrading an existing fact, record the reason in `Evidence Gaps` or a review trigger instead of silently dropping it. + +## Repo Facts Evidence Rules + +- Every non-placeholder fact must name an evidence source such as a file, directory, configuration, test, script, manifest, command output, or commit signal. +- Confidence values are `High`, `Medium`, or `Low`: `High` means multiple independent sources agree; `Medium` means one strong source is present; `Low` means naming, directory structure, isolated code, or Git history suggests a fact but lacks behavior evidence. +- Git history is an auxiliary signal for change axes and boundary risks. It cannot independently prove architecture conclusions. +- Repository-first outputs are fact inputs. Summarize only architecture constraints, governance signals, gaps, or review triggers; do not copy full dependency inventories into architecture views. +- Concrete classes, functions, fields, endpoints, database tables, and implementation data structures may appear only as evidence sources, not architecture conclusions. +- Architecture conclusions must trace to repo facts, available supporting-view conclusions, user input, or stated external constraints. Unsupported items MUST appear only in `Evidence Gaps` or the target view gaps. + +## Structured Contract + +`ARCH_SCHEMA_FILE` is the authoritative structural contract for architecture artifacts. Build JSON-compatible working models for `REPO_FACTS_FILE` and the target view, validate sections, traceability records, gaps, and `High` / `Medium` / `Low` confidence values against the schema, then render Markdown with the corresponding templates. The command owns evidence collection, classification, merge policy, validation, and write policy; templates own Markdown layout only. + +## Synthesis Readiness + +Parse all five view paths from setup JSON: `SCENARIO_VIEW`, `LOGICAL_VIEW`, `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, and `PHYSICAL_VIEW`. + +A view is ready for synthesis only when it exists, no longer contains `NEEDS ARCH UPDATE`, and contains at least one source-backed non-gap architecture conclusion in its required sections. Unsupported or unknown items must be recorded in that view's gaps and do not make the view ready. Refresh `ARCH_FILE` only when all five views are ready and cross-view terminology is coherent. Otherwise leave `ARCH_FILE` unchanged and report the missing or not-ready views. + +## Outline + +1. Run `{SCRIPT}` from repo root and parse JSON for `ARCH_FILE`, `ARCH_SCHEMA_FILE`, `REPO_FACTS_FILE`, and all five view paths. +2. Load `REPO_FACTS_FILE`, `SCENARIO_VIEW`, `LOGICAL_VIEW`, `PROCESS_VIEW`, `ARCH_SCHEMA_FILE`, `architecture-repo-facts-template.md`, `architecture-process-template.md`, and `architecture-template.md`. +3. Refresh repo facts for this command's evidence focus and classify unsupported observations as evidence gaps. +4. Populate a schema-compliant process working model from eligible repo facts plus `SCENARIO_VIEW` and `LOGICAL_VIEW` when they satisfy Supporting View Availability. +5. Put low-confidence facts in gaps rather than promoting them into process conclusions. +6. Apply the repo facts merge rule before writing `REPO_FACTS_FILE`. +7. Apply the synthesis readiness rule. If all five view files are ready, refresh `ARCH_FILE`; otherwise leave synthesis untouched and report the missing or not-ready views. +8. Report updated paths and explicit unresolved gaps. + +## Quality Gates + +- ERROR if the process view promotes call stacks, queue names, retry counts, thread/process details, endpoint sequences, workflow engine config, or orchestration code into architecture conclusions. +- ERROR if a target-view conclusion lacks a repo fact, available `SCENARIO_VIEW` or `LOGICAL_VIEW` source, or stated constraint. +- Unsupported target-view conclusions MUST appear only in `Evidence Gaps` or `Process Gaps`, not in conclusion tables. +- ERROR if Git history is used alone as a process conclusion. +- GAP if runtime ownership, handoffs, or failure closure cannot be supported by repository evidence. diff --git a/extensions/arch/commands/speckit.arch.reverse.md b/extensions/arch/commands/speckit.arch.reverse.md deleted file mode 100644 index e2fa579889..0000000000 --- a/extensions/arch/commands/speckit.arch.reverse.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -description: Reverse-generate 4+1 architecture artifacts from observable repository facts. -scripts: - sh: .specify/extensions/arch/scripts/bash/setup-arch.sh --json - ps: .specify/extensions/arch/scripts/powershell/setup-arch.ps1 -Json ---- - -## User Input - -```text -$ARGUMENTS -``` - -You **MUST** consider the user input before proceeding (if not empty). - -## Goal - -Reverse-generate or refresh the project-level architecture SSOT for an ordinary historical repository by first recording observable repository facts, then deriving 4+1 architecture artifacts from those facts. - -The workflow writes: - -- Repo facts: `.specify/memory/architecture-repo-facts.md` -- Main synthesis: `.specify/memory/architecture.md` -- Scenario view: `.specify/memory/architecture-scenario-view.md` -- Logical view: `.specify/memory/architecture-logical-view.md` -- Process view: `.specify/memory/architecture-process-view.md` -- Development view: `.specify/memory/architecture-development-view.md` -- Physical view: `.specify/memory/architecture-physical-view.md` - -`architecture-repo-facts.md` is the evidence layer for this reverse pass. The six architecture files remain the architecture SSOT. They must summarize architecture conclusions, not become source-code audit reports. - -## Operating Boundaries - -- Write only the seven files listed above. -- Do not modify source code, README files, project documentation, package files, configuration, tests, deployment files, `.specify/memory/uc.md`, `.specify/memory/constitution.md`, feature specs, plans, tasks, root `docs/`, deployment manifests, or runbooks. -- Stay at abstract architecture-design level. -- Do not write concrete classes, files, functions, endpoints, DTO fields, database table fields, framework selections, library choices, UI component details, deployment manifests, task breakdowns, test strategy, validation anchors, code notes, deployment scripts, or runbooks into the architecture views. -- If evidence is insufficient, record a specific gap in the repo facts file and affected view instead of inventing business facts, actors, use cases, components, interfaces, modules, deployment units, or numeric metrics. - -## Evidence Sources - -Inspect the repository for observable facts. Prefer concise inventory over exhaustive source listing. - -Use these source categories when present: - -- README and project documentation -- Source layout and named entry points -- Package manager files and build scripts -- Configuration and environment examples -- Tests, fixtures, examples, and sample data -- CLI commands, API routes, UI routes, scheduled jobs, workers, middleware, or event handlers -- CI/CD definitions -- Docker, Compose, Kubernetes, Terraform, or deployment configuration -- Operational scripts -- Recent Git history signals - -Git history is auxiliary. It may indicate change axes, hotspots, and boundary risks, but it must not independently prove an architecture conclusion. - -### Repository-First Inputs - -If `.specify/memory/repository-first/` exists, inspect its markdown artifacts before deriving the architecture views. These files are optional, project-local evidence projections for repositories that already ran a repository-first analysis pass. - -Treat these repository-first artifacts as evidence inputs, not as architecture artifacts: - -- `.specify/memory/repository-first/technical-dependency-matrix.md`: 2nd/3rd-party dependency facts, version-source signals, version-divergence signals, and unresolved-version signals. -- `.specify/memory/repository-first/module-invocation-spec.md`: first-party module invocation governance, allowed directions, forbidden directions, and dependency-governance rules derived from matrix signals. - -When present, summarize repository-first facts into `REPO_FACTS_FILE` under the repository-first projection sections. Use them primarily to support the development view and synthesis constraints. They may also support physical or process gaps when dependency evidence reveals unresolved external systems or runtime ownership ambiguity. - -Repository-first content must not be copied verbatim into architecture views. The architecture views may only carry architecture-level conclusions such as stable package boundaries, allowed/forbidden dependency directions, module-governance invariants, unresolved dependency risks, and review triggers. - -## Outline - -1. **Setup**: Run `{SCRIPT}` from repo root and parse JSON for `ARCH_FILE`, `ARCH_DIR`, `REPO_FACTS_FILE`, `SCENARIO_VIEW`, `LOGICAL_VIEW`, `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, and `PHYSICAL_VIEW`. -2. **Load templates and existing artifacts**: - - Read the seven architecture artifacts created by setup. - - Read the seven architecture templates under `.specify/extensions/arch/templates/`. -3. **Build repo fact inventory**: - - Identify repository identity, entry points, visible behaviors, boundaries, state clues, runtime clues, development structure, deployment clues, and Git history signals. - - If repository-first artifacts exist, extract first-party module edges, allowed/forbidden invocation rules, dependency matrix signals, and dependency-governance actions. - - Assign confidence: `High`, `Medium`, or `Low`. -4. **Write repo facts**: - - Fill `REPO_FACTS_FILE` using the repo facts template. - - Every non-placeholder fact must name an evidence source. -5. **Derive scenario view**: - - Derive actors, triggers, use cases, paths, branches, and acceptance semantics from repo facts. -6. **Derive logical view**: - - Derive capability boundaries, domain objects, states, relationships, and invariants from scenario facts and repo facts. -7. **Derive process view**: - - Derive runtime links, handoffs, retries, approvals, receipts, and failure closure from repo facts plus scenario and logical views. -8. **Derive development view**: - - Derive architecture-level components and package boundaries from repo facts plus logical and process views. -9. **Derive physical view**: - - Derive deployment, external systems, fact sources, observability, and operations constraints from repo facts plus process and development views. -10. **Update synthesis**: - - Update `ARCH_FILE` using the synthesis template and all completed source artifacts. -11. **Stop and report**: - - Report the seven updated paths and explicit unresolved architecture gaps. - -## Inference Rules - -- README, docs, CLI help, API route declarations, UI routes, tests, and examples primarily support scenario view. -- Package directories, module boundaries, imports, configuration ownership, and test grouping primarily support development view. -- Repository-first module invocation specs primarily support development view dependency rules, package boundary intent, synthesis constraints, and review triggers. -- Repository-first dependency matrices primarily support repo facts dependency governance signals; only signal-level conclusions should flow into architecture views. -- State fields, migrations, schema files, fixtures, domain naming, and persistence configuration may support logical view, but cannot alone prove a complete domain model. -- Workers, queues, schedulers, middleware, async jobs, transactions, retries, locks, event handlers, approvals, and receipts may support process view when evidence sources are named. -- Dockerfiles, Compose files, Kubernetes manifests, Terraform, CI/CD, environment examples, and service configuration may support physical view. -- Low-confidence facts may appear in evidence gaps or view gaps, but must not become architecture conclusions. -- Concrete class names, function names, DTO fields, database table fields, and implementation data structures may appear only as evidence sources, not architecture conclusions. - -## Confidence - -- `High`: multiple independent sources agree, or docs/tests and code entry points agree. -- `Medium`: one strong source is present, such as clear configuration, entry point, route declaration, or behavior test. -- `Low`: naming, directory structure, isolated code, or Git history suggests a fact but lacks behavior evidence. - -## Quality Gates - -- ERROR if an architecture conclusion has no repo fact source or stated external constraint source. -- ERROR if the repo facts file contains generic claims without file paths, directories, configuration, tests, or commit signals. -- ERROR if 4+1 views promote classes, functions, fields, endpoints, database structure, or implementation details into architecture conclusions. -- ERROR if repository-first dependency matrices are copied wholesale into a 4+1 architecture view instead of being summarized as architecture constraints, dependency rules, gaps, or review triggers. -- ERROR if Git history signals are used alone as architecture conclusions. -- ERROR if physical view invents deployment units not visible from repo facts. -- GAP if actors, use cases, authority boundaries, runtime processes, or deployment topology cannot be supported by repository evidence. -- GAP if the repository lacks deployment, runtime, or business documentation. Missing evidence does not block generation, but it must be explicit. - -## Quality Bar - -- `architecture-repo-facts.md` must contain enough evidence for every non-gap architecture conclusion. -- Every non-placeholder architecture conclusion must be grounded in a repo fact, scenario, object, runtime link, component boundary, deployment boundary, or stated external constraint. -- Use stable names consistently across repo facts, all five views, and synthesis. -- Keep uncertainty specific: record what is unknown, which view it affects, and which architecture conclusion cannot yet be made. -- Remove generic statements such as "scalable", "secure", "observable", or "modular" unless they name owner, affected view, scope, evidence source, and architecture consequence. diff --git a/extensions/arch/commands/speckit.arch.scenario-generate.md b/extensions/arch/commands/speckit.arch.scenario-generate.md new file mode 100644 index 0000000000..4a28eee4db --- /dev/null +++ b/extensions/arch/commands/speckit.arch.scenario-generate.md @@ -0,0 +1,68 @@ +--- +description: Generate the 4+1 scenario view from intended product and use-case context. +scripts: + sh: .specify/extensions/arch/scripts/bash/setup-arch.sh --json + ps: .specify/extensions/arch/scripts/powershell/setup-arch.ps1 -Json +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Goal + +Generate or refresh the scenario view: + +- Target view: `.specify/memory/architecture-scenario-view.md` +- Optional synthesis refresh: `.specify/memory/architecture.md` + +This is the `+1` view in 4+1 architecture. It produces actor, goal, use-case, path, branch, and acceptance semantics for the other architecture views. + +## Operating Boundaries + +- Write only `SCENARIO_VIEW`; update `ARCH_FILE` only if the five architecture views are already coherent enough to refresh synthesis without inventing content. +- Do not read, populate, or update `REPO_FACTS_FILE`. +- Read `.specify/memory/uc.md` only as optional scenario background. +- Do not modify `.specify/memory/uc.md`, `.specify/memory/constitution.md`, feature specs, plans, tasks, source code, tests, root `docs/`, deployment manifests, or runbooks. +- Stay at abstract architecture-design level. +- If context is insufficient, record a specific scenario gap instead of inventing actors, goals, business facts, or acceptance meaning. + +## Setup Bootstrap + +`{SCRIPT}` may create missing architecture memory files from templates, including non-target view placeholders and `REPO_FACTS_FILE`. Treat that as bootstrap scaffolding only. After setup, this command must populate only `SCENARIO_VIEW`, and may refresh `ARCH_FILE` only under the synthesis readiness rule below. + +If setup creates `REPO_FACTS_FILE`, leave it as-is. Do not read it as input and do not add facts to it from this generate command. + +## Structured Contract + +`ARCH_SCHEMA_FILE` is the authoritative structural contract for architecture artifacts. Build a JSON-compatible working model for every file you update, validate required sections, traceability records, gaps, and `High` / `Medium` / `Low` confidence values against the schema, then render Markdown with the corresponding template. The command owns extraction, classification, validation, and write policy; templates own Markdown layout only. + +## Synthesis Readiness + +Parse all five view paths from setup JSON: `SCENARIO_VIEW`, `LOGICAL_VIEW`, `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, and `PHYSICAL_VIEW`. + +A view is ready for synthesis only when it exists, no longer contains `NEEDS ARCH UPDATE`, and contains at least one source-backed non-gap architecture conclusion in its required sections. Unsupported or unknown items must be recorded in that view's gaps and do not make the view ready. Refresh `ARCH_FILE` only when all five views are ready and cross-view terminology is coherent. Otherwise leave `ARCH_FILE` unchanged and report the missing or not-ready views. + +## Outline + +1. Run `{SCRIPT}` from repo root and parse JSON for `ARCH_FILE`, `ARCH_SCHEMA_FILE`, `REPO_FACTS_FILE`, and all five view paths. +2. Load `SCENARIO_VIEW`, `ARCH_FILE`, `ARCH_SCHEMA_FILE`, `architecture-scenario-template.md`, and `architecture-template.md`. +3. Load `.specify/memory/uc.md` if present as supporting context. +4. Extract source-backed actors, goals, triggers, paths, branches, and acceptance semantics. +5. Normalize scenario terminology and derive scenario-level boundaries. +6. Classify each candidate as a supported scenario conclusion or a scenario gap under the schema and quality gates. +7. Render the schema-compliant scenario working model with `architecture-scenario-template.md`. +8. Apply the synthesis readiness rule. If all five view files are ready, refresh `ARCH_FILE` using `architecture-template.md`; otherwise leave synthesis untouched and report the missing or not-ready views. +9. Report updated paths and explicit scenario gaps. + +## Quality Gates + +- ERROR if the scenario view contains implementation details, components, APIs, database tables, deployment scripts, or task plans. +- ERROR if a target-view conclusion has no stated scenario source, user input source, or existing memory source. +- Unsupported target-view conclusions MUST appear only in `Scenario Gaps`, not in conclusion tables. +- ERROR if a boundary has responsibilities but no explicit non-responsibility. +- Record gaps instead of inventing business facts. diff --git a/extensions/arch/commands/speckit.arch.scenario-reverse.md b/extensions/arch/commands/speckit.arch.scenario-reverse.md new file mode 100644 index 0000000000..ca378951e3 --- /dev/null +++ b/extensions/arch/commands/speckit.arch.scenario-reverse.md @@ -0,0 +1,80 @@ +--- +description: Reverse-generate the 4+1 scenario view from observable repository facts. +scripts: + sh: .specify/extensions/arch/scripts/bash/setup-arch.sh --json + ps: .specify/extensions/arch/scripts/powershell/setup-arch.ps1 -Json +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Goal + +Reverse-generate or refresh the scenario view from observable repository evidence: + +- Evidence layer: `.specify/memory/architecture-repo-facts.md` +- Target view: `.specify/memory/architecture-scenario-view.md` +- Optional synthesis refresh: `.specify/memory/architecture.md` + +## Operating Boundaries + +- Write only `REPO_FACTS_FILE` and `SCENARIO_VIEW`; update `ARCH_FILE` only if the five architecture views are already coherent enough to refresh synthesis without inventing content. +- Do not modify source code, README files, project documentation, package files, configuration, tests, deployment files, `.specify/memory/uc.md`, `.specify/memory/constitution.md`, feature specs, plans, tasks, root `docs/`, deployment manifests, or runbooks. +- Stay at abstract architecture-design level. +- If repository evidence is insufficient, record a specific evidence gap and scenario gap instead of inventing actors, use cases, business meaning, or acceptance semantics. + +## Setup Bootstrap + +`{SCRIPT}` may create missing architecture memory files from templates, including non-target view placeholders. Treat that as bootstrap scaffolding only. After setup, this command must populate only `REPO_FACTS_FILE` and `SCENARIO_VIEW`, and may refresh `ARCH_FILE` only under the synthesis readiness rule below. + +## Evidence Sources + +Inspect README/docs, CLI/API entry points, examples, tests, route declarations, configuration, package manifests, and user-visible behavior descriptions. Infer target-view conclusions only from user-visible behavior, explicit documentation, or stated external constraints. Prefer concise inventory over exhaustive source listing. + +Repository-First Inputs: if `.specify/memory/repository-first/` exists, inspect its markdown artifacts and summarize build manifest detection, first-party module edges, invocation governance, and dependency governance signals that affect scenario meaning in `REPO_FACTS_FILE`. Repository-first content must not be copied verbatim into architecture views. + +## Repo Facts Merge Rule + +Treat `REPO_FACTS_FILE` as a cumulative evidence layer. Preserve existing non-placeholder facts from sections outside this command's evidence focus unless the cited evidence no longer exists or is contradicted by stronger evidence. When replacing, removing, or downgrading an existing fact, record the reason in `Evidence Gaps` or a review trigger instead of silently dropping it. + +## Repo Facts Evidence Rules + +- Every non-placeholder fact must name an evidence source such as a file, directory, configuration, test, script, manifest, command output, or commit signal. +- Confidence values are `High`, `Medium`, or `Low`: `High` means multiple independent sources agree; `Medium` means one strong source is present; `Low` means naming, directory structure, isolated code, or Git history suggests a fact but lacks behavior evidence. +- Git history is an auxiliary signal for change axes and boundary risks. It cannot independently prove architecture conclusions. +- Repository-first outputs are fact inputs. Summarize only architecture constraints, governance signals, gaps, or review triggers; do not copy full dependency inventories into architecture views. +- Concrete classes, functions, fields, endpoints, database tables, and implementation data structures may appear only as evidence sources, not architecture conclusions. +- Architecture conclusions must trace to repo facts, user input, or stated external constraints. Unsupported items MUST appear only in `Evidence Gaps` or the target view gaps. + +## Structured Contract + +`ARCH_SCHEMA_FILE` is the authoritative structural contract for architecture artifacts. Build JSON-compatible working models for `REPO_FACTS_FILE` and the target view, validate sections, traceability records, gaps, and `High` / `Medium` / `Low` confidence values against the schema, then render Markdown with the corresponding templates. The command owns evidence collection, classification, merge policy, validation, and write policy; templates own Markdown layout only. + +## Synthesis Readiness + +Parse all five view paths from setup JSON: `SCENARIO_VIEW`, `LOGICAL_VIEW`, `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, and `PHYSICAL_VIEW`. + +A view is ready for synthesis only when it exists, no longer contains `NEEDS ARCH UPDATE`, and contains at least one source-backed non-gap architecture conclusion in its required sections. Unsupported or unknown items must be recorded in that view's gaps and do not make the view ready. Refresh `ARCH_FILE` only when all five views are ready and cross-view terminology is coherent. Otherwise leave `ARCH_FILE` unchanged and report the missing or not-ready views. + +## Outline + +1. Run `{SCRIPT}` from repo root and parse JSON for `ARCH_FILE`, `ARCH_SCHEMA_FILE`, `REPO_FACTS_FILE`, and all five view paths. +2. Load `REPO_FACTS_FILE`, `SCENARIO_VIEW`, `ARCH_SCHEMA_FILE`, `architecture-repo-facts-template.md`, `architecture-scenario-template.md`, and `architecture-template.md`. +3. Refresh repo facts for this command's evidence focus and classify unsupported observations as evidence gaps. +4. Populate a schema-compliant scenario working model from high- or medium-confidence facts. +5. Put low-confidence or missing business meaning in evidence gaps and scenario gaps. +6. Apply the repo facts merge rule before writing `REPO_FACTS_FILE`. +7. Apply the synthesis readiness rule. If all five view files are ready, refresh `ARCH_FILE`; otherwise leave synthesis untouched and report the missing or not-ready views. +8. Report updated paths and explicit unresolved gaps. + +## Quality Gates + +- ERROR if a target-view conclusion has no repo fact source or stated external constraint source. +- ERROR if the repo facts file contains generic claims without file paths, directories, configuration, tests, commands, or commit signals. +- ERROR if Git history is used alone as a scenario conclusion. +- GAP if actors, goals, or business meaning cannot be supported by repository evidence. diff --git a/extensions/arch/extension.yml b/extensions/arch/extension.yml index 10a8f07fef..2a1bfdb0fd 100644 --- a/extensions/arch/extension.yml +++ b/extensions/arch/extension.yml @@ -3,8 +3,8 @@ schema_version: "1.0" extension: id: arch name: "Architecture Workflow" - version: "1.1.0" - description: "Generate or reverse project-level 4+1 architecture view artifacts and synthesis" + version: "1.2.1" + description: "Generate or reverse project-level 4+1 architecture views as separate commands" author: bigsmartben repository: https://github.com/bigsmartben/spec-kit-arch homepage: https://github.com/bigsmartben/spec-kit-arch @@ -15,12 +15,36 @@ requires: provides: commands: - - name: speckit.arch.generate - file: commands/speckit.arch.generate.md - description: "Execute the 4+1 architecture workflow and generate architecture view artifacts" - - name: speckit.arch.reverse - file: commands/speckit.arch.reverse.md - description: "Reverse-generate 4+1 architecture artifacts from observable repository facts" + - name: speckit.arch.scenario-generate + file: commands/speckit.arch.scenario-generate.md + description: "Generate the 4+1 scenario view from intended product and use-case context" + - name: speckit.arch.logical-generate + file: commands/speckit.arch.logical-generate.md + description: "Generate the 4+1 logical view from scenario architecture semantics" + - name: speckit.arch.process-generate + file: commands/speckit.arch.process-generate.md + description: "Generate the 4+1 process view from scenario and logical architecture views" + - name: speckit.arch.development-generate + file: commands/speckit.arch.development-generate.md + description: "Generate the 4+1 development view from logical and process architecture views" + - name: speckit.arch.physical-generate + file: commands/speckit.arch.physical-generate.md + description: "Generate the 4+1 physical view from process and development architecture views" + - name: speckit.arch.scenario-reverse + file: commands/speckit.arch.scenario-reverse.md + description: "Reverse-generate the 4+1 scenario view from observable repository facts" + - name: speckit.arch.logical-reverse + file: commands/speckit.arch.logical-reverse.md + description: "Reverse-generate the 4+1 logical view from observable repository facts" + - name: speckit.arch.process-reverse + file: commands/speckit.arch.process-reverse.md + description: "Reverse-generate the 4+1 process view from observable repository facts" + - name: speckit.arch.development-reverse + file: commands/speckit.arch.development-reverse.md + description: "Reverse-generate the 4+1 development view from observable repository facts" + - name: speckit.arch.physical-reverse + file: commands/speckit.arch.physical-reverse.md + description: "Reverse-generate the 4+1 physical view from observable repository facts" tags: - architecture diff --git a/extensions/arch/schemas/architecture-artifacts.schema.json b/extensions/arch/schemas/architecture-artifacts.schema.json new file mode 100644 index 0000000000..e970065047 --- /dev/null +++ b/extensions/arch/schemas/architecture-artifacts.schema.json @@ -0,0 +1,159 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/bigsmartben/spec-kit-arch/schemas/architecture-artifacts.schema.json", + "title": "Spec Kit Architecture Artifact Contract", + "type": "object", + "additionalProperties": false, + "required": [ + "artifactType", + "project", + "sections", + "traceability", + "gaps" + ], + "properties": { + "artifactType": { + "type": "string", + "enum": [ + "architecture-synthesis", + "repo-facts", + "scenario-view", + "logical-view", + "process-view", + "development-view", + "physical-view" + ] + }, + "project": { + "type": "string", + "minLength": 1 + }, + "sourceMode": { + "type": "string", + "enum": [ + "generate", + "reverse", + "synthesis" + ] + }, + "sections": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/section" + } + }, + "traceability": { + "type": "array", + "items": { + "$ref": "#/$defs/trace" + } + }, + "gaps": { + "type": "array", + "items": { + "$ref": "#/$defs/gap" + } + } + }, + "$defs": { + "section": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "title", + "records" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "title": { + "type": "string", + "minLength": 1 + }, + "records": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": [ + "string", + "number", + "boolean", + "array", + "object", + "null" + ] + }, + "not": { + "required": [ + "NEEDS ARCH UPDATE" + ] + } + } + } + } + }, + "trace": { + "type": "object", + "additionalProperties": false, + "required": [ + "conclusion", + "sourceType", + "sourceReference", + "confidence" + ], + "properties": { + "conclusion": { + "type": "string", + "minLength": 1 + }, + "sourceType": { + "type": "string", + "minLength": 1 + }, + "sourceReference": { + "type": "string", + "minLength": 1 + }, + "confidence": { + "$ref": "#/$defs/confidence" + } + } + }, + "gap": { + "type": "object", + "additionalProperties": false, + "required": [ + "gap", + "affectedArea", + "reason" + ], + "properties": { + "gap": { + "type": "string", + "minLength": 1 + }, + "affectedArea": { + "type": "string", + "minLength": 1 + }, + "reason": { + "type": "string", + "minLength": 1 + } + } + }, + "confidence": { + "type": "string", + "enum": [ + "High", + "Medium", + "Low" + ] + } + } +} diff --git a/extensions/arch/scripts/bash/setup-arch.sh b/extensions/arch/scripts/bash/setup-arch.sh index f840541f98..1058c5b4d6 100755 --- a/extensions/arch/scripts/bash/setup-arch.sh +++ b/extensions/arch/scripts/bash/setup-arch.sh @@ -96,6 +96,8 @@ resolve_architecture_template() { REPO_ROOT=$(get_repo_root) ARCH_DIR="$REPO_ROOT/.specify/memory" +SCHEMA_DIR="$REPO_ROOT/.specify/extensions/arch/schemas" +ARCH_SCHEMA_FILE="$SCHEMA_DIR/architecture-artifacts.schema.json" ARCH_FILE="$ARCH_DIR/architecture.md" REPO_FACTS_FILE="$ARCH_DIR/architecture-repo-facts.md" SCENARIO_VIEW="$ARCH_DIR/architecture-scenario-view.md" @@ -142,17 +144,21 @@ if $JSON_MODE; then jq -cn \ --arg arch_file "$ARCH_FILE" \ --arg arch_dir "$ARCH_DIR" \ + --arg schema_dir "$SCHEMA_DIR" \ + --arg arch_schema_file "$ARCH_SCHEMA_FILE" \ --arg repo_facts_file "$REPO_FACTS_FILE" \ --arg scenario_view "$SCENARIO_VIEW" \ --arg logical_view "$LOGICAL_VIEW" \ --arg process_view "$PROCESS_VIEW" \ --arg development_view "$DEVELOPMENT_VIEW" \ --arg physical_view "$PHYSICAL_VIEW" \ - '{ARCH_FILE:$arch_file,ARCH_DIR:$arch_dir,REPO_FACTS_FILE:$repo_facts_file,SCENARIO_VIEW:$scenario_view,LOGICAL_VIEW:$logical_view,PROCESS_VIEW:$process_view,DEVELOPMENT_VIEW:$development_view,PHYSICAL_VIEW:$physical_view}' + '{ARCH_FILE:$arch_file,ARCH_DIR:$arch_dir,SCHEMA_DIR:$schema_dir,ARCH_SCHEMA_FILE:$arch_schema_file,REPO_FACTS_FILE:$repo_facts_file,SCENARIO_VIEW:$scenario_view,LOGICAL_VIEW:$logical_view,PROCESS_VIEW:$process_view,DEVELOPMENT_VIEW:$development_view,PHYSICAL_VIEW:$physical_view}' else - printf '{"ARCH_FILE":"%s","ARCH_DIR":"%s","REPO_FACTS_FILE":"%s","SCENARIO_VIEW":"%s","LOGICAL_VIEW":"%s","PROCESS_VIEW":"%s","DEVELOPMENT_VIEW":"%s","PHYSICAL_VIEW":"%s"}\n' \ + printf '{"ARCH_FILE":"%s","ARCH_DIR":"%s","SCHEMA_DIR":"%s","ARCH_SCHEMA_FILE":"%s","REPO_FACTS_FILE":"%s","SCENARIO_VIEW":"%s","LOGICAL_VIEW":"%s","PROCESS_VIEW":"%s","DEVELOPMENT_VIEW":"%s","PHYSICAL_VIEW":"%s"}\n' \ "$(json_escape "$ARCH_FILE")" \ "$(json_escape "$ARCH_DIR")" \ + "$(json_escape "$SCHEMA_DIR")" \ + "$(json_escape "$ARCH_SCHEMA_FILE")" \ "$(json_escape "$REPO_FACTS_FILE")" \ "$(json_escape "$SCENARIO_VIEW")" \ "$(json_escape "$LOGICAL_VIEW")" \ @@ -163,6 +169,8 @@ if $JSON_MODE; then else echo "ARCH_FILE: $ARCH_FILE" echo "ARCH_DIR: $ARCH_DIR" + echo "SCHEMA_DIR: $SCHEMA_DIR" + echo "ARCH_SCHEMA_FILE: $ARCH_SCHEMA_FILE" echo "REPO_FACTS_FILE: $REPO_FACTS_FILE" echo "SCENARIO_VIEW: $SCENARIO_VIEW" echo "LOGICAL_VIEW: $LOGICAL_VIEW" diff --git a/extensions/arch/scripts/powershell/setup-arch.ps1 b/extensions/arch/scripts/powershell/setup-arch.ps1 index 822478ceb9..70e5c94d5e 100644 --- a/extensions/arch/scripts/powershell/setup-arch.ps1 +++ b/extensions/arch/scripts/powershell/setup-arch.ps1 @@ -82,6 +82,8 @@ function Convert-ToPlainPath { $repoRoot = Convert-ToPlainPath (Get-RepoRoot) $archDir = Join-Path $repoRoot ".specify/memory" +$schemaDir = Join-Path $repoRoot ".specify/extensions/arch/schemas" +$archSchemaFile = Join-Path $schemaDir "architecture-artifacts.schema.json" $archFile = Join-Path $archDir "architecture.md" $repoFactsFile = Join-Path $archDir "architecture-repo-facts.md" $scenarioView = Join-Path $archDir "architecture-scenario-view.md" @@ -128,6 +130,8 @@ if ($Json) { [PSCustomObject]@{ ARCH_FILE = $archFile ARCH_DIR = $archDir + SCHEMA_DIR = $schemaDir + ARCH_SCHEMA_FILE = $archSchemaFile REPO_FACTS_FILE = $repoFactsFile SCENARIO_VIEW = $scenarioView LOGICAL_VIEW = $logicalView @@ -138,6 +142,8 @@ if ($Json) { } else { Write-Output "ARCH_FILE: $archFile" Write-Output "ARCH_DIR: $archDir" + Write-Output "SCHEMA_DIR: $schemaDir" + Write-Output "ARCH_SCHEMA_FILE: $archSchemaFile" Write-Output "REPO_FACTS_FILE: $repoFactsFile" Write-Output "SCENARIO_VIEW: $scenarioView" Write-Output "LOGICAL_VIEW: $logicalView" diff --git a/extensions/arch/templates/architecture-development-template.md b/extensions/arch/templates/architecture-development-template.md index 59bcb47136..f4786aa4f2 100644 --- a/extensions/arch/templates/architecture-development-template.md +++ b/extensions/arch/templates/architecture-development-template.md @@ -2,27 +2,27 @@ **Input**: `.specify/memory/architecture-logical-view.md`, `.specify/memory/architecture-process-view.md` -**Purpose**: Derive architecture-level components, package boundary intent, contract/artifact semantics, and dependency rules from logical and process views. +**Purpose**: Architecture-level components, package boundary intent, contract/artifact semantics, and dependency rules from logical and process views. ## Architecture Intent -[State what component, package, contract, or dependency meaning this view must preserve.] +[Component, package, contract, or dependency decision and source.] ## Core Tensions -| Tension | Current Tradeoff Direction | Development Consequence | +| Tension | Chosen Architectural Direction | Development Consequence | |---------|----------------------------|-------------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | ## Stable Boundaries -| Boundary | Must Remain Stable Because | Explicitly Must Not Own | +| Boundary | Must Remain Stable Because | Excluded Responsibility / Scope | |----------|----------------------------|-------------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | ## Change Axes -| Expected Change | Isolated By | Development Impact | +| Expected Change | Isolation Mechanism / Boundary Rule | Development Impact | |-----------------|-------------|--------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | @@ -40,7 +40,7 @@ ## Architecture-Level Components -| Component / Capability Package | Responsibility | Input / Output Boundary | Collaborators | Explicitly Must Not Own | Source View Evidence | +| Component / Capability Package | Responsibility | Input / Output Boundary | Collaborators | Excluded Responsibility / Scope | Source Reference | |--------------------------------|----------------|-------------------------|---------------|--------------------------|----------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | @@ -62,12 +62,14 @@ |------|-------------------|---------------------|--------|------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | +## Source Traceability + +| Architecture Conclusion | Source Type | Source Reference | Confidence | +|-------------------------|-------------|------------------|------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + ## Development View Gaps | Gap | Affected Component / Boundary | Why It Matters | |-----|-------------------------------|----------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | - -## Prohibited Content - -Do not write source file paths, concrete package trees, classes, functions, implementation tasks, framework-specific wiring, or code generation notes here. diff --git a/extensions/arch/templates/architecture-logical-template.md b/extensions/arch/templates/architecture-logical-template.md index 3e6514b850..22d7de1439 100644 --- a/extensions/arch/templates/architecture-logical-template.md +++ b/extensions/arch/templates/architecture-logical-template.md @@ -2,27 +2,27 @@ **Input**: `.specify/memory/architecture-scenario-view.md` -**Purpose**: Derive capability boundaries, domain objects, states, relationships, and invariants from the scenario view. +**Purpose**: Capability boundaries, domain objects, states, relationships, and invariants from the scenario view. ## Architecture Intent -[State what logical separation, authority, or lifecycle meaning this view must preserve.] +[Logical authority, boundary, or lifecycle decision and source.] ## Core Tensions -| Tension | Current Tradeoff Direction | Logical Consequence | +| Tension | Chosen Architectural Direction | Logical Consequence | |---------|----------------------------|---------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | ## Stable Boundaries -| Boundary | Must Remain Stable Because | Explicitly Does Not Own | +| Boundary | Must Remain Stable Because | Excluded Responsibility / Scope | |----------|----------------------------|-------------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | ## Change Axes -| Expected Change | Isolated By | Logical Impact | +| Expected Change | Isolation Mechanism / Boundary Rule | Logical Impact | |-----------------|-------------|----------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | @@ -40,7 +40,7 @@ ## Capability Boundaries -| Capability / Boundary | Responsibility | Input | Output | Explicitly Does Not Own | Scenario Source | +| Capability / Boundary | Responsibility | Input | Output | Excluded Responsibility / Scope | Scenario Source | |-----------------------|----------------|-------|--------|--------------------------|-----------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | @@ -62,12 +62,14 @@ |----------|-------|------------------|---------------------------|-------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | +## Source Traceability + +| Architecture Conclusion | Source Type | Source Reference | Confidence | +|-------------------------|-------------|------------------|------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + ## Logical Gaps | Gap | Affected Capability / Object | Why It Matters | |-----|------------------------------|----------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | - -## Prohibited Content - -Do not write classes, DTOs, database tables, fields, method names, endpoints, schemas, or implementation data structures here. diff --git a/extensions/arch/templates/architecture-physical-template.md b/extensions/arch/templates/architecture-physical-template.md index a300eaaaa5..335cb8dbe0 100644 --- a/extensions/arch/templates/architecture-physical-template.md +++ b/extensions/arch/templates/architecture-physical-template.md @@ -2,27 +2,27 @@ **Input**: `.specify/memory/architecture-process-view.md`, `.specify/memory/architecture-development-view.md` -**Purpose**: Derive deployment, hosting, external system, fact-source, observability, and operational boundaries from process and development views. +**Purpose**: Deployment, hosting, external system, fact-source, observability, and operational boundaries from process and development views. ## Architecture Intent -[State what deployment, fact-source, operational, or external-boundary meaning this view must preserve.] +[Deployment, fact-source, operational, or external-boundary decision and source.] ## Core Tensions -| Tension | Current Tradeoff Direction | Physical Consequence | +| Tension | Chosen Architectural Direction | Physical Consequence | |---------|----------------------------|----------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | ## Stable Boundaries -| Boundary | Must Remain Stable Because | Explicitly Does Not Carry | +| Boundary | Must Remain Stable Because | Excluded Responsibility / Scope | |----------|----------------------------|---------------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | ## Change Axes -| Expected Change | Isolated By | Physical Impact | +| Expected Change | Isolation Mechanism / Boundary Rule | Physical Impact | |-----------------|-------------|-----------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | @@ -58,16 +58,18 @@ ## Operations and Release Boundaries -| Operational Concern | Responsible Boundary | Trigger | Affected Views | Architecture Consequence | +| Operational Concern | Responsible Boundary | Trigger | Affected Views | Review Trigger / Architecture Consequence | |---------------------|----------------------|---------|----------------|--------------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | +## Source Traceability + +| Architecture Conclusion | Source Type | Source Reference | Confidence | +|-------------------------|-------------|------------------|------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + ## Physical View Gaps | Gap | Affected Deployment / External Boundary | Why It Matters | |-----|-----------------------------------------|----------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | - -## Prohibited Content - -Do not write Kubernetes YAML, cloud resource manifests, machine sizes, service SKUs, deployment scripts, runbooks, or concrete infrastructure configuration here. diff --git a/extensions/arch/templates/architecture-process-template.md b/extensions/arch/templates/architecture-process-template.md index a24bce6ca8..3de19c8ab8 100644 --- a/extensions/arch/templates/architecture-process-template.md +++ b/extensions/arch/templates/architecture-process-template.md @@ -2,27 +2,27 @@ **Input**: `.specify/memory/architecture-scenario-view.md`, `.specify/memory/architecture-logical-view.md` -**Purpose**: Derive runtime collaboration, handoffs, approvals, receipts, state advancement, and failure closure from scenario paths and logical boundaries. +**Purpose**: Runtime collaboration, handoffs, approvals, receipts, state advancement, and failure closure from scenario paths and logical boundaries. ## Architecture Intent -[State what runtime collaboration, handoff, or failure-closure meaning this view must preserve.] +[Runtime collaboration, handoff, or failure-closure decision and source.] ## Core Tensions -| Tension | Current Tradeoff Direction | Process Consequence | +| Tension | Chosen Architectural Direction | Process Consequence | |---------|----------------------------|---------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | ## Stable Boundaries -| Boundary | Must Remain Stable Because | Explicitly Does Not Control | +| Boundary | Must Remain Stable Because | Excluded Responsibility / Scope | |----------|----------------------------|-----------------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | ## Change Axes -| Expected Change | Isolated By | Process Impact | +| Expected Change | Isolation Mechanism / Boundary Rule | Process Impact | |-----------------|-------------|----------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | @@ -62,12 +62,14 @@ |------------------|--------------------|----------------------|-----------------------------|---------------------|-------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | +## Source Traceability + +| Architecture Conclusion | Source Type | Source Reference | Confidence | +|-------------------------|-------------|------------------|------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + ## Process Gaps | Gap | Affected Runtime Link / Scenario | Why It Matters | |-----|----------------------------------|----------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | - -## Prohibited Content - -Do not write call stacks, queue names, retry counts, thread/process details, endpoint sequences, workflow engine configuration, or orchestration code here. diff --git a/extensions/arch/templates/architecture-repo-facts-template.md b/extensions/arch/templates/architecture-repo-facts-template.md index c7fc8dcc09..888f6660ad 100644 --- a/extensions/arch/templates/architecture-repo-facts-template.md +++ b/extensions/arch/templates/architecture-repo-facts-template.md @@ -1,8 +1,8 @@ # Architecture Repo Facts: [PROJECT] -**Purpose**: Record observable repository facts that support reverse generation of the project-level 4+1 architecture artifacts. +**Purpose**: Observable repository facts that support reverse generation of the project-level 4+1 architecture artifacts. -**Note**: This evidence file is filled in by the `__SPECKIT_COMMAND_ARCH_REVERSE__` command before the architecture views are updated. It is a fact source for architecture reasoning, not an implementation audit report. +**Scope**: Observable repository evidence for architecture reasoning, not an implementation audit report. ## Repository Identity @@ -48,7 +48,7 @@ ## Repository-First Projection -Record repository-first evidence only when `.specify/memory/repository-first/` exists. Leave explicit gaps when the directory or expected artifacts are absent. +Repository-first signals relevant to architecture evidence. ### Build Manifest Detection @@ -91,15 +91,3 @@ Record repository-first evidence only when `.specify/memory/repository-first/` e | Gap | Affected View | Why It Blocks Architecture Conclusion | |-----|---------------|----------------------------------------| | NEEDS REPO FACTS UPDATE | NEEDS REPO FACTS UPDATE | NEEDS REPO FACTS UPDATE | - -## Evidence Rules - -- Every non-placeholder fact must name an evidence source such as a file, directory, configuration, test, script, manifest, command output, or commit signal. -- Confidence values are `High`, `Medium`, or `Low`. -- `High` means multiple independent sources agree, or docs/tests and code entry points agree. -- `Medium` means one strong source is present, such as clear configuration, entry point, route declaration, or behavior test. -- `Low` means naming, directory structure, isolated code, or Git history suggests a fact but lacks behavior evidence. -- Git history is an auxiliary signal for change axes and boundary risks. It cannot independently prove architecture conclusions. -- Repository-first dependency matrices are fact inputs. Do not copy full dependency inventories into architecture views; summarize only architecture constraints, governance signals, gaps, or review triggers. -- Repository-first module invocation specs may support development-view dependency rules when each rule maps to a concrete module edge or dependency signal. -- Concrete classes, functions, fields, endpoints, database tables, and implementation data structures may appear only as evidence sources, not architecture conclusions. diff --git a/extensions/arch/templates/architecture-scenario-template.md b/extensions/arch/templates/architecture-scenario-template.md index 08026e3258..4055221481 100644 --- a/extensions/arch/templates/architecture-scenario-template.md +++ b/extensions/arch/templates/architecture-scenario-template.md @@ -1,26 +1,26 @@ # Scenario View -**Purpose**: Produce the UC semantics for the architecture workflow. This view is the source for the logical, process, development, and physical views. +**Purpose**: Use-case semantics for the architecture workflow. This view is the source for the logical, process, development, and physical views. ## Architecture Intent -[State what scenario-level meaning this view must stabilize for later architecture decisions.] +[Scenario-level decision, boundary, and source.] ## Core Tensions -| Tension | Current Tradeoff Direction | Scenario Consequence | +| Tension | Chosen Architectural Direction | Scenario Consequence | |---------|----------------------------|----------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | ## Stable Boundaries -| Boundary | Must Remain Stable Because | Explicitly Does Not Cover | +| Boundary | Must Remain Stable Because | Excluded Responsibility / Scope | |----------|----------------------------|---------------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | ## Change Axes -| Expected Change | Isolated By | Scenario Impact | +| Expected Change | Isolation Mechanism / Boundary Rule | Scenario Impact | |-----------------|-------------|-----------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | @@ -60,12 +60,14 @@ |---------------------|-------------------|-----------|-------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | +## Source Traceability + +| Architecture Conclusion | Source Type | Source Reference | Confidence | +|-------------------------|-------------|------------------|------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + ## Scenario Gaps | Gap | Affected Scenario | Why It Matters | |-----|-------------------|----------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | - -## Prohibited Content - -Do not write architecture components, class designs, APIs, database tables, implementation tasks, test strategy, deployment scripts, or framework choices here. diff --git a/extensions/arch/templates/architecture-template.md b/extensions/arch/templates/architecture-template.md index a3f79d20be..a0572d2eb1 100644 --- a/extensions/arch/templates/architecture-template.md +++ b/extensions/arch/templates/architecture-template.md @@ -7,13 +7,11 @@ - Development: `.specify/memory/architecture-development-view.md` - Physical: `.specify/memory/architecture-physical-view.md` -**Note**: This synthesis is filled in by the `__SPECKIT_COMMAND_ARCH__` command after the five 4+1 view files are updated. - ## View Index | View | File | Purpose | Current Status | |------|------|---------|----------------| -| Scenario | `.specify/memory/architecture-scenario-view.md` | UC-producing actor, use case, path, branch, and acceptance semantics | NEEDS ARCH UPDATE | +| Scenario | `.specify/memory/architecture-scenario-view.md` | Use-case actor, path, branch, and acceptance semantics | NEEDS ARCH UPDATE | | Logical | `.specify/memory/architecture-logical-view.md` | Capability boundaries, domain objects, states, and invariants | NEEDS ARCH UPDATE | | Process | `.specify/memory/architecture-process-view.md` | Runtime links, handoffs, approvals, receipts, failure closure | NEEDS ARCH UPDATE | | Development | `.specify/memory/architecture-development-view.md` | Architecture-level components, package boundaries, contracts, dependencies | NEEDS ARCH UPDATE | @@ -21,11 +19,11 @@ ## Architecture Intent -[State what architecture intent the five views stabilize together.] +[Cross-view architecture decision, affected boundary, and input views.] ## Central Design Forces -[Summarize the central design forces that connect the five views: primary scenario flow, authority boundary, fact-source model, collaboration model, deployment constraint, or failure-closure model.] +[Primary scenario flow, authority boundary, fact-source model, collaboration model, deployment constraint, or failure-closure model that connects the five views.] ## Primary Tradeoffs @@ -41,7 +39,7 @@ ## Change Axes -| Expected Change | Isolated By | Affected Views | Architecture Consequence | +| Expected Change | Isolation Mechanism / Boundary Rule | Affected Views | Architecture Consequence | |-----------------|-------------|----------------|--------------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | @@ -53,7 +51,7 @@ ## Cross-View Architecture Model -This section normalizes the 4+1 design results into the architecture SSOT. Record how concepts derive, constrain, depend on, or guard each other. This is architecture design synthesis, not tracking or audit. Do not treat view-specific concepts as equivalent or interchangeable. +Cross-view mappings between view-specific concepts, constraints, dependencies, and guardrails. | Architecture Concept | Scenario Meaning | Logical Interpretation | Runtime Role | Development Boundary | Physical Constraint | Architecture Constraint | |----------------------|------------------|------------------------|--------------|----------------------|---------------------|---------------------------| diff --git a/extensions/arch/tests/repository-first-contract.sh b/extensions/arch/tests/repository-first-contract.sh index ce894e39ea..87dd14dc76 100755 --- a/extensions/arch/tests/repository-first-contract.sh +++ b/extensions/arch/tests/repository-first-contract.sh @@ -1,10 +1,96 @@ #!/usr/bin/env bash set -euo pipefail +trap 'echo "repository-first-contract failed at line $LINENO: $BASH_COMMAND" >&2' ERR -rg -n "Repository-First Inputs" commands/speckit.arch.reverse.md >/dev/null -rg -n "repository-first/technical-dependency-matrix.md" commands/speckit.arch.reverse.md >/dev/null -rg -n "Repository-First Projection" templates/architecture-repo-facts-template.md >/dev/null -rg -n "Module Invocation Governance" templates/architecture-repo-facts-template.md >/dev/null -rg -n "Dependency Governance Signals" templates/architecture-repo-facts-template.md >/dev/null -rg -n "must not be copied verbatim into 4\\+1 views|must not be copied verbatim into architecture views" commands/speckit.arch.reverse.md >/dev/null -rg -n "repository-first" README.md CHANGELOG.md CATALOG-SUBMISSION.md >/dev/null +if command -v rg >/dev/null 2>&1 && rg --version >/dev/null 2>&1; then + search() { + rg -n "$@" + } +else + search() { + local pattern="$1" + shift + grep -R -n -E "$pattern" "$@" + } +fi + +views=(scenario logical process development physical) + +for view in "${views[@]}"; do + search "name: speckit\\.arch\\.${view}-generate" extension.yml >/dev/null + search "file: commands/speckit\\.arch\\.${view}-generate\\.md" extension.yml >/dev/null + test -f "commands/speckit.arch.${view}-generate.md" + search 'Target view|Write only|Do not read, populate, or update `REPO_FACTS_FILE`|Setup Bootstrap|Synthesis Readiness|all five view paths' "commands/speckit.arch.${view}-generate.md" >/dev/null + search "Setup Bootstrap" "commands/speckit.arch.${view}-generate.md" >/dev/null + search "Synthesis Readiness" "commands/speckit.arch.${view}-generate.md" >/dev/null + search "all five view paths" "commands/speckit.arch.${view}-generate.md" >/dev/null + search "source-backed non-gap architecture conclusion" "commands/speckit.arch.${view}-generate.md" >/dev/null + search "Structured Contract" "commands/speckit.arch.${view}-generate.md" >/dev/null + search "ARCH_SCHEMA_FILE" "commands/speckit.arch.${view}-generate.md" >/dev/null + + search "name: speckit\\.arch\\.${view}-reverse" extension.yml >/dev/null + search "file: commands/speckit\\.arch\\.${view}-reverse\\.md" extension.yml >/dev/null + test -f "commands/speckit.arch.${view}-reverse.md" + search "Evidence layer|Repository-First Inputs|REPO_FACTS_FILE|Repo Facts Merge Rule|Synthesis Readiness|all five view paths" "commands/speckit.arch.${view}-reverse.md" >/dev/null + search "Repo Facts Merge Rule" "commands/speckit.arch.${view}-reverse.md" >/dev/null + search "Repo Facts Evidence Rules" "commands/speckit.arch.${view}-reverse.md" >/dev/null + search "Synthesis Readiness" "commands/speckit.arch.${view}-reverse.md" >/dev/null + search "all five view paths" "commands/speckit.arch.${view}-reverse.md" >/dev/null + search "Git history is used alone" "commands/speckit.arch.${view}-reverse.md" >/dev/null + search "Unsupported .*MUST appear only" "commands/speckit.arch.${view}-reverse.md" >/dev/null + search "Structured Contract" "commands/speckit.arch.${view}-reverse.md" >/dev/null + search "ARCH_SCHEMA_FILE" "commands/speckit.arch.${view}-reverse.md" >/dev/null + + search "Source Traceability" "templates/architecture-${view}-template.md" >/dev/null +done + +if search "name: speckit\\.arch\\.(generate|reverse)$|name: speckit\\.arch\\.[a-z]+\\.(generate|reverse)$" extension.yml >/dev/null; then + echo "legacy broad arch commands must not be registered" >&2 + exit 1 +fi + +search "Commands count: 10" CATALOG-SUBMISSION.md >/dev/null +search "speckit.arch.scenario-generate" README.md >/dev/null +search "speckit.arch.physical-reverse" README.md >/dev/null +search "legacy .*speckit\\.arch\\.generate.*speckit\\.arch\\.reverse" README.md >/dev/null +search "Repository-First Projection" templates/architecture-repo-facts-template.md >/dev/null +search "Module Invocation Governance" templates/architecture-repo-facts-template.md >/dev/null +search "Dependency Governance Signals" templates/architecture-repo-facts-template.md >/dev/null +test -f schemas/architecture-artifacts.schema.json +search '"artifactType"' schemas/architecture-artifacts.schema.json >/dev/null +search '"traceability"' schemas/architecture-artifacts.schema.json >/dev/null +search '"confidence"' schemas/architecture-artifacts.schema.json >/dev/null +search '"High"|\"Medium\"|\"Low\"' schemas/architecture-artifacts.schema.json >/dev/null +search "Preserve existing non-placeholder facts" commands/speckit.arch.development-reverse.md >/dev/null +search "must not be copied verbatim into 4\\+1 views|must not be copied verbatim into architecture views" commands/speckit.arch.development-reverse.md >/dev/null +search "Git history is used alone as a development conclusion" commands/speckit.arch.development-reverse.md >/dev/null + +if search "Evidence Rules|Prohibited Content|Move unsupported|One sentence|Identify the|List the|Record how|Do not treat|Record observable|projected into" templates >/dev/null; then + echo "templates must not carry command execution rules" >&2 + exit 1 +fi + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT +git -C "$tmpdir" init >/dev/null +mkdir -p "$tmpdir/.specify/extensions/arch" +cp -R commands scripts templates schemas "$tmpdir/.specify/extensions/arch/" +( + cd "$tmpdir" + setup_json=$(.specify/extensions/arch/scripts/bash/setup-arch.sh --json) + printf '%s\n' "$setup_json" | search '"ARCH_SCHEMA_FILE"' - >/dev/null + test -f .specify/memory/architecture.md + test -f .specify/memory/architecture-repo-facts.md + test -f .specify/memory/architecture-scenario-view.md + test -f .specify/memory/architecture-logical-view.md + test -f .specify/memory/architecture-process-view.md + test -f .specify/memory/architecture-development-view.md + test -f .specify/memory/architecture-physical-view.md + test -f .specify/extensions/arch/schemas/architecture-artifacts.schema.json + + printf 'custom sentinel\n' > .specify/memory/architecture-scenario-view.md + .specify/extensions/arch/scripts/bash/setup-arch.sh --json >/dev/null + search "custom sentinel" .specify/memory/architecture-scenario-view.md >/dev/null + + find .specify/memory -maxdepth 1 -type f | wc -l | search '^[[:space:]]*7[[:space:]]*$' - >/dev/null +) diff --git a/extensions/catalog.community.json b/extensions/catalog.community.json index 40a2f69a66..4a8a8dae10 100644 --- a/extensions/catalog.community.json +++ b/extensions/catalog.community.json @@ -1410,10 +1410,10 @@ "intake": { "name": "Intake", "id": "intake", - "description": "Extract and validate SDD-ready intake artifacts from PRDs, visual designs, test cases, and other software sources", + "description": "Normalize PRD, design, and test-case evidence into SDD-ready intake artifacts", "author": "bigsmartben", - "version": "0.1.1", - "download_url": "https://github.com/bigsmartben/spec-kit-intake/archive/refs/tags/v0.1.1.zip", + "version": "0.1.2", + "download_url": "https://github.com/bigsmartben/spec-kit-intake/archive/refs/tags/v0.1.2.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", @@ -1436,11 +1436,10 @@ }, "tags": [ "intake", - "prd", - "test-cases", - "visual-design", + "sdd", "requirements", - "validation" + "validation", + "figma" ], "verified": false, "downloads": 0, diff --git a/extensions/catalog.json b/extensions/catalog.json index e3706c1264..02486c0658 100644 --- a/extensions/catalog.json +++ b/extensions/catalog.json @@ -20,8 +20,8 @@ "arch": { "name": "Architecture Workflow", "id": "arch", - "version": "1.1.0", - "description": "Generate or reverse project-level 4+1 architecture view artifacts and synthesis", + "version": "1.2.1", + "description": "Generate or reverse project-level 4+1 architecture views as separate commands", "author": "bigsmartben", "repository": "https://github.com/bigsmartben/spec-kit-arch", "license": "MIT", @@ -30,7 +30,7 @@ "speckit_version": ">=0.8.10.dev0" }, "provides": { - "commands": 2 + "commands": 10 }, "tags": [ "architecture", @@ -69,6 +69,30 @@ "core" ] }, + "intake": { + "name": "Intake", + "id": "intake", + "version": "0.1.2", + "description": "Normalize PRD, design, 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" + ] + }, "preview": { "name": "Spec Kit Preview", "id": "preview", diff --git a/extensions/discovery/.gitignore b/extensions/discovery/.gitignore index 84ffb1d8ee..2380a9307f 100644 --- a/extensions/discovery/.gitignore +++ b/extensions/discovery/.gitignore @@ -1,5 +1,12 @@ .DS_Store Thumbs.db +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ # Local editor state .idea/ @@ -7,10 +14,23 @@ Thumbs.db *.swp *.swo +# Python environments and builds +.venv/ +venv/ +dist/ +build/ +*.egg-info/ +*.zip +.env +.env.* +!.env.example + # Spec Kit local-only config generated by installed extensions .specify/extensions/**/*-config.local.yml # Logs and temporary files *.log +.tmp/ tmp/ temp/ +.worktrees/ diff --git a/extensions/intake/.extensionignore b/extensions/intake/.extensionignore new file mode 100644 index 0000000000..24052f3cc5 --- /dev/null +++ b/extensions/intake/.extensionignore @@ -0,0 +1,9 @@ +.git/ +.github/ +.gitignore +.pytest_cache/ +.tmp/ +__pycache__/ +*.pyc +tests/ +docs/ diff --git a/extensions/intake/.gitignore b/extensions/intake/.gitignore new file mode 100644 index 0000000000..1e94d05bf7 --- /dev/null +++ b/extensions/intake/.gitignore @@ -0,0 +1,23 @@ +__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 new file mode 100644 index 0000000000..1d9c955434 --- /dev/null +++ b/extensions/intake/AGENTS.md @@ -0,0 +1,33 @@ +# 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 new file mode 100644 index 0000000000..899dae282a --- /dev/null +++ b/extensions/intake/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog + +## [0.1.2] - 2026-06-23 + +### Changed + +- Added release installation and runtime dependency documentation. +- Shortened manifest description and reduced tags for community catalog submission. + +## [0.1.1] - 2026-06-23 + +### Changed + +- Added community catalog metadata for category and effect. + +## [0.1.0] - 2026-06-23 + +### Added + +- Initial Spec Kit extension manifest. +- Intake commands for visual design, PRD, and test-case sources. +- Intake contracts and evidence packet templates for visual design, PRD, and test-case sources. +- Local readiness validators for PRD, test-case, visual design, and Figma metadata intake artifacts. + +### Changed + +- Repositioned the extension as the generic `intake` extension with domain commands for visual design, PRD, and test-case sources. +- Expanded the extension goal from Figma-only metadata intake to visual design requirement intake. +- Added support contracts for image, PDF, Markdown, and Figma sources across low, medium, and high-fidelity drafts. +- Added readiness concepts for source integrity, visual requirement traceability, fidelity rules, and delivery parity planning. +- Updated the validator to support generic visual sources while preserving Figma metadata parity validation. diff --git a/extensions/intake/LICENSE b/extensions/intake/LICENSE new file mode 100644 index 0000000000..14fac913cc --- /dev/null +++ b/extensions/intake/LICENSE @@ -0,0 +1,21 @@ +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 new file mode 100644 index 0000000000..1ffc81270b --- /dev/null +++ b/extensions/intake/README.md @@ -0,0 +1,136 @@ +# Spec Kit Intake Extension + +Extract, normalize, and validate SDD-ready intake artifacts from PRDs, visual designs, test cases, and other software sources before downstream Spec Kit workflows project them into requirements. + +The first goal of intake is not to generate requirements. It is to preserve as much input information as possible and turn it into structured material that SDD `specify` can consume accurately. + +The intake goal is high-information source restoration: extracted facts must be usable as provider-neutral engineering input, and every downstream projection should remain traceable to the original artifact. + +Intake artifacts are validated in two layers: JSON Schema checks enforce the required structure, field types, and enums; readiness validators then check source integrity, traceability, hashes, gaps, and cross-file parity. + +## 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 +- Existing test cases, Gherkin files, QA exports, and test management spreadsheets + +## Intake Scenario Coverage + +Intake commands are organized by vertical source domain. Each domain uses the same evidence pattern: preserve the original source, normalize source-backed facts, keep uncertainty explicit, and report readiness before downstream Spec Kit workflows project the evidence. + +| Domain | Vertical scenarios | Normalized artifact | Primary readiness focus | +| --- | --- | --- | --- | +| PRD | product briefs, Markdown PRDs, exported docs, PDFs, issue or epic links, mixed stakeholder notes | `prd-intake.yaml` | source identity, product intent traceability, scope boundaries, acceptance evidence, clarification gaps | +| Visual design | static images, wireframes, PDF design packs, Markdown design briefs, Figma files or selected nodes | `visual-requirements.yaml` | source integrity, fidelity rules, visual requirement traceability, parity planning, Figma metadata completeness when relevant | +| Test cases | automated tests, Gherkin files, manual QA cases, spreadsheets, test management exports, bug or issue repro steps | `test-case-intake.yaml` | scenario traceability, assertion extraction, fixture evidence, coverage gaps, flaky or skipped case reporting | + +Vertical instructions should never convert source evidence directly into downstream-owned requirement IDs, implementation tasks, or code component names. They produce provider-neutral intake facts that downstream workflows can consume with source refs intact. + +## Commands + +- `/speckit.intake.visual-design` captures or validates visual design evidence, source manifests, Figma metadata when available, inventories, and readiness for the active feature. +- `/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 +│ └── visual-evidence-packet.md +└── test-cases/ + ├── source-manifest.yaml + ├── source-files/ + ├── test-case-intake.yaml + └── evidence-packet.md +``` + +Figma metadata artifacts are required for Figma visual-design sources. Image, PDF, and Markdown visual-design sources use `design-source-manifest.yaml`, source-file checksums, extracted visual requirements, and visual parity evidence instead. PRD and test-case domains use their own source manifests and normalized intake files. + +Machine-readable JSON Schemas live under `templates/schemas/` and are used by the validators before readiness rules run. + +All intake commands provide capture instructions, evidence contracts, and readiness validation. Visual design validation additionally checks visual fidelity and Figma metadata parity. + +## 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.1.2.zip +``` + +Then run: + +```text +/speckit.intake.visual-design capture +/speckit.intake.visual-design validate +/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 +- parity evidence explains how implementation output will be compared with the original design artifact +- Figma sources also pass raw metadata completeness and node-inventory parity +- no blocker lint errors exist + +## 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 artifacts: + +```bash +python scripts/python/validate_visual_design_intake.py specs//intake/visual-design +``` + +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 new file mode 100644 index 0000000000..5246ec4e03 --- /dev/null +++ b/extensions/intake/commands/speckit.intake.prd.md @@ -0,0 +1,97 @@ +--- +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 new file mode 100644 index 0000000000..cbaa50bde3 --- /dev/null +++ b/extensions/intake/commands/speckit.intake.test-cases.md @@ -0,0 +1,97 @@ +--- +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 new file mode 100644 index 0000000000..fe785eb3b1 --- /dev/null +++ b/extensions/intake/commands/speckit.intake.visual-design.md @@ -0,0 +1,126 @@ +--- +description: Capture or validate visual design intake for the active Spec Kit feature. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Classify the input before proceeding: + +- `source`: image, PDF, Markdown design brief, Figma URL, file, page, frame, node, or exported design asset +- `intake_dir`: existing visual-design intake artifact directory +- `validation_request`: validate, check, gate, or readiness request +- `review_guidance`: target platform, required fidelity, capture scope, source precedence, or reviewer instructions + +## Goal + +Create, update, or validate provider-neutral visual design intake artifacts for the active Spec Kit feature. Intake preserves reachable design sources, raw provider evidence, stable source refs, checksums or retrieval metadata, and schema-required visual facts so downstream SDD workflows can project requirements and define their own visual verification criteria with traceability. + +Default artifact directory: + +```text +specs//intake/visual-design/ +``` + +Normative authority: + +- `templates/schemas/*.json` defines machine-readable structure, required fields, types, and enums. +- `scripts/python/validate_visual_design_intake.py` defines readiness evaluation and blocker emission. +- `templates/intake-visual-design-contract.md` defines semantic extraction policy, fidelity policy, and provider evidence policy. +- This command only performs input routing, context loading, capture orchestration, validation invocation, and reporting. + +## Operating Boundaries + +- Preserve original design sources and record checksums before extraction. +- Extract visual requirements as traceable engineering input, not as unsupported prose summaries or downstream-specific schema projections. +- Mark low, medium, or high fidelity explicitly and apply the matching extraction rules. +- Use stable provider-neutral evidence IDs and source refs. Do not invent downstream-owned item IDs, requirement IDs, schema fields, code component names, or product semantics. +- Do not mark intake ready unless source integrity, requirement traceability, fidelity proof, and intake parity planning pass. +- Preserve raw Figma metadata exactly in `figma-metadata.part-*.xml` for Figma sources. +- Do not modify application source, tests, package manifests, feature implementation files, or existing Spec Kit core templates. +- If required tooling is unavailable, create a blocked evidence packet that records the missing tool and stop before claiming readiness. + +## Context Loading + +1. Verify the current directory is a Spec Kit project by checking for `.specify/`, unless `$ARGUMENTS` points to a standalone artifact directory for extension development. +2. Identify the active feature: + - Prefer `SPECIFY_FEATURE` when set. + - Otherwise use the current Git branch name when it matches a directory under `specs/`. + - Otherwise inspect `specs/` and choose the most recent feature directory only if there is a single clear candidate. + - If the feature cannot be identified and no standalone artifact directory was provided, stop and ask the user to set `SPECIFY_FEATURE` or run from the feature branch. +3. Read `.specify/extensions/intake/intake-config.yml` when present. +4. Read `templates/intake-visual-design-contract.md` and the referenced JSON Schemas from this extension before creating or validating artifacts. +5. Read any existing intake artifacts and preserve valid evidence unless the user explicitly asks to recapture it. + +## Mode Routing + +- Capture mode: use when `$ARGUMENTS` names an image, PDF, Markdown design brief, Figma URL, frame, node, platform, fidelity level, or asks to capture, ingest, update, or recapture visual evidence. +- Validate mode: use when `$ARGUMENTS` includes `validate`, `check`, `gate`, `readiness`, or only names an existing visual-design intake directory. +- Capture then validate: use when both a source and validation intent are present, or after capture artifacts are updated. + +## Capture Procedure + +1. Resolve the source from `$ARGUMENTS` or existing artifact metadata: + - source type: `image`, `pdf`, `markdown`, or `figma` + - source path, URL, file key, page, frame, node, region, or Markdown section scope + - required fidelity: `low`, `medium`, or `high` + - design version or timestamp +2. Create `design-source-manifest.yaml` with contract-required source identity, integrity, coverage, capture method, and fidelity fields. +3. Preserve file-based originals under `source-files/`; for remote or Figma sources, record stable URLs and exported screenshots or assets, or record a structured gap/blocker when unavailable. +4. For Figma sources, preserve raw provider evidence before deriving normalized requirements: + - write raw metadata shards as `figma-metadata.part-NNN.xml` + - build `figma-metadata.index.yaml` + - build `figma-node-inventory.yaml` + - validate metadata and inventory parity before deriving visual requirements +5. Extract source-specific evidence: + - image: dimensions, regions, OCR status, visual hierarchy, assets, and region coverage + - pdf: original file hash, page count, rendered page refs, text extraction status, and page coverage + - markdown: heading structure, design notes, embedded or linked assets, and visual requirement mappings + - figma: complete descendant metadata, node inventory, variables/styles/components, screenshots, and assets +6. Classify source-domain scenario coverage using `templates/intake-visual-design-contract.md`; do not define additional scenario categories in this command. +7. Build `visual-requirements.yaml` according to `templates/schemas/visual-requirements.schema.json` and the semantic policies in `templates/intake-visual-design-contract.md`. +8. For unavailable required evidence, record a structured gap or blocker instead of omitting the field. +9. Create or update `visual-evidence-packet.md` from `templates/intake-visual-design-evidence-packet-template.md` with readiness front matter and human-readable evidence notes; keep structured records in `visual-requirements.yaml`. Preserve an existing `figma-evidence-packet.md` only as a legacy compatibility alias when already configured by the host project. +10. Add an intake parity plan that records source-side comparison targets, methods, thresholds, accepted exceptions, and blocking difference categories without defining implementation capture artifacts or downstream delivery approval. +11. Run validation before reporting readiness. + +## Validation Procedure + +1. Resolve the visual-design intake directory from `$ARGUMENTS` or the active feature. +2. Run: + +```bash +python .specify/extensions/intake/scripts/python/validate_visual_design_intake.py +``` + +3. Prefer `--json` when a machine-readable result is needed. Report the validator result exactly: + - `PASS` means the evidence passed JSON Schema structure checks and is ready for downstream projection as traceable visual engineering input. + - `BLOCKED` means downstream workflows must keep design-derived requirements blocked, unresolved, or marked `[NEEDS CLARIFICATION]` instead of promoting unsupported design facts. + +## Readiness Authority + +Use this precedence when sources disagree: + +1. JSON Schemas are canonical for structural validity. +2. `validate_visual_design_intake.py` is canonical for readiness status and blocker codes. +3. `templates/intake-visual-design-contract.md` is canonical for semantic extraction, fidelity, and provider evidence policy. + +Do not restate, reinterpret, or override blocker codes in this command. + +## Report + +Return: + +- mode executed: capture, validate, or capture_then_validate +- output or validated directory +- source type and source refs captured, or the recorded gap/blocker +- required fidelity, or the recorded gap/blocker +- source file count and processed count, or the recorded gap/blocker +- visual requirement count +- readiness result +- blocker lint errors +- next corrective action when blocked +- open questions that must remain `[NEEDS CLARIFICATION]` diff --git a/extensions/intake/config-template.yml b/extensions/intake/config-template.yml new file mode 100644 index 0000000000..362204fac0 --- /dev/null +++ b/extensions/intake/config-template.yml @@ -0,0 +1,75 @@ +# Intake Configuration + +artifacts: + base_dir: "specs/{feature}/intake" + visual_design_dir: "visual-design" + 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" + 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" + 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" + metadata_glob: "figma-metadata.part-*.xml" + metadata_index: "figma-metadata.index.yaml" + node_inventory: "figma-node-inventory.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 + 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-" + preserve_raw_metadata: true + allow_screenshot_only_intake: true + default_screenshot_level: "L0" diff --git a/extensions/intake/extension.yml b/extensions/intake/extension.yml new file mode 100644 index 0000000000..ef24cee21f --- /dev/null +++ b/extensions/intake/extension.yml @@ -0,0 +1,114 @@ +schema_version: "1.0" + +extension: + id: intake + name: "Intake" + version: "0.1.2" + description: "Normalize PRD, design, 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: "Capture or validate visual design evidence, source manifests, Figma metadata, inventories, and readiness for the active feature" + - 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 intake evidence before downstream SDD projection?" + description: "Checks visual design intake readiness before downstream workflows project source evidence" + +tags: + - "intake" + - "sdd" + - "requirements" + - "validation" + - "figma" + +defaults: + artifacts: + base_dir: "specs/{feature}/intake" + visual_design_dir: "visual-design" + 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" + 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" + 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" + metadata_glob: "figma-metadata.part-*.xml" + metadata_index: "figma-metadata.index.yaml" + node_inventory: "figma-node-inventory.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 + fail_on_blocker_lint_errors: true diff --git a/extensions/intake/requirements-dev.txt b/extensions/intake/requirements-dev.txt new file mode 100644 index 0000000000..75cdb5c50d --- /dev/null +++ b/extensions/intake/requirements-dev.txt @@ -0,0 +1,11 @@ +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/intake_validator_common.py b/extensions/intake/scripts/python/intake_validator_common.py new file mode 100644 index 0000000000..fa9448f37b --- /dev/null +++ b/extensions/intake/scripts/python/intake_validator_common.py @@ -0,0 +1,384 @@ +"""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 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"), + "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]] = [] + for source_file in source_files: + if not isinstance(source_file, dict): + blocker_codes.append(missing_file_code) + continue + + missing_source_file_fields = [ + field for field in required_source_file_fields if field not in source_file + ] + rel_path = str(source_file.get("path") or "").strip() + 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, + } + + 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 is_remote_ref(rel_path): + 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 + + ready_match = re.search( + r"^\s*[-*]?\s*ready[_ ]gate:\s*(PASS|BLOCKED)\s*$", + text, + re.IGNORECASE | re.MULTILINE, + ) + if ready_match: + ready_gate = ready_match.group(1).upper() + result["ready_gate"] = ready_gate + result["metadata"] = {"ready_gate": ready_gate, "blockers": []} + result["warnings"].append( + "evidence packet uses legacy Markdown ready_gate; add YAML front matter metadata" + ) + 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/validate_prd_intake.py b/extensions/intake/scripts/python/validate_prd_intake.py new file mode 100644 index 0000000000..3cc993aaf0 --- /dev/null +++ b/extensions/intake/scripts/python/validate_prd_intake.py @@ -0,0 +1,255 @@ +#!/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_test_cases_intake.py b/extensions/intake/scripts/python/validate_test_cases_intake.py new file mode 100644 index 0000000000..4cfaaf26ec --- /dev/null +++ b/extensions/intake/scripts/python/validate_test_cases_intake.py @@ -0,0 +1,271 @@ +#!/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 new file mode 100644 index 0000000000..29eda0aa1b --- /dev/null +++ b/extensions/intake/scripts/python/validate_visual_design_intake.py @@ -0,0 +1,660 @@ +#!/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, 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_SCHEMA_INVALID": "VISUAL_SCHEMA_INVALID", + "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", +} + + +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"), + } + + 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]] = [] + 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"] + missing_source_file_fields = [ + field for field in required_source_file_fields if field not in source_file + ] + rel_path = str(source_file.get("path") or "").strip() + 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, + } + + 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 is_remote_ref(rel_path): + 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", "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 + + 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 + + evidence_type = str(item.get("evidence_type") or "").strip().lower() + 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 + + 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"]["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"]) + + +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"]) + + +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"]: + blocker_codes.append(BLOCKERS["VISUAL_READY_WITHOUT_EVIDENCE"]) + 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 new file mode 100644 index 0000000000..e1ce4b567b --- /dev/null +++ b/extensions/intake/templates/intake-prd-contract.md @@ -0,0 +1,124 @@ +# 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: + - sha256: + - 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`, and `integrity_gap_reason` 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 new file mode 100644 index 0000000000..ce0d0ad378 --- /dev/null +++ b/extensions/intake/templates/intake-prd-evidence-packet-template.md @@ -0,0 +1,108 @@ +--- +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-test-cases-contract.md b/extensions/intake/templates/intake-test-cases-contract.md new file mode 100644 index 0000000000..64a14ef3bc --- /dev/null +++ b/extensions/intake/templates/intake-test-cases-contract.md @@ -0,0 +1,137 @@ +# 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: + - sha256: + - 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 and mark unavailable checksum fields as explicit gaps 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 new file mode 100644 index 0000000000..7bfd7d266c --- /dev/null +++ b/extensions/intake/templates/intake-test-cases-evidence-packet-template.md @@ -0,0 +1,108 @@ +--- +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 new file mode 100644 index 0000000000..e0c4e2062b --- /dev/null +++ b/extensions/intake/templates/intake-visual-design-contract.md @@ -0,0 +1,215 @@ +# Visual Design Intake Contract + +Required visual design intake artifacts and readiness gates. Runtime agents or external intake tools extract traceable, provider-neutral visual facts before downstream SDD workflows project evidence into requirements or visual verification criteria. + +Intake does not generate requirements. It preserves all reachable design sources, raw provider evidence, stable source refs, checksums or retrieval metadata, and schema-required visual facts that SDD `specify` can consume accurately. + +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 not a downstream workflow schema. It must not require downstream-owned artifact names, item IDs, code component names, or product semantics that are not directly evidenced by the source. Downstream workflows own any projection from these neutral facts into their local schemas. + +## 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: + - path: + - mime_type: + - byte_size: + - sha256: + - role: original|rendered_page|screenshot|asset|markdown +- source_integrity_complete: +- captured_at: +- capture_method: +- page_or_frame_count: +- processed_count: +- extraction_scope: + +Source-specific requirements: + +- Image sources must record image dimensions, crop/region coverage, OCR status when text is present, and any exported asset refs. +- PDF sources must record original PDF hash, page count, processed page count, rendered page references, and text extraction status. +- Markdown sources must record original Markdown hash, embedded or linked asset refs, heading structure, and design-note parsing status. +- Figma sources must additionally satisfy the Figma provider contract below. + +## Fidelity Profile + +The required fidelity level controls extraction depth and validation strength. + +- Low fidelity: capture screen or page intent, rough layout hierarchy, major content groups, interaction hints, missing information, and source refs. +- Medium fidelity: include low-fidelity requirements plus key spacing, sizing, typography categories, color roles, assets, states, and responsive clues. +- High fidelity: include medium-fidelity requirements plus exact or bounded dimensions, spacing, typography, tokens, asset export contracts, component variants, page/frame coverage, and comparison thresholds. + +The intake must record `fidelity_rules_applied: true` and explain any accepted gap. + +## Visual Requirements + +`visual-requirements.yaml` must normalize extracted visual facts into engineering input. The evidence packet may summarize the same records for human review, but readiness validation uses the standalone machine-readable file. + +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|missing|out_of_scope +- confidence: low|medium|high +- confidence_rationale: +- engineering_action: +- acceptance_check: +- fidelity_level: + +Use `id` as a stable visual evidence ID within this intake package. IDs MUST remain unchanged across recaptures unless the source ref or normalized fact changes. Changed IDs MUST be recorded as a traceability gap. IDs must not be named or formatted as downstream-owned requirement IDs or item IDs. + +When evidenced by the source, include provider-neutral optional fields: + +- state_or_variant_refs: +- asset_refs: +- constraint_refs: +- proof_refs: +- blockers: + +Readiness requires: + +- visual_requirements_complete: true +- visual_requirements_count greater than 0 and equal to the number of records in `requirements` +- source_refs_complete: true +- every requirement has the required fields listed above +- missing_or_uncertain items explicitly recorded instead of silently inferred +- no unsupported summary substitution for original source evidence +- no blocker lint errors + +## Intake Parity Plan + +The extracted requirements must make later downstream comparison possible against the original design source. The intake package records source-side comparison constraints only; downstream workflows own implementation captures, delivery reports, and final approval decisions. + +Required parity fields in `visual-requirements.yaml`: + +- visual_parity_plan_complete: +- parity_plan: + - comparison_targets: + - original_refs: + - comparison_method: visual_diff|measurement|token_match|asset_match|manual_review + - thresholds: + - accepted_exceptions: + - blocking_difference_categories: + +The evidence packet front matter records the `ready_gate` enum only for intake readiness. + +## Figma Provider Contract + +For `source_type: figma`, `figma-metadata.part-*.xml` must preserve raw `get_metadata` output. + +- Do not summarize, rewrite, compress into prose, or replace real nodes with natural language. +- Cover the complete descendant subtree for every selected frame or node. +- Treat truncation as failed evidence. + +Each part must be listed with: + +- path: +- byte_size: +- sha256: +- root_node_ids: +- node_count: +- truncated: + +`figma-metadata.index.yaml` must prove source identity, shard integrity, and selected subtree completeness. + +Required source fields: + +- file_url: +- file_key: +- page_id: +- selected_node_ids: +- captured_at: +- mcp_tool: get_metadata +- design_version_or_timestamp: + +Required completeness fields: + +- selected_subtree_complete: +- raw_metadata_complete: +- expected_root_node_ids: +- captured_root_node_ids: +- missing_root_node_ids: +- gap_count: +- gaps: + +`figma-node-inventory.yaml` must reconcile inventory with raw metadata. + +Required parity fields: + +- raw_node_count: +- inventory_node_count: +- excluded_node_count: +- missing_node_count: +- duplicate_node_count: +- truncated_raw_evidence: +- node_inventory_coverage: 100% +- parity_passed: true + +Required parity rules: + +- inventory_node_count + excluded_node_count + missing_node_count == raw_node_count +- duplicate_node_count == 0 +- missing_node_count == 0 +- truncated_raw_evidence == false +- parity_passed equals count balance, no duplicates, no missing nodes, and no truncation + +## Evidence Readiness Gate + +Visual design intake is ready only when all conditions pass: + +- source_integrity_complete: true +- source_type is image, pdf, markdown, or figma +- required_fidelity is low, medium, or high +- visual_requirements_complete: true +- source_refs_complete: true +- fidelity_rules_applied: true +- visual_parity_plan_complete: true and required parity plan fields are present +- Figma sources also pass raw_metadata_complete, selected_subtree_complete, node_inventory_coverage, and parity_passed +- No blocker lint errors + +## Blocker Lint Errors + +- VISUAL_SOURCE_MANIFEST_MISSING +- VISUAL_SOURCE_TYPE_UNSUPPORTED +- VISUAL_FIDELITY_LEVEL_UNSUPPORTED +- VISUAL_SOURCE_FILE_MISSING +- VISUAL_SOURCE_HASH_MISMATCH +- VISUAL_SOURCE_INTEGRITY_INCOMPLETE +- VISUAL_REQUIREMENTS_MISSING +- VISUAL_REQUIREMENTS_UNTRACEABLE +- VISUAL_FIDELITY_RULES_MISSING +- VISUAL_PARITY_PLAN_MISSING +- VISUAL_READY_WITHOUT_EVIDENCE +- VISUAL_EVIDENCE_PACKET_MISSING +- VISUAL_BLOCKER_LINT_ERRORS +- VISUAL_SCHEMA_INVALID +- 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 + +## Gap Rules + +Record a gap instead of passing silently when source evidence is missing, summarized, truncated, incomplete, untraceable, missing fidelity proof, missing visual requirements, missing comparison proof, missing Figma parity proof, missing nodes, duplicate nodes, or marked ready without completeness proof. + +## Evidence Packet Metadata + +`visual-evidence-packet.md` must start with YAML front matter: + +- ready_gate: `PASS|BLOCKED` +- blockers: +- source_ref_count: +- extracted_item_count: +- generated_at: + +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-visual-design-evidence-packet-template.md b/extensions/intake/templates/intake-visual-design-evidence-packet-template.md new file mode 100644 index 0000000000..dcba5e4734 --- /dev/null +++ b/extensions/intake/templates/intake-visual-design-evidence-packet-template.md @@ -0,0 +1,165 @@ +--- +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: + +## Derived Assumptions + +- Inferred navigation: +- Inferred grouping: +- Inferred content priority: +- Inferred responsive behavior: +- Required marker: `evidence_type: inferred` +- Required clarification or acceptance gap: +- Confidence notes: + +## 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 new file mode 100644 index 0000000000..2c7f63aa27 --- /dev/null +++ b/extensions/intake/templates/schemas/figma-metadata-index.schema.json @@ -0,0 +1,57 @@ +{ + "$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 new file mode 100644 index 0000000000..3e3519f696 --- /dev/null +++ b/extensions/intake/templates/schemas/figma-node-inventory.schema.json @@ -0,0 +1,27 @@ +{ + "$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/prd-intake.schema.json b/extensions/intake/templates/schemas/prd-intake.schema.json new file mode 100644 index 0000000000..6b06e80f88 --- /dev/null +++ b/extensions/intake/templates/schemas/prd-intake.schema.json @@ -0,0 +1,77 @@ +{ + "$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 }, + "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" } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true +} diff --git a/extensions/intake/templates/schemas/prd-source-manifest.schema.json b/extensions/intake/templates/schemas/prd-source-manifest.schema.json new file mode 100644 index 0000000000..f92b104101 --- /dev/null +++ b/extensions/intake/templates/schemas/prd-source-manifest.schema.json @@ -0,0 +1,109 @@ +{ + "$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_type": { "const": "mixed" } } }, + "then": { "required": ["source_precedence"] } + } + ], + "$defs": { + "source_file": { + "type": "object", + "required": ["path", "mime_type", "byte_size", "sha256", "role"], + "properties": { + "path": { "type": "string", "minLength": 1 }, + "mime_type": { "type": "string", "minLength": 1 }, + "byte_size": { "type": "integer", "minimum": 0 }, + "sha256": { "type": "string", "minLength": 1 }, + "role": { "enum": ["original", "export", "attachment", "snapshot"] } + }, + "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 new file mode 100644 index 0000000000..829f78953c --- /dev/null +++ b/extensions/intake/templates/schemas/test-case-intake.schema.json @@ -0,0 +1,89 @@ +{ + "$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 }, + "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" } + }, + "additionalProperties": true + } + } + }, + "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 new file mode 100644 index 0000000000..b4ca696b65 --- /dev/null +++ b/extensions/intake/templates/schemas/test-case-source-manifest.schema.json @@ -0,0 +1,127 @@ +{ + "$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_type": { "const": "mixed" } } }, + "then": { "required": ["source_precedence"] } + } + ], + "$defs": { + "source_file": { + "type": "object", + "required": ["path", "mime_type", "byte_size", "sha256", "role"], + "properties": { + "path": { "type": "string", "minLength": 1 }, + "mime_type": { "type": "string", "minLength": 1 }, + "byte_size": { "type": "integer", "minimum": 0 }, + "sha256": { "type": "string", "minLength": 1 }, + "role": { "enum": ["original", "export", "attachment", "snapshot"] } + }, + "additionalProperties": true + } + }, + "additionalProperties": true +} diff --git a/extensions/intake/templates/schemas/visual-requirements.schema.json b/extensions/intake/templates/schemas/visual-requirements.schema.json new file mode 100644 index 0000000000..657d3760de --- /dev/null +++ b/extensions/intake/templates/schemas/visual-requirements.schema.json @@ -0,0 +1,105 @@ +{ + "$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", + "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", + "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 }, + "evidence_type": { "enum": ["observed", "inferred", "missing", "out_of_scope"] }, + "confidence": { "enum": ["low", "medium", "high"] }, + "confidence_rationale": { "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" } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true +} diff --git a/extensions/intake/templates/schemas/visual-source-manifest.schema.json b/extensions/intake/templates/schemas/visual-source-manifest.schema.json new file mode 100644 index 0000000000..f058b2ff57 --- /dev/null +++ b/extensions/intake/templates/schemas/visual-source-manifest.schema.json @@ -0,0 +1,134 @@ +{ + "$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" } + }, + "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 + } + } + } + } + ], + "$defs": { + "source_file": { + "type": "object", + "required": ["path", "mime_type", "byte_size", "sha256", "role"], + "properties": { + "path": { "type": "string", "minLength": 1 }, + "mime_type": { "type": "string", "minLength": 1 }, + "byte_size": { "type": "integer", "minimum": 0 }, + "sha256": { "type": "string", "minLength": 1 }, + "role": { + "enum": ["original", "rendered_page", "screenshot", "asset", "markdown"] + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true +} diff --git a/extensions/intake/tests/test_extension_contract.py b/extensions/intake/tests/test_extension_contract.py new file mode 100644 index 0000000000..ef8421bf16 --- /dev/null +++ b/extensions/intake/tests/test_extension_contract.py @@ -0,0 +1,992 @@ +import os +import json +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + + +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" + + +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_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 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_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_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_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", + "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_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_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", + ) + 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_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) diff --git a/extensions/preview/.gitignore b/extensions/preview/.gitignore index 1ae8f6d522..4f5e1d4482 100644 --- a/extensions/preview/.gitignore +++ b/extensions/preview/.gitignore @@ -1,7 +1,23 @@ .venv/ +venv/ __pycache__/ -*.pyc +*.py[cod] .pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ dist/ +build/ +*.egg-info/ *.zip - +*.log +.env +.env.* +!.env.example +.tmp/ +tmp/ +temp/ +.DS_Store +Thumbs.db +.worktrees/ diff --git a/extensions/repository-governance/.gitignore b/extensions/repository-governance/.gitignore index 16ec6bd7d9..717ad111dc 100644 --- a/extensions/repository-governance/.gitignore +++ b/extensions/repository-governance/.gitignore @@ -1,8 +1,23 @@ __pycache__/ -*.pyc +*.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/template/.gitignore b/extensions/template/.gitignore index 1f7b132a76..4653c30fa0 100644 --- a/extensions/template/.gitignore +++ b/extensions/template/.gitignore @@ -9,9 +9,12 @@ __pycache__/ .Python env/ venv/ +.venv/ # Testing .pytest_cache/ +.mypy_cache/ +.ruff_cache/ .coverage htmlcov/ @@ -36,4 +39,12 @@ build/ # Temporary files *.tmp +.tmp/ +tmp/ +temp/ .cache/ +.worktrees/ +*.zip +.env +.env.* +!.env.example diff --git a/presets/workflow-preset/.gitignore b/presets/workflow-preset/.gitignore index 42262154d0..756e140ac4 100644 --- a/presets/workflow-preset/.gitignore +++ b/presets/workflow-preset/.gitignore @@ -1,8 +1,24 @@ __pycache__/ -*.pyc +*.py[cod] .pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ .venv/ +venv/ dist/ +build/ +*.egg-info/ *.zip +*.log +.env +.env.* +!.env.example +.tmp/ +tmp/ +temp/ .DS_Store +Thumbs.db +.worktrees/ docs/superpowers/ diff --git a/pyproject.toml b/pyproject.toml index fe723fab49..4ca79144b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,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/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" diff --git a/scripts/community/cleanup-worktrees.ps1 b/scripts/community/cleanup-worktrees.ps1 new file mode 100644 index 0000000000..5f88d696fb --- /dev/null +++ b/scripts/community/cleanup-worktrees.ps1 @@ -0,0 +1,55 @@ +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [string]$RepoRoot = '', + [string]$NamePattern = '^spec-kit-pr-', + [switch]$Force +) + +$ErrorActionPreference = 'Stop' + +if ($RepoRoot -eq '') { + $RepoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..')).Path +} + +$repoRootPath = (Resolve-Path -LiteralPath $RepoRoot).Path +$parentPath = (Resolve-Path -LiteralPath (Join-Path $repoRootPath '..')).Path + +$worktreeOutput = git -C $repoRootPath worktree list --porcelain +$paths = @() +foreach ($line in $worktreeOutput) { + if ($line -match '^worktree\s+(.+)$') { + $paths += $Matches[1] + } +} + +foreach ($path in $paths) { + $fullPath = [System.IO.Path]::GetFullPath($path) + $name = Split-Path -Leaf $fullPath + + if ($fullPath -ieq $repoRootPath) { + continue + } + if ($name -notmatch $NamePattern) { + continue + } + if (-not $fullPath.StartsWith($parentPath, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to remove worktree outside repository parent: $fullPath" + } + + $dirtyLines = @(git -C $fullPath status --porcelain) + if ($dirtyLines.Count -gt 0 -and -not $Force) { + Write-Warning "Skipping dirty worktree: $fullPath" + continue + } + + if ($PSCmdlet.ShouldProcess($fullPath, 'git worktree remove')) { + if ($Force) { + git -C $repoRootPath worktree remove --force $fullPath + } else { + git -C $repoRootPath worktree remove $fullPath + } + } +} + +git -C $repoRootPath worktree prune +git -C $repoRootPath worktree list diff --git a/scripts/community/new-worktree.ps1 b/scripts/community/new-worktree.ps1 new file mode 100644 index 0000000000..203aaa1418 --- /dev/null +++ b/scripts/community/new-worktree.ps1 @@ -0,0 +1,52 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$Branch, + + [string]$WorktreeName = '', + + [string]$RepoRoot = '', + + [string]$Base = 'main' +) + +$ErrorActionPreference = 'Stop' + +if ($RepoRoot -eq '') { + $RepoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..')).Path +} + +if ($Branch -notmatch '^community/(?:\d+-)?[a-z0-9][a-z0-9._-]*$') { + throw "Community integration branches must match community/(-)?: $Branch" +} + +$repoRootPath = (Resolve-Path -LiteralPath $RepoRoot).Path +$parentPath = (Resolve-Path -LiteralPath (Join-Path $repoRootPath '..')).Path + +if ($WorktreeName -eq '') { + $WorktreeName = 'spec-kit-pr-' + (($Branch -replace '^community/', '') -replace '[^a-zA-Z0-9._-]', '-') +} + +$targetPath = Join-Path $parentPath $WorktreeName +if (Test-Path -LiteralPath $targetPath) { + throw "Worktree target already exists: $targetPath" +} + +$resolvedParent = [System.IO.Path]::GetFullPath($parentPath) +$resolvedTarget = [System.IO.Path]::GetFullPath($targetPath) +if (-not $resolvedTarget.StartsWith($resolvedParent, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to create a worktree outside the repository parent: $resolvedTarget" +} + +git -C $repoRootPath fetch --all --prune +$existingBranch = git -C $repoRootPath branch --list $Branch + +if ($existingBranch) { + git -C $repoRootPath worktree add $targetPath $Branch +} else { + git -C $repoRootPath worktree add -b $Branch $targetPath $Base +} + +Write-Host "Created community integration worktree:" +Write-Host " Branch: $Branch" +Write-Host " Path: $targetPath" diff --git a/scripts/community/validate-integration.ps1 b/scripts/community/validate-integration.ps1 new file mode 100644 index 0000000000..f6f4e7cd82 --- /dev/null +++ b/scripts/community/validate-integration.ps1 @@ -0,0 +1,30 @@ +[CmdletBinding()] +param( + [string]$RepoRoot = '', + [string]$Branch = '', + [string]$PrBodyFile = '', + [switch]$ShowWarnings +) + +$ErrorActionPreference = 'Stop' + +if ($RepoRoot -eq '') { + $RepoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..')).Path +} + +$script = Join-Path $PSScriptRoot 'validate_integration.py' +$argsList = @($script, '--repo-root', $RepoRoot) + +if ($Branch -ne '') { + $argsList += @('--branch', $Branch) +} + +if ($PrBodyFile -ne '') { + $argsList += @('--pr-body-file', $PrBodyFile) +} + +if ($ShowWarnings) { + $argsList += '--show-warnings' +} + +python @argsList diff --git a/scripts/community/validate_integration.py b/scripts/community/validate_integration.py new file mode 100644 index 0000000000..0d70d270ef --- /dev/null +++ b/scripts/community/validate_integration.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +"""Validate local community integration workflow invariants. + +This script is intentionally dependency-free so it can run from PowerShell, +GitHub Actions, or a fresh checkout before project dependencies are installed. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + + +ID_RE = re.compile(r"^[a-z][a-z0-9-]*$") +SEMVER_RE = re.compile(r"^v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$") +ISO_DAY_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T00:00:00Z$") +SHA_RE = re.compile(r"\b[0-9a-fA-F]{40}\b") +URL_RE = re.compile(r"^https://") +COMMUNITY_BRANCH_RE = re.compile(r"^community/(?:\d+-)?[a-z0-9][a-z0-9._-]*$") + + +class Validation: + def __init__(self) -> None: + self.errors: list[str] = [] + self.warnings: list[str] = [] + + def error(self, message: str) -> None: + self.errors.append(message) + + def warn(self, message: str) -> None: + self.warnings.append(message) + + def require(self, condition: bool, message: str) -> None: + if not condition: + self.error(message) + + +def load_json(path: Path, validation: Validation) -> dict: + try: + with path.open(encoding="utf-8") as handle: + data = json.load(handle) + except FileNotFoundError: + validation.error(f"{path}: file not found") + return {} + except json.JSONDecodeError as exc: + validation.error(f"{path}: invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}") + return {} + + if not isinstance(data, dict): + validation.error(f"{path}: root value must be a JSON object") + return {} + return data + + +def validate_catalog( + path: Path, + collection_key: str, + validation: Validation, +) -> None: + data = load_json(path, validation) + if not data: + return + + validation.require(data.get("schema_version") == "1.0", f"{path}: schema_version must be 1.0") + updated_at = data.get("updated_at") + validation.require( + isinstance(updated_at, str) and ISO_DAY_RE.match(updated_at) is not None, + f"{path}: updated_at must use YYYY-MM-DDT00:00:00Z", + ) + + entries = data.get(collection_key) + if not isinstance(entries, dict): + validation.error(f"{path}: missing object field {collection_key!r}") + return + + ids = list(entries) + validation.require(ids == sorted(ids), f"{path}: {collection_key} keys must be sorted alphabetically") + + for entry_id, entry in entries.items(): + prefix = f"{path}: {collection_key}.{entry_id}" + if not isinstance(entry, dict): + validation.error(f"{prefix}: entry must be an object") + continue + + validation.require(ID_RE.match(entry_id) is not None, f"{prefix}: key must match {ID_RE.pattern}") + if "id" in entry: + validation.require(entry["id"] == entry_id, f"{prefix}: id field must match catalog key") + + for field in ("name", "version", "description", "author", "repository", "license"): + validation.require( + isinstance(entry.get(field), str) and entry[field].strip() != "", + f"{prefix}: missing non-empty string field {field!r}", + ) + + version = entry.get("version") + validation.require( + isinstance(version, str) and SEMVER_RE.match(version) is not None, + f"{prefix}: version must be semver, with optional v prefix", + ) + + repository = entry.get("repository") + validation.require( + isinstance(repository, str) and URL_RE.match(repository) is not None, + f"{prefix}: repository must be an https URL", + ) + + download_url = entry.get("download_url") + if not (isinstance(download_url, str) and URL_RE.match(download_url)): + validation.warn(f"{prefix}: download_url should be a non-empty https URL") + + requires = entry.get("requires") + validation.require(isinstance(requires, dict), f"{prefix}: requires must be an object") + if isinstance(requires, dict): + validation.require( + isinstance(requires.get("speckit_version"), str) and requires["speckit_version"].strip() != "", + f"{prefix}: requires.speckit_version is required", + ) + + validation.require(isinstance(entry.get("provides"), dict), f"{prefix}: provides must be an object") + validation.require(isinstance(entry.get("tags"), list) and len(entry["tags"]) >= 1, f"{prefix}: tags must be a non-empty array") + + for date_field in ("created_at", "updated_at"): + value = entry.get(date_field) + if not (isinstance(value, str) and ISO_DAY_RE.match(value)): + validation.warn(f"{prefix}: {date_field} should use YYYY-MM-DDT00:00:00Z") + + +def field_value(body: str, label: str) -> str | None: + pattern = re.compile(rf"(?im)^\s*[-*]?\s*{re.escape(label)}\s*:\s*(.+?)\s*$") + match = pattern.search(body) + if not match: + return None + value = match.group(1).strip() + if value.startswith("