diff --git a/README.md b/README.md index e5e3c58bba..b621b32472 100644 --- a/README.md +++ b/README.md @@ -380,7 +380,7 @@ specify extension search specify extension add ``` -For example, extensions could add Jira integration, post-implementation code review, V-Model test traceability, or project health diagnostics. +For example, extensions can add the official 4+1 architecture workflow (`specify extension add arch`, then `/speckit.arch.generate`), Jira integration, post-implementation code review, V-Model test traceability, or project health diagnostics. See the [Extensions reference](https://github.github.io/spec-kit/reference/extensions.html) for the full command guide. Browse the [community extensions](#-community-extensions) above for what's available. diff --git a/extensions/arch/README.md b/extensions/arch/README.md new file mode 100644 index 0000000000..afccf4e0a1 --- /dev/null +++ b/extensions/arch/README.md @@ -0,0 +1,19 @@ +# Architecture Workflow Extension + +Generates or updates the project-level 4+1 architecture SSOT artifacts under `.specify/memory/`. + +## Install + +```bash +specify extension add arch +``` + +## Use + +Run the registered command in your AI coding agent: + +```text +/speckit.arch.generate +``` + +The extension intentionally does not provide the legacy `/speckit.arch` command. diff --git a/extensions/arch/commands/speckit.arch.generate.md b/extensions/arch/commands/speckit.arch.generate.md new file mode 100644 index 0000000000..5c49b84696 --- /dev/null +++ b/extensions/arch/commands/speckit.arch.generate.md @@ -0,0 +1,155 @@ +--- +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 for later iterations. They serve two purposes: produce high-quality 4+1 architecture reasoning in this command, and constrain later `plan` reasoning to stay inside the architecture SSOT. + +## Operating Boundaries + +- Write only the six architecture artifacts listed above. +- Do not require `.specify/memory/uc.md`. If it 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 and later `plan` reasoning 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`. + +2. **Load context**: + - Read all six architecture artifacts created by setup. + - 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/extension.yml b/extensions/arch/extension.yml new file mode 100644 index 0000000000..6690c55c44 --- /dev/null +++ b/extensions/arch/extension.yml @@ -0,0 +1,25 @@ +schema_version: "1.0" + +extension: + id: arch + name: "Architecture Workflow" + version: "1.0.0" + description: "Generate project-level 4+1 architecture view artifacts and synthesis" + author: spec-kit-core + repository: https://github.com/github/spec-kit + license: MIT + +requires: + speckit_version: ">=0.8.10.dev0" + +provides: + commands: + - name: speckit.arch.generate + file: commands/speckit.arch.generate.md + description: "Execute the 4+1 architecture workflow and generate architecture view artifacts" + +tags: + - "architecture" + - "4+1" + - "workflow" + - "core" diff --git a/extensions/arch/scripts/bash/setup-arch.sh b/extensions/arch/scripts/bash/setup-arch.sh new file mode 100644 index 0000000000..b591789651 --- /dev/null +++ b/extensions/arch/scripts/bash/setup-arch.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash + +set -e + +# Parse command line arguments +JSON_MODE=false + +for arg in "$@"; do + case "$arg" in + --json) + JSON_MODE=true + ;; + --help|-h) + echo "Usage: $0 [--json]" + echo " --json Output results in JSON format" + echo " --help Show this help message" + exit 0 + ;; + *) + ;; + esac +done + +find_specify_root() { + local dir="${1:-$(pwd)}" + dir="$(cd -- "$dir" 2>/dev/null && pwd)" || return 1 + local prev_dir="" + while true; do + if [ -d "$dir/.specify" ]; then + echo "$dir" + return 0 + fi + if [ "$dir" = "/" ] || [ "$dir" = "$prev_dir" ]; then + break + fi + prev_dir="$dir" + dir="$(dirname "$dir")" + done + return 1 +} + +get_repo_root() { + local specify_root + if specify_root=$(find_specify_root); then + echo "$specify_root" + return + fi + + if git rev-parse --show-toplevel >/dev/null 2>&1; then + git rev-parse --show-toplevel + return + fi + + local script_dir + script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + (cd "$script_dir/../../../../.." && pwd) +} + +has_jq() { + command -v jq >/dev/null 2>&1 +} + +json_escape() { + local s="$1" + s="${s//\\/\\\\}" + s="${s//\"/\\\"}" + s="${s//$'\n'/\\n}" + s="${s//$'\t'/\\t}" + s="${s//$'\r'/\\r}" + s="${s//$'\b'/\\b}" + s="${s//$'\f'/\\f}" + local LC_ALL=C + local i char code + for (( i=0; i<${#s}; i++ )); do + char="${s:$i:1}" + printf -v code '%d' "'$char" 2>/dev/null || code=256 + if (( code >= 1 && code <= 31 )); then + printf '\\u%04x' "$code" + else + printf '%s' "$char" + fi + done +} + +resolve_architecture_template() { + local template_name="$1" + local repo_root="$2" + local ext_templates="$repo_root/.specify/extensions/arch/templates" + local override="$repo_root/.specify/templates/overrides/${template_name}.md" + local candidate="$ext_templates/${template_name}.md" + + [ -f "$override" ] && echo "$override" && return 0 + [ -f "$candidate" ] && echo "$candidate" && return 0 + return 1 +} + +REPO_ROOT=$(get_repo_root) +ARCH_DIR="$REPO_ROOT/.specify/memory" +ARCH_FILE="$ARCH_DIR/architecture.md" +SCENARIO_VIEW="$ARCH_DIR/architecture-scenario-view.md" +LOGICAL_VIEW="$ARCH_DIR/architecture-logical-view.md" +PROCESS_VIEW="$ARCH_DIR/architecture-process-view.md" +DEVELOPMENT_VIEW="$ARCH_DIR/architecture-development-view.md" +PHYSICAL_VIEW="$ARCH_DIR/architecture-physical-view.md" + +mkdir -p "$ARCH_DIR" + +copy_template_if_missing() { + local template_name="$1" + local destination="$2" + + if [[ -f "$destination" ]]; then + return 0 + fi + + local template + template=$(resolve_architecture_template "$template_name" "$REPO_ROOT") || true + if [[ -n "$template" ]] && [[ -f "$template" ]]; then + cp "$template" "$destination" + echo "Copied $template_name template to $destination" + else + echo "Warning: $template_name template not found" + touch "$destination" + fi +} + +copy_template_if_missing "architecture-template" "$ARCH_FILE" +copy_template_if_missing "architecture-scenario-template" "$SCENARIO_VIEW" +copy_template_if_missing "architecture-logical-template" "$LOGICAL_VIEW" +copy_template_if_missing "architecture-process-template" "$PROCESS_VIEW" +copy_template_if_missing "architecture-development-template" "$DEVELOPMENT_VIEW" +copy_template_if_missing "architecture-physical-template" "$PHYSICAL_VIEW" + +if $JSON_MODE; then + if has_jq; then + jq -cn \ + --arg arch_file "$ARCH_FILE" \ + --arg arch_dir "$ARCH_DIR" \ + --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,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","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 "$SCENARIO_VIEW")" \ + "$(json_escape "$LOGICAL_VIEW")" \ + "$(json_escape "$PROCESS_VIEW")" \ + "$(json_escape "$DEVELOPMENT_VIEW")" \ + "$(json_escape "$PHYSICAL_VIEW")" + fi +else + echo "ARCH_FILE: $ARCH_FILE" + echo "ARCH_DIR: $ARCH_DIR" + echo "SCENARIO_VIEW: $SCENARIO_VIEW" + echo "LOGICAL_VIEW: $LOGICAL_VIEW" + echo "PROCESS_VIEW: $PROCESS_VIEW" + echo "DEVELOPMENT_VIEW: $DEVELOPMENT_VIEW" + echo "PHYSICAL_VIEW: $PHYSICAL_VIEW" +fi diff --git a/extensions/arch/scripts/powershell/setup-arch.ps1 b/extensions/arch/scripts/powershell/setup-arch.ps1 new file mode 100644 index 0000000000..cd6615a331 --- /dev/null +++ b/extensions/arch/scripts/powershell/setup-arch.ps1 @@ -0,0 +1,139 @@ +#!/usr/bin/env pwsh +# Setup project-level 4+1 architecture artifacts + +[CmdletBinding()] +param( + [switch]$Json, + [switch]$Help +) + +$ErrorActionPreference = 'Stop' + +if ($Help) { + Write-Output "Usage: ./setup-arch.ps1 [-Json] [-Help]" + Write-Output " -Json Output results in JSON format" + Write-Output " -Help Show this help message" + exit 0 +} + +function Find-SpecifyRoot { + param([string]$StartDir = (Get-Location).Path) + + $resolved = Resolve-Path -LiteralPath $StartDir -ErrorAction SilentlyContinue + $current = if ($resolved) { $resolved.Path } else { $null } + if (-not $current) { return $null } + + while ($true) { + if (Test-Path -LiteralPath (Join-Path $current ".specify") -PathType Container) { + return $current + } + $parent = Split-Path $current -Parent + if ([string]::IsNullOrEmpty($parent) -or $parent -eq $current) { + return $null + } + $current = $parent + } +} + +function Get-RepoRoot { + $specifyRoot = Find-SpecifyRoot + if ($specifyRoot) { + return $specifyRoot + } + + try { + $result = git rev-parse --show-toplevel 2>$null + if ($LASTEXITCODE -eq 0) { + return $result + } + } catch { + } + + return (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "../../../../..")).Path +} + +function Resolve-ArchitectureTemplate { + param( + [Parameter(Mandatory = $true)][string]$TemplateName, + [Parameter(Mandatory = $true)][string]$RepoRoot + ) + + $override = Join-Path $RepoRoot ".specify/templates/overrides/$TemplateName.md" + if (Test-Path -LiteralPath $override -PathType Leaf) { + return $override + } + + $candidate = Join-Path $RepoRoot ".specify/extensions/arch/templates/$TemplateName.md" + if (Test-Path -LiteralPath $candidate -PathType Leaf) { + return $candidate + } + + return $null +} + +function Convert-ToPlainPath { + param([Parameter(Mandatory = $true)][string]$Path) + + if ($Path -like 'Microsoft.PowerShell.Core\FileSystem::*') { + return $Path.Substring('Microsoft.PowerShell.Core\FileSystem::'.Length) + } + return $Path +} + +$repoRoot = Convert-ToPlainPath (Get-RepoRoot) +$archDir = Join-Path $repoRoot ".specify/memory" +$archFile = Join-Path $archDir "architecture.md" +$scenarioView = Join-Path $archDir "architecture-scenario-view.md" +$logicalView = Join-Path $archDir "architecture-logical-view.md" +$processView = Join-Path $archDir "architecture-process-view.md" +$developmentView = Join-Path $archDir "architecture-development-view.md" +$physicalView = Join-Path $archDir "architecture-physical-view.md" + +New-Item -ItemType Directory -Path $archDir -Force | Out-Null + +function Copy-TemplateIfMissing { + param( + [Parameter(Mandatory = $true)][string]$TemplateName, + [Parameter(Mandatory = $true)][string]$Destination + ) + + if (Test-Path -LiteralPath $Destination -PathType Leaf) { + return + } + + $template = Resolve-ArchitectureTemplate -TemplateName $TemplateName -RepoRoot $repoRoot + if ($template -and (Test-Path -LiteralPath $template -PathType Leaf)) { + Copy-Item -LiteralPath $template -Destination $Destination -Force + Write-Output "Copied $TemplateName template to $Destination" + } else { + Write-Warning "$TemplateName template not found" + New-Item -ItemType File -Path $Destination -Force | Out-Null + } +} + +Copy-TemplateIfMissing -TemplateName "architecture-template" -Destination $archFile +Copy-TemplateIfMissing -TemplateName "architecture-scenario-template" -Destination $scenarioView +Copy-TemplateIfMissing -TemplateName "architecture-logical-template" -Destination $logicalView +Copy-TemplateIfMissing -TemplateName "architecture-process-template" -Destination $processView +Copy-TemplateIfMissing -TemplateName "architecture-development-template" -Destination $developmentView +Copy-TemplateIfMissing -TemplateName "architecture-physical-template" -Destination $physicalView + +if ($Json) { + [PSCustomObject]@{ + ARCH_FILE = $archFile + ARCH_DIR = $archDir + SCENARIO_VIEW = $scenarioView + LOGICAL_VIEW = $logicalView + PROCESS_VIEW = $processView + DEVELOPMENT_VIEW = $developmentView + PHYSICAL_VIEW = $physicalView + } | ConvertTo-Json -Compress +} else { + Write-Output "ARCH_FILE: $archFile" + Write-Output "ARCH_DIR: $archDir" + Write-Output "SCENARIO_VIEW: $scenarioView" + Write-Output "LOGICAL_VIEW: $logicalView" + Write-Output "PROCESS_VIEW: $processView" + Write-Output "DEVELOPMENT_VIEW: $developmentView" + Write-Output "PHYSICAL_VIEW: $physicalView" +} diff --git a/extensions/arch/templates/architecture-development-template.md b/extensions/arch/templates/architecture-development-template.md new file mode 100644 index 0000000000..59bcb47136 --- /dev/null +++ b/extensions/arch/templates/architecture-development-template.md @@ -0,0 +1,73 @@ +# Development View + +**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. + +## Architecture Intent + +[State what component, package, contract, or dependency meaning this view must preserve.] + +## Core Tensions + +| Tension | Current Tradeoff Direction | Development Consequence | +|---------|----------------------------|-------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Stable Boundaries + +| Boundary | Must Remain Stable Because | Explicitly Must Not Own | +|----------|----------------------------|-------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Change Axes + +| Expected Change | Isolated By | Development Impact | +|-----------------|-------------|--------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Invariants + +| Invariant | Source Boundary / Contract / Dependency Rule | Risk If Violated | +|-----------|----------------------------------------------|------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Non-goals / Anti-patterns + +| Non-goal / Anti-pattern | Why It Is Out of Scope or Harmful | +|-------------------------|-----------------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Architecture-Level Components + +| Component / Capability Package | Responsibility | Input / Output Boundary | Collaborators | Explicitly Must Not Own | Source View Evidence | +|--------------------------------|----------------|-------------------------|---------------|--------------------------|----------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Package Boundary Intent + +| Package / Boundary | Abstraction Level | Owned Concepts | May Depend On | Must Not Depend On | Evolution Rule | +|--------------------|-------------------|----------------|---------------|--------------------|----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Contracts and Artifacts + +| Contract / Artifact | Semantics | Producer | Consumer | Lifecycle | Architecture Consequence | +|---------------------|-----------|----------|----------|-----------|--------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Dependency Rules + +| Rule | Allowed Direction | Forbidden Direction | Reason | Risk If Violated | +|------|-------------------|---------------------|--------|------------------| +| NEEDS ARCH UPDATE | 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 new file mode 100644 index 0000000000..3e6514b850 --- /dev/null +++ b/extensions/arch/templates/architecture-logical-template.md @@ -0,0 +1,73 @@ +# Logical View + +**Input**: `.specify/memory/architecture-scenario-view.md` + +**Purpose**: Derive 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.] + +## Core Tensions + +| Tension | Current Tradeoff Direction | Logical Consequence | +|---------|----------------------------|---------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Stable Boundaries + +| Boundary | Must Remain Stable Because | Explicitly Does Not Own | +|----------|----------------------------|-------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Change Axes + +| Expected Change | Isolated By | Logical Impact | +|-----------------|-------------|----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Invariants + +| Invariant | Source Scenario / Object / State | Risk If Violated | +|-----------|----------------------------------|------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Non-goals / Anti-patterns + +| Non-goal / Anti-pattern | Why It Is Out of Scope or Harmful | +|-------------------------|-----------------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Capability Boundaries + +| Capability / Boundary | Responsibility | Input | Output | Explicitly Does Not Own | Scenario Source | +|-----------------------|----------------|-------|--------|--------------------------|-----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Domain Objects and Relationships + +| Object | Meaning | Owning Capability | Key Relationships | Fact Source | Invariants | +|--------|---------|-------------------|-------------------|-------------|------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## State and Lifecycle + +| Object / Flow | State | Entered When | Exited When | Forbidden Transition | Responsible Boundary | +|---------------|-------|--------------|-------------|----------------------|----------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Logical Decisions + +| Decision | Scope | Owner / Boundary | Affected Objects or Flows | Consequence | +|----------|-------|------------------|---------------------------|-------------| +| NEEDS ARCH UPDATE | 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 new file mode 100644 index 0000000000..a300eaaaa5 --- /dev/null +++ b/extensions/arch/templates/architecture-physical-template.md @@ -0,0 +1,73 @@ +# Physical View + +**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. + +## Architecture Intent + +[State what deployment, fact-source, operational, or external-boundary meaning this view must preserve.] + +## Core Tensions + +| Tension | Current Tradeoff Direction | Physical Consequence | +|---------|----------------------------|----------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Stable Boundaries + +| Boundary | Must Remain Stable Because | Explicitly Does Not Carry | +|----------|----------------------------|---------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Change Axes + +| Expected Change | Isolated By | Physical Impact | +|-----------------|-------------|-----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Invariants + +| Invariant | Source Deployment / External / Fact Boundary | Risk If Violated | +|-----------|----------------------------------------------|------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Non-goals / Anti-patterns + +| Non-goal / Anti-pattern | Why It Is Out of Scope or Harmful | +|-------------------------|-----------------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Deployment and Hosting Boundaries + +| Runtime / Hosting Unit | Carries | Boundary | Depends On | Release / Migration Impact | +|------------------------|---------|----------|------------|----------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## External System Collaboration + +| External System | Purpose | Exchanged Content | Authoritative Fact | Failure Impact | Isolation / Substitute Boundary | +|-----------------|---------|-------------------|--------------------|----------------|---------------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Fact Sources and Observability + +| Fact / Event | Authoritative Source | Observable Location | Consumers | Traceability Requirement | +|--------------|----------------------|---------------------|-----------|--------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Operations and Release Boundaries + +| Operational Concern | Responsible Boundary | Trigger | Affected Views | Architecture Consequence | +|---------------------|----------------------|---------|----------------|--------------------------| +| NEEDS ARCH UPDATE | 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 new file mode 100644 index 0000000000..a24bce6ca8 --- /dev/null +++ b/extensions/arch/templates/architecture-process-template.md @@ -0,0 +1,73 @@ +# Process View + +**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. + +## Architecture Intent + +[State what runtime collaboration, handoff, or failure-closure meaning this view must preserve.] + +## Core Tensions + +| Tension | Current Tradeoff Direction | Process Consequence | +|---------|----------------------------|---------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Stable Boundaries + +| Boundary | Must Remain Stable Because | Explicitly Does Not Control | +|----------|----------------------------|-----------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Change Axes + +| Expected Change | Isolated By | Process Impact | +|-----------------|-------------|----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Invariants + +| Invariant | Source Scenario / Runtime Link | Risk If Violated | +|-----------|--------------------------------|------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Non-goals / Anti-patterns + +| Non-goal / Anti-pattern | Why It Is Out of Scope or Harmful | +|-------------------------|-----------------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Main Runtime Links + +| Runtime Link | Trigger | Source | Target | Transferred Content / Fact | Completion Condition | +|--------------|---------|--------|--------|----------------------------|----------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Handoffs and Approvals + +| Handoff / Approval | From | To | Meaning | Accepted Path | Rejected / Returned Path | +|--------------------|------|----|---------|---------------|--------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Receipts and User Participation + +| Receipt / Participation Point | Sender | Receiver | Content | User Action | Architecture Consequence | +|-------------------------------|--------|----------|---------|-------------|--------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Failure, Degradation, and Closure + +| Failure / Branch | Detection Boundary | Responsible Boundary | Degradation or Compensation | User-Visible Result | Closure Condition | +|------------------|--------------------|----------------------|-----------------------------|---------------------|-------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | 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-scenario-template.md b/extensions/arch/templates/architecture-scenario-template.md new file mode 100644 index 0000000000..08026e3258 --- /dev/null +++ b/extensions/arch/templates/architecture-scenario-template.md @@ -0,0 +1,71 @@ +# Scenario View + +**Purpose**: Produce the UC 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.] + +## Core Tensions + +| Tension | Current Tradeoff Direction | Scenario Consequence | +|---------|----------------------------|----------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Stable Boundaries + +| Boundary | Must Remain Stable Because | Explicitly Does Not Cover | +|----------|----------------------------|---------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Change Axes + +| Expected Change | Isolated By | Scenario Impact | +|-----------------|-------------|-----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Invariants + +| Invariant | Scenario Evidence | Risk If Violated | +|-----------|-------------------|------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Non-goals / Anti-patterns + +| Non-goal / Anti-pattern | Why It Is Out of Scope or Harmful | +|-------------------------|-----------------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Actors and Participants + +| Actor / Participant | Goal | Responsibility | Boundary | +|---------------------|------|----------------|----------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Use Cases + +| Use Case | Actor | Goal | Preconditions | Scope Boundary | +|----------|-------|------|---------------|----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Scenario Paths + +| Scenario | Main Path | Successful Outcome | Alternative / Failure Branches | +|----------|-----------|--------------------|--------------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Acceptance Semantics + +| Acceptance Scenario | Observable Result | Must Hold | Not Covered | +|---------------------|-------------------|-----------|-------------| +| 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 new file mode 100644 index 0000000000..c0bbd5a100 --- /dev/null +++ b/extensions/arch/templates/architecture-template.md @@ -0,0 +1,78 @@ +# Architecture Synthesis: [PROJECT] + +**Input Views**: +- Scenario: `.specify/memory/architecture-scenario-view.md` +- Logical: `.specify/memory/architecture-logical-view.md` +- Process: `.specify/memory/architecture-process-view.md` +- 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 | +| 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 | +| Physical | `.specify/memory/architecture-physical-view.md` | Deployment, external systems, fact sources, observability, operations | NEEDS ARCH UPDATE | + +## Architecture Intent + +[State what architecture intent the five views stabilize together.] + +## 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 Tradeoffs + +| Tradeoff | Chosen Direction | Consequence | Revisit When | +|----------|------------------|-------------|--------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Stable Boundaries + +| Boundary | Affected Views | Must Remain Stable Because | Forbidden Crossing | +|----------|----------------|----------------------------|--------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Change Axes + +| Expected Change | Isolated By | Affected Views | Architecture Consequence | +|-----------------|-------------|----------------|--------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Anti-patterns + +| Anti-pattern | Why It Violates Intent | Affected Views | +|--------------|------------------------|----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Cross-View Architecture Model + +This section normalizes the 4+1 design results into the architecture SSOT for later `plan` reasoning. 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. + +| Architecture Concept | Scenario Meaning | Logical Interpretation | Runtime Role | Development Boundary | Physical Constraint | Plan Reasoning Constraint | +|----------------------|------------------|------------------------|--------------|----------------------|---------------------|---------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Key Architecture Conclusions + +| Conclusion | Affected Views | Boundary/Owner | Consequence | +|------------|----------------|----------------|-------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Cross-Cutting Constraints + +| Constraint | Source | Affected Views | Scope | Architecture Consequence | +|------------|--------|----------------|-------|--------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Open Risks and Review Triggers + +| Risk or Trigger | Missing Evidence / Change Condition | Affected Views | Required Architecture Review | +|-----------------|-------------------------------------|----------------|------------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | diff --git a/extensions/catalog.json b/extensions/catalog.json index de9372e2bc..1921ca2a12 100644 --- a/extensions/catalog.json +++ b/extensions/catalog.json @@ -1,8 +1,23 @@ { "schema_version": "1.0", - "updated_at": "2026-04-10T00:00:00Z", + "updated_at": "2026-05-13T00:00:00Z", "catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json", "extensions": { + "arch": { + "name": "Architecture Workflow", + "id": "arch", + "version": "1.0.0", + "description": "Generate project-level 4+1 architecture view artifacts and synthesis", + "author": "spec-kit-core", + "repository": "https://github.com/github/spec-kit", + "bundled": true, + "tags": [ + "architecture", + "4+1", + "workflow", + "core" + ] + }, "git": { "name": "Git Branching Workflow", "id": "git", @@ -19,4 +34,4 @@ ] } } -} \ No newline at end of file +} diff --git a/pyproject.toml b/pyproject.toml index 92735f3e9e..f6cd205a30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,8 @@ packages = ["src/specify_cli"] "scripts/bash" = "specify_cli/core_pack/scripts/bash" "scripts/powershell" = "specify_cli/core_pack/scripts/powershell" # Bundled extensions (installable via `specify extension add `) +"extensions/catalog.json" = "specify_cli/core_pack/extensions/catalog.json" +"extensions/arch" = "specify_cli/core_pack/extensions/arch" "extensions/git" = "specify_cli/core_pack/extensions/git" # Bundled workflows (auto-installed during `specify init`) "workflows/speckit" = "specify_cli/core_pack/workflows/speckit" @@ -70,4 +72,3 @@ omit = ["*/tests/*", "*/__pycache__/*"] precision = 2 show_missing = true skip_covered = false - diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index d3eb36391e..43a957a34a 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -1649,7 +1649,8 @@ def _display_cmd(name: str) -> str: "", f"○ [cyan]{_display_cmd('clarify')}[/] [bright_black](optional)[/bright_black] - Ask structured questions to de-risk ambiguous areas before planning (run before [cyan]{_display_cmd('plan')}[/] if used)", f"○ [cyan]{_display_cmd('analyze')}[/] [bright_black](optional)[/bright_black] - Cross-artifact consistency & alignment report (after [cyan]{_display_cmd('tasks')}[/], before [cyan]{_display_cmd('implement')}[/])", - f"○ [cyan]{_display_cmd('checklist')}[/] [bright_black](optional)[/bright_black] - Generate quality checklists to validate requirements completeness, clarity, and consistency (after [cyan]{_display_cmd('plan')}[/])" + f"○ [cyan]{_display_cmd('checklist')}[/] [bright_black](optional)[/bright_black] - Generate quality checklists to validate requirements completeness, clarity, and consistency (after [cyan]{_display_cmd('plan')}[/])", + "○ [cyan]/speckit.arch.generate[/] [bright_black](extension)[/bright_black] - Install with [cyan]specify extension add arch[/cyan] to shape 4+1 architecture views", ] enhancements_title = "Enhancement Skills" if native_skill_mode else "Enhancement Commands" enhancements_panel = Panel("\n".join(enhancement_lines), title=enhancements_title, border_style="cyan", padding=(1,2)) diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index 944ee4a06d..080ec20b37 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -1669,6 +1669,7 @@ def register_commands_for_claude( class ExtensionCatalog: """Manages extension catalog fetching, caching, and searching.""" + BUNDLED_CATALOG_URL = "bundled://extensions/catalog.json" DEFAULT_CATALOG_URL = "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json" COMMUNITY_CATALOG_URL = "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json" CACHE_DURATION = 3600 # 1 hour in seconds @@ -1842,8 +1843,9 @@ def get_active_catalogs(self) -> List[CatalogEntry]: # 4. Built-in default stack return [ - CatalogEntry(url=self.DEFAULT_CATALOG_URL, name="default", priority=1, install_allowed=True, description="Built-in catalog of installable extensions"), - CatalogEntry(url=self.COMMUNITY_CATALOG_URL, name="community", priority=2, install_allowed=False, description="Community-contributed extensions (discovery only)"), + CatalogEntry(url=self.BUNDLED_CATALOG_URL, name="bundled", priority=1, install_allowed=True, description="Bundled official extensions shipped with this install"), + CatalogEntry(url=self.DEFAULT_CATALOG_URL, name="default", priority=2, install_allowed=True, description="Built-in catalog of installable extensions"), + CatalogEntry(url=self.COMMUNITY_CATALOG_URL, name="community", priority=3, install_allowed=False, description="Community-contributed extensions (discovery only)"), ] def get_catalog_url(self) -> str: @@ -1880,6 +1882,23 @@ def _fetch_single_catalog(self, entry: CatalogEntry, force_refresh: bool = False """ import urllib.error + if entry.url == self.BUNDLED_CATALOG_URL: + candidate_paths = [ + Path(__file__).parent / "core_pack" / "extensions" / "catalog.json", + Path(__file__).resolve().parent.parent.parent / "extensions" / "catalog.json", + ] + for candidate in candidate_paths: + if not candidate.is_file(): + continue + try: + catalog_data = json.loads(candidate.read_text(encoding="utf-8")) + except json.JSONDecodeError as e: + raise ExtensionError(f"Invalid JSON in bundled catalog {candidate}: {e}") + if "schema_version" not in catalog_data or "extensions" not in catalog_data: + raise ExtensionError(f"Invalid bundled catalog format from {candidate}") + return catalog_data + raise ExtensionError("Bundled extension catalog not found") + # Determine cache file paths (backward compat for default catalog) if entry.url == self.DEFAULT_CATALOG_URL: cache_file = self.cache_file diff --git a/tests/test_arch_templates.py b/tests/test_arch_templates.py new file mode 100644 index 0000000000..66a097d2a6 --- /dev/null +++ b/tests/test_arch_templates.py @@ -0,0 +1,167 @@ +"""Quality guards for 4+1 architecture templates and command.""" + +import re +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +ARCHITECTURE_EXTENSION = PROJECT_ROOT / "extensions" / "arch" +TEMPLATES = ARCHITECTURE_EXTENSION / "templates" +COMMANDS = ARCHITECTURE_EXTENSION / "commands" + + +def _read_template(name: str) -> str: + return (TEMPLATES / name).read_text(encoding="utf-8") + + +def test_arch_command_is_phase_based_and_does_not_require_uc_command(): + content = (COMMANDS / "speckit.arch.generate.md").read_text(encoding="utf-8") + + assert "scripts:" in content + assert ".specify/extensions/arch/scripts/bash/setup-arch.sh --json" in content + assert ".specify/extensions/arch/scripts/powershell/setup-arch.ps1 -Json" in content + for phase in [ + "Phase -1: Architecture Framing", + "Phase 0: Scenario View", + "Phase 1: Logical View", + "Phase 2: Process View", + "Phase 3: Development View", + "Phase 4: Physical View", + "Phase 5: Architecture Synthesis", + ]: + assert phase in content + assert "Before filling any view, identify the architecture judgment" in content + assert "Architecture Reasoning Layer" in content + assert "Representation Layer" in content + assert "project-level architecture SSOT" in content + assert "constrain later `plan` reasoning to stay inside the architecture SSOT" in content + assert "Use each view template as the source of truth for that view's reasoning contract" in content + assert "Produce architecture design inference, not tracking, audit, or implementation planning" in content + assert "normalizes architecture meaning for synthesis and later `plan` reasoning" in content + assert "Markdown tables are the default artifact structure" in content + assert "Optional diagrams are renderings, not reasoning inputs" in content + assert "Add optional diagrams only after the relevant view's reasoning is complete" in content + assert "Defer any optional diagram or notation-specific rendering until the affected view's 4+1 reasoning" in content + assert "Representation choices:" not in content + assert "Before filling tables" not in content + assert "Architecture Gates" in content + assert "ERROR if a boundary has responsibilities but no explicit non-responsibility" in content + assert "ERROR if notation-specific output changes 4+1 view responsibilities" in content + assert "Use Case, Domain Object, Component, Container, or Deployment Unit" in content + for term in ["C4", "UML", "Mermaid", "PlantUML"]: + assert len(re.findall(rf"\b{re.escape(term)}\b", content)) == 1 + assert "Do not require `.specify/memory/uc.md`" in content + assert "Read the six architecture templates under `.specify/extensions/arch/templates/`" in content + assert "__SPECKIT_COMMAND_UC__" not in content + assert "Read `.specify/memory/constitution.md`" not in content + assert ".specify/memory/architecture/" not in content + + +def test_arch_command_delegates_view_details_to_templates(): + content = (COMMANDS / "speckit.arch.generate.md").read_text(encoding="utf-8") + + delegated_phrases = [ + "using its template", + "following the scenario view template", + "following the logical view template", + "following the process view template", + "following the development view template", + "following the physical view template", + "following the synthesis template", + ] + for phrase in delegated_phrases: + assert phrase in content + + template_owned_details = [ + "Actors and external participants", + "System capability boundaries", + "Main runtime links", + "Architecture-level components or capability packages", + "Deployment and hosting boundaries", + "Do not write class models", + "Do not write call stacks", + "Do not write Kubernetes YAML", + ] + for phrase in template_owned_details: + assert phrase not in content + + +def test_architecture_synthesis_references_five_view_files(): + content = _read_template("architecture-template.md") + + for filename in [ + "architecture-scenario-view.md", + "architecture-logical-view.md", + "architecture-process-view.md", + "architecture-development-view.md", + "architecture-physical-view.md", + ]: + assert f".specify/memory/{filename}" in content + assert "Cross-View Architecture Model" in content + assert "normalizes the 4+1 design results into the architecture SSOT for later `plan` reasoning" in content + assert "This is architecture design synthesis, not tracking or audit" in content + assert "Do not treat view-specific concepts as equivalent or interchangeable" in content + assert "Plan Reasoning Constraint" in content + assert "Key Architecture Conclusions" in content + for section in [ + "Architecture Intent", + "Central Design Forces", + "Primary Tradeoffs", + "Stable Boundaries", + "Change Axes", + "Anti-patterns", + ]: + assert section in content + assert ".specify/memory/architecture/" not in content + + +def test_init_next_steps_do_not_list_arch_as_core_workflow(): + init_source = (PROJECT_ROOT / "src" / "specify_cli" / "__init__.py").read_text(encoding="utf-8") + + assert "_display_cmd('arch')" not in init_source + assert "specify extension add arch" in init_source + + +def test_view_templates_define_inputs_and_reject_implementation_detail(): + scenario = _read_template("architecture-scenario-template.md") + logical = _read_template("architecture-logical-template.md") + process = _read_template("architecture-process-template.md") + development = _read_template("architecture-development-template.md") + physical = _read_template("architecture-physical-template.md") + + assert "Produce the UC semantics" in scenario + assert "Do not write architecture components" in scenario + assert "**Input**: `.specify/memory/architecture-scenario-view.md`" in logical + assert "Do not write classes, DTOs, database tables" in logical + assert "**Input**: `.specify/memory/architecture-scenario-view.md`, `.specify/memory/architecture-logical-view.md`" in process + assert "Do not write call stacks, queue names, retry counts" in process + assert "**Input**: `.specify/memory/architecture-logical-view.md`, `.specify/memory/architecture-process-view.md`" in development + assert "Do not write source file paths, concrete package trees" in development + assert "**Input**: `.specify/memory/architecture-process-view.md`, `.specify/memory/architecture-development-view.md`" in physical + assert "Do not write Kubernetes YAML, cloud resource manifests" in physical + + for content in [scenario, logical, process, development, physical]: + for section in [ + "Architecture Intent", + "Core Tensions", + "Stable Boundaries", + "Change Axes", + "Invariants", + "Non-goals / Anti-patterns", + ]: + assert section in content + + +def test_view_templates_keep_notations_out_of_reasoning_contracts(): + view_contents = [ + _read_template("architecture-scenario-template.md"), + _read_template("architecture-logical-template.md"), + _read_template("architecture-process-template.md"), + _read_template("architecture-development-template.md"), + _read_template("architecture-physical-template.md"), + ] + + notation_terms = ["C4", "UML", "Mermaid", "PlantUML", "notation-specific"] + for content in view_contents: + for term in notation_terms: + assert term not in content diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 1434ba309d..50bb83e007 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -205,6 +205,24 @@ def test_core_command_names_match_bundled_templates(self): } assert CORE_COMMAND_NAMES == expected + assert "arch" not in CORE_COMMAND_NAMES + + def test_bundled_architecture_manifest(self): + """The architecture workflow is provided as a bundled extension, not a core command.""" + manifest_path = ( + Path(__file__).resolve().parent.parent + / "extensions" + / "arch" + / "extension.yml" + ) + manifest = ExtensionManifest(manifest_path) + + assert manifest.id == "arch" + assert len(manifest.commands) == 1 + command = manifest.commands[0] + assert command["name"] == "speckit.arch.generate" + assert command["file"] == "commands/speckit.arch.generate.md" + assert command.get("aliases") == [] def test_missing_required_field(self, temp_dir): """Test manifest missing required field.""" @@ -769,6 +787,48 @@ def test_install_from_directory(self, extension_dir, project_dir): assert (ext_dir / "extension.yml").exists() assert (ext_dir / "commands" / "hello.md").exists() + def test_install_bundled_architecture_registers_markdown_command(self, project_dir): + """Architecture extension should register its namespaced command without the legacy alias.""" + source_dir = Path(__file__).resolve().parent.parent / "extensions" / "arch" + commands_dir = project_dir / ".qwen" / "commands" + commands_dir.mkdir(parents=True) + (project_dir / ".specify" / "init-options.json").parent.mkdir(parents=True, exist_ok=True) + (project_dir / ".specify" / "init-options.json").write_text( + '{"ai":"qwen","script":"sh"}', + encoding="utf-8", + ) + + manager = ExtensionManager(project_dir) + manager.install_from_directory(source_dir, "0.8.10", register_commands=True) + + command_file = commands_dir / "speckit.arch.generate.md" + assert command_file.exists() + assert not (commands_dir / "speckit.arch.md").exists() + content = command_file.read_text(encoding="utf-8") + assert "{SCRIPT}" not in content + assert ".specify/extensions/arch/scripts/bash/setup-arch.sh --json" in content + + def test_install_bundled_architecture_registers_skill_command(self, project_dir): + """Architecture extension should register as speckit-arch-generate for skill agents.""" + source_dir = Path(__file__).resolve().parent.parent / "extensions" / "arch" + skills_dir = project_dir / ".agents" / "skills" + skills_dir.mkdir(parents=True) + (project_dir / ".specify" / "init-options.json").parent.mkdir(parents=True, exist_ok=True) + (project_dir / ".specify" / "init-options.json").write_text( + '{"ai":"codex","ai_skills":true,"script":"sh"}', + encoding="utf-8", + ) + + manager = ExtensionManager(project_dir) + manager.install_from_directory(source_dir, "0.8.10", register_commands=True) + + skill_file = skills_dir / "speckit-arch-generate" / "SKILL.md" + assert skill_file.exists() + assert not (skills_dir / "speckit-arch" / "SKILL.md").exists() + content = skill_file.read_text(encoding="utf-8") + assert "{SCRIPT}" not in content + assert ".specify/extensions/arch/scripts/bash/setup-arch.sh --json" in content + def test_install_duplicate(self, extension_dir, project_dir): """Test installing already installed extension.""" manager = ExtensionManager(project_dir) @@ -2679,21 +2739,46 @@ def _write_valid_cache( # --- get_active_catalogs --- def test_default_stack(self, temp_dir): - """Default stack includes default and community catalogs.""" + """Default stack includes bundled, default, and community catalogs.""" project_dir = self._make_project(temp_dir) catalog = ExtensionCatalog(project_dir) entries = catalog.get_active_catalogs() - assert len(entries) == 2 - assert entries[0].url == ExtensionCatalog.DEFAULT_CATALOG_URL - assert entries[0].name == "default" + assert len(entries) == 3 + assert entries[0].url == ExtensionCatalog.BUNDLED_CATALOG_URL + assert entries[0].name == "bundled" assert entries[0].priority == 1 assert entries[0].install_allowed is True - assert entries[1].url == ExtensionCatalog.COMMUNITY_CATALOG_URL - assert entries[1].name == "community" + assert entries[1].url == ExtensionCatalog.DEFAULT_CATALOG_URL + assert entries[1].name == "default" assert entries[1].priority == 2 - assert entries[1].install_allowed is False + assert entries[1].install_allowed is True + assert entries[2].url == ExtensionCatalog.COMMUNITY_CATALOG_URL + assert entries[2].name == "community" + assert entries[2].priority == 3 + assert entries[2].install_allowed is False + + def test_bundled_catalog_discovers_arch(self, temp_dir, monkeypatch): + """Bundled official extensions should be searchable before remote catalog publication.""" + project_dir = self._make_project(temp_dir) + catalog = ExtensionCatalog(project_dir) + + def fake_fetch(entry, force_refresh=False): + if entry.url == ExtensionCatalog.BUNDLED_CATALOG_URL: + return ExtensionCatalog._fetch_single_catalog(catalog, entry, force_refresh) + return {"schema_version": "1.0", "extensions": {}} + + monkeypatch.setattr(catalog, "_fetch_single_catalog", fake_fetch) + + results = catalog.search(query="arch") + + assert any( + result["id"] == "arch" + and result["_catalog_name"] == "bundled" + and result["_install_allowed"] is True + for result in results + ) def test_env_var_overrides_default_stack(self, temp_dir, monkeypatch): """SPECKIT_CATALOG_URL replaces the entire default stack.""" diff --git a/tests/test_setup_arch.py b/tests/test_setup_arch.py new file mode 100644 index 0000000000..6fc016e018 --- /dev/null +++ b/tests/test_setup_arch.py @@ -0,0 +1,163 @@ +"""Tests for architecture extension artifact initialization.""" + +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +from tests.conftest import requires_bash + + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +ARCHITECTURE_EXTENSION = PROJECT_ROOT / "extensions" / "arch" +SETUP_ARCH_SH = ARCHITECTURE_EXTENSION / "scripts" / "bash" / "setup-arch.sh" +SETUP_ARCH_PS = ARCHITECTURE_EXTENSION / "scripts" / "powershell" / "setup-arch.ps1" +ARCH_TEMPLATES = [ + "architecture-template.md", + "architecture-scenario-template.md", + "architecture-logical-template.md", + "architecture-process-template.md", + "architecture-development-template.md", + "architecture-physical-template.md", +] + +HAS_PWSH = shutil.which("pwsh") is not None +_POWERSHELL = shutil.which("powershell.exe") or shutil.which("powershell") + + +def _install_bash_scripts(repo: Path) -> None: + d = repo / ".specify" / "extensions" / "arch" / "scripts" / "bash" + d.mkdir(parents=True, exist_ok=True) + shutil.copy(SETUP_ARCH_SH, d / "setup-arch.sh") + + +def _install_ps_scripts(repo: Path) -> None: + d = repo / ".specify" / "extensions" / "arch" / "scripts" / "powershell" + d.mkdir(parents=True, exist_ok=True) + shutil.copy(SETUP_ARCH_PS, d / "setup-arch.ps1") + + +def _install_templates(repo: Path) -> None: + d = repo / ".specify" / "extensions" / "arch" / "templates" + d.mkdir(parents=True, exist_ok=True) + for name in ARCH_TEMPLATES: + shutil.copy(ARCHITECTURE_EXTENSION / "templates" / name, d / name) + + +def _clean_env() -> dict[str, str]: + env = os.environ.copy() + for key in list(env): + if key.startswith("SPECIFY_"): + env.pop(key) + return env + + +def _powershell_script_arg(exe: str, script: Path) -> str: + if sys.platform != "win32" and str(exe).endswith("powershell.exe") and shutil.which("wslpath"): + result = subprocess.run( + ["wslpath", "-w", str(script)], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + return str(script) + + +@pytest.fixture +def arch_repo(tmp_path: Path) -> Path: + repo = tmp_path / "proj" + repo.mkdir() + (repo / ".specify").mkdir() + _install_templates(repo) + _install_bash_scripts(repo) + _install_ps_scripts(repo) + return repo + + +def _json_from_output(output: str) -> dict[str, str]: + for line in reversed(output.strip().splitlines()): + line = line.strip() + if line.startswith("{") and line.endswith("}"): + return json.loads(line) + raise AssertionError(f"No JSON object found in output:\n{output}") + + +def _assert_arch_json(repo: Path, data: dict[str, str], *, exact_paths: bool = True) -> None: + expected = { + "ARCH_FILE": repo / ".specify" / "memory" / "architecture.md", + "ARCH_DIR": repo / ".specify" / "memory", + "SCENARIO_VIEW": repo / ".specify" / "memory" / "architecture-scenario-view.md", + "LOGICAL_VIEW": repo / ".specify" / "memory" / "architecture-logical-view.md", + "PROCESS_VIEW": repo / ".specify" / "memory" / "architecture-process-view.md", + "DEVELOPMENT_VIEW": repo / ".specify" / "memory" / "architecture-development-view.md", + "PHYSICAL_VIEW": repo / ".specify" / "memory" / "architecture-physical-view.md", + } + assert set(data) == set(expected) + for key, path in expected.items(): + if exact_paths: + assert Path(data[key]) == path + else: + normalized = data[key].replace("\\", "/") + assert normalized.endswith(path.relative_to(repo).as_posix()) + assert path.is_file() if key != "ARCH_DIR" else path.is_dir() + + +@requires_bash +def test_setup_arch_bash_creates_all_artifacts_and_json(arch_repo: Path) -> None: + script = arch_repo / ".specify" / "extensions" / "arch" / "scripts" / "bash" / "setup-arch.sh" + result = subprocess.run( + ["bash", str(script), "--json"], + cwd=arch_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode == 0, result.stderr + result.stdout + data = _json_from_output(result.stdout) + _assert_arch_json(arch_repo, data) + assert "Scenario View" in (arch_repo / ".specify" / "memory" / "architecture-scenario-view.md").read_text(encoding="utf-8") + + +@requires_bash +def test_setup_arch_bash_preserves_existing_files(arch_repo: Path) -> None: + existing = arch_repo / ".specify" / "memory" / "architecture-scenario-view.md" + existing.parent.mkdir(parents=True) + existing.write_text("# Custom Scenario\n", encoding="utf-8") + + script = arch_repo / ".specify" / "extensions" / "arch" / "scripts" / "bash" / "setup-arch.sh" + result = subprocess.run( + ["bash", str(script), "--json"], + cwd=arch_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode == 0, result.stderr + result.stdout + assert existing.read_text(encoding="utf-8") == "# Custom Scenario\n" + + +@pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available") +def test_setup_arch_powershell_creates_all_artifacts_and_json(arch_repo: Path) -> None: + script = arch_repo / ".specify" / "extensions" / "arch" / "scripts" / "powershell" / "setup-arch.ps1" + exe = "pwsh" if HAS_PWSH else _POWERSHELL + result = subprocess.run( + [exe, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", _powershell_script_arg(exe, script), "-Json"], + cwd=arch_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode == 0, result.stderr + result.stdout + data = _json_from_output(result.stdout) + _assert_arch_json(arch_repo, data, exact_paths=False)