diff --git a/docs/SPECSMITH.yml b/docs/SPECSMITH.yml index 6b6585a1..aaa33432 100644 --- a/docs/SPECSMITH.yml +++ b/docs/SPECSMITH.yml @@ -9,7 +9,7 @@ description: | through governance, traceability, and automated compliance checking. type: python -version: 0.22.3 +version: 0.22.4 license: MIT author: Layer1 Labs url: https://github.com/layer1labs/specsmith diff --git a/docs/releases/0.22.4.md b/docs/releases/0.22.4.md new file mode 100644 index 00000000..87b97505 --- /dev/null +++ b/docs/releases/0.22.4.md @@ -0,0 +1,26 @@ +# Specsmith 0.22.4 — 2026-07-15 + +## Added + +- Universal Zoo Code lifecycle commands: `specsmith zoo-code setup`, `doctor`, and `uninstall`. +- Managed global Specsmith rules, slash commands, and skills under the Zoo Code global directory. +- Safe backup and adoption of unmanaged files at reserved Specsmith paths. +- Managed manifests, stale-asset removal, and migration from the earlier v1 marker. +- Project MCP merging that preserves unrelated servers. +- Recognition and removal of generic Specsmith rule duplicates from project `.roo` directories while preserving customized files. +- `--scope`, `--global-roo`, `ROO_GLOBAL_DIR`, `--dry-run`, and `--preserve-existing` controls. +- Cross-platform lifecycle tests and CLI registration coverage. + +## Changed + +- Specsmith is now the canonical distribution point for reusable Zoo Code integration assets. +- Project repositories retain ownership of project-specific rules, commands, skills, custom modes, and provider/model configuration. +- Package revision advanced from 0.22.3 to 0.22.4. + +## Safety + +- Uninstall removes only marker-owned global assets. +- Customized workspace files are preserved. +- The `specsmith-governance` MCP entry is removed only when it still matches the managed value. + +Closes issue #307. diff --git a/docs/site/zoo-code-assets.md b/docs/site/zoo-code-assets.md new file mode 100644 index 00000000..de9add3f --- /dev/null +++ b/docs/site/zoo-code-assets.md @@ -0,0 +1,30 @@ +# Portable Zoo Code integration + +Specsmith is the canonical distribution point for reusable Zoo Code rules, slash commands, skills, and MCP configuration. Project repositories retain only project-specific `.roo` assets and model/provider settings. + +## Install or update + +```bash +pipx upgrade specsmith +cd /path/to/project +specsmith zoo-code setup --project-dir . +specsmith zoo-code doctor --project-dir . +``` + +By default, setup installs reusable Specsmith assets under `~/.roo`, merges the `specsmith-governance` MCP server into `.roo/mcp.json`, and removes recognized generic Specsmith rule duplicates from the workspace. + +Use `ROO_GLOBAL_DIR` or `--global-roo` to override the global Zoo Code directory. Use `--scope global` or `--scope project` to operate on one side only. `--dry-run` previews changes. `--preserve-existing` refuses to adopt an unmanaged file at a reserved Specsmith path. + +## Safety + +Existing unmanaged files at reserved global paths are backed up before adoption. Customized workspace files are preserved and reported. Setup removes stale assets only when they carry a Specsmith managed marker. Uninstall removes only managed assets and removes the MCP entry only when it still matches the Specsmith-managed value. + +## Remove + +```bash +specsmith zoo-code uninstall --project-dir . +``` + +## Repository ownership boundary + +Specsmith owns reusable global rules, commands, skills, lifecycle manifests, migration, doctor, uninstall, and the standard MCP merge. Individual repositories own project-specific rules, commands, skills, custom modes, provider configuration, and local model routing. diff --git a/docs/site/zoo-code-roo.md b/docs/site/zoo-code-roo.md index 43f31121..d5e738a9 100644 --- a/docs/site/zoo-code-roo.md +++ b/docs/site/zoo-code-roo.md @@ -1,23 +1,31 @@ # Zoo Code / Roo Code Integration -Specsmith integrates with Zoo Code / Roo Code through a repo-local `.roo/` directory and the Specsmith MCP governance server. +Specsmith integrates with Zoo Code / Roo Code through reusable global assets, a repo-local `.roo/` directory, and the Specsmith MCP governance server. -## What belongs in the repository +## Setup -Project-local integration files: +Install or upgrade Specsmith with pipx, then configure the active project: -| File | Purpose | -|---|---| -| `.roo/mcp.json` | Registers the `specsmith-governance` MCP server for this repo. | -| `.roo/specsmith-rules.md` | Mandatory rules for governed Zoo/Roo sessions. | -| `.roo/modes.local.json` | Reference mode-to-model mapping for this project. | -| `.roo/global-settings.copy-to-zoo-code.json` | Copyable global provider/model settings for Zoo Code. | +```bash +pipx upgrade specsmith +cd /path/to/project +specsmith zoo-code setup --project-dir . +specsmith zoo-code doctor --project-dir . +``` + +The setup command installs reusable Specsmith rules, slash commands, and skills under `~/.roo`. It also merges the `specsmith-governance` MCP server into the project `.roo/mcp.json` without removing unrelated servers. + +Use `--global-roo PATH` or `ROO_GLOBAL_DIR` to override the global directory. `--scope global` and `--scope project` limit the operation. `--dry-run` previews changes, and `--preserve-existing` refuses to replace unmanaged files at reserved Specsmith paths. -Zoo Code global settings are not automatically loaded from a repository. Keep copyable global settings in the project as a reference, then copy them into Zoo Code global/user settings manually. +## Ownership boundary + +Specsmith owns reusable global governance assets and their setup, migration, doctor, and uninstall lifecycle. Repositories should contain only project-specific rules, commands, skills, custom modes, and provider/model settings. + +Existing generic Specsmith rules duplicated inside a project are removed only when they match a recognized legacy asset or carry a Specsmith managed marker. Customized project files are preserved and reported. ## MCP setup -The repo-local MCP config should look like this: +The generated repo-local MCP entry is: ```json { @@ -49,7 +57,7 @@ The MCP server exposes governance tools such as: Every Zoo/Roo session should follow this sequence: ```text -read AGENTS.md + .roo/specsmith-rules.md +read AGENTS.md and applicable global/project rules call governance_checkpoint call governance_phase call governance_req_list @@ -60,7 +68,7 @@ run verification seal meaningful decisions ``` -Do not edit files unless `governance_preflight` returns `accepted`. +Do not edit files unless `governance_preflight` returns `accepted` or the request is classified as a permitted environment-only operation. ## Local model routing @@ -82,18 +90,18 @@ Recommended role mapping: The model that writes a patch must not be the only model that approves it. -## Operator setup checklist +## Operator checklist -1. Start the local model router if using local models. -2. Copy `.roo/global-settings.copy-to-zoo-code.json` into Zoo Code global settings, adapting field names to the current extension version. -3. Ensure Zoo/Roo sees `.roo/mcp.json` for the active workspace. -4. Start a session by asking the agent to read `.roo/specsmith-rules.md` and call `governance_checkpoint`. -5. Run governed tasks using the role split: Architect -> Code -> Debug/Tool -> Review. +1. Start the local model router when using local models. +2. Run `specsmith zoo-code setup --project-dir .`. +3. Run `specsmith zoo-code doctor --project-dir .`. +4. Reload the editor so Zoo Code discovers the final rules, commands, skills, and MCP configuration. +5. Start governed work with `/specsmith-intake` or the appropriate project-specific command. -## Safe defaults +## Remove managed integration state + +```bash +specsmith zoo-code uninstall --project-dir . +``` -- Architect mode plans and reads; it should not directly edit production code. -- Code mode implements accepted preflight tasks. -- Debug/Tool mode runs safe commands and parses logs. -- Review mode critiques diffs against requirements, tests, and prior decisions. -- Specsmith blocks or escalates work when confidence, phase, or scope checks fail. +Uninstall removes only marker-owned global assets and removes the MCP entry only when it still matches the Specsmith-managed value. Unmanaged and customized files are preserved. diff --git a/pyproject.toml b/pyproject.toml index 3bbdfcff..e6141257 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta" [project] name = "specsmith" -version = "0.22.3" -description = "AEE governance toolkit for AI-assisted development — session preflight gates, multi-agent dispatch, requirements↔test traceability, ESDB persistence, MCP server, and skills for Warp, Cursor, Claude Code, Copilot, Windsurf, and Aider." +version = "0.22.4" +description = "AEE governance toolkit for AI-assisted development — session preflight gates, multi-agent dispatch, requirements↔test traceability, ESDB persistence, MCP server, and skills for Warp, Cursor, Claude Code, Copilot, Windsurf, Aider, and Zoo Code." readme = "README.md" license = "MIT" requires-python = ">=3.10" @@ -32,20 +32,16 @@ classifiers = [ "Topic :: Software Development :: Code Generators", "Topic :: Software Development :: Build Tools", "Topic :: Scientific/Engineering :: Artificial Intelligence", - "Topic :: Scientific/Engineering :: Information Analysis", + "Topic :: Software Development :: Code Generators", ] dependencies = [ "click>=8.1", - # `specsmith esdb enable` verifies Ed25519 license signatures in every install. "cryptography>=42.0", "jinja2>=3.1", "pyyaml>=6.0", "pydantic>=2.0", "rich>=13.0", - # chronomemory is now an OPTIONAL commercial dep (specsmith[esdb]). - # The free SQLite backend is built-in and requires no extra install. - # To unlock ChronoStore: pip install specsmith[esdb] + specsmith esdb enable ] [project.optional-dependencies] @@ -57,36 +53,22 @@ dev = [ "pre-commit>=3.0", "types-pyyaml>=6.0", ] -# ESDB commercial backend — requires a valid license from licensing@layer1labs.ai -# Install: pip install specsmith[esdb] -# Activate: specsmith esdb enable --key-file /path/to/your.esdb.key -# Terms: https://github.com/layer1labs/specsmith/blob/develop/COMMERCIAL-LICENSE.md esdb = [ - "chronomemory>=0.2.7", # proprietary commercial — ChronoMemory terms only (not SQLite free tier) + "chronomemory>=0.2.7", ] docs = [ "mkdocs>=1.6", "mkdocs-material>=9.5", ] -# LLM provider extras for specsmith run (agentic client) anthropic = ["anthropic>=0.56"] openai = ["openai>=1.0"] -# gemini: supports both the new google-genai SDK (GA May 2025, recommended) -# and the legacy google-generativeai SDK (deprecated Nov 2025, fallback). -# The provider auto-detects which is installed. gemini = ["google-genai>=1.0"] -mistral = ["openai>=1.0"] # Mistral uses the openai SDK pointed at api.mistral.ai +mistral = ["openai>=1.0"] gui = ["PySide6>=6.6"] -# AG2 agent shell (Planner/Builder/Verifier over Ollama) ag2 = ["ag2[ollama]"] -# Optional semantic backend for `specsmith history search --semantic` (REQ-135). -# Falls back gracefully to keyword matching if these are not installed. history-semantic = ["sentence-transformers>=2.2", "numpy>=1.24"] -# Optional whisper-cpp wrapper for the voice agent input (REQ-141). voice = ["whisper-cpp-python>=0.2"] -# Install all optional LLM providers agent = ["anthropic>=0.56", "openai>=1.0"] -# Convenience bundle: everything EXCEPT esdb (commercial license required separately) all = [ "anthropic>=0.56", "openai>=1.0", @@ -95,7 +77,6 @@ all = [ "ruff>=0.4", "mypy>=1.10", ] -# Compatibility extra: cryptography is now part of every supported installation. license-verify = [] [project.scripts] @@ -123,29 +104,19 @@ line-length = 100 select = ["E", "F", "W", "I", "UP", "B", "SIM"] [tool.ruff.lint.per-file-ignores] -# Long rule text and install command strings in documentation modules "src/specsmith/toolrules.py" = ["E501"] "src/specsmith/tool_installer.py" = ["E501"] -# One-shot rebuild scripts contain literal REQ description strings that are -# intentionally long; wrapping them would damage readability of the source -# of truth they reconstruct. +"src/specsmith/commands/zoo_code_assets.py" = ["E501", "UP035"] "scripts/rebuild_requirements_json.py" = ["E501"] "scripts/rebuild_requirements_md.py" = ["E501"] -# Compliance package: regulation description strings and HTML templates are -# intentionally long; wrapping would damage readability. "src/specsmith/compliance/_compat.py" = ["E501"] "src/specsmith/compliance/regulations.py" = ["E501"] "src/specsmith/compliance/reporter.py" = ["E501"] "src/specsmith/compliance/evidence.py" = ["E501"] -# CI manager: error message strings are intentionally descriptive "src/specsmith/ci_manager.py" = ["E501"] -# Context orchestrator: tier descriptions are intentionally descriptive "src/specsmith/context_orchestrator.py" = ["E501"] -# Skills module: skill body strings contain markdown content with long lines "src/specsmith/skills/*.py" = ["E501"] -# MCP server module: tool schema description strings are long but cannot be split "src/specsmith/mcp_server.py" = ["E501"] -# Migration files: rule description strings and template content "src/specsmith/migrations/m001_governance_yaml.py" = ["E501"] "src/specsmith/migrations/m004_ledger_esdb.py" = ["E501"] @@ -154,17 +125,13 @@ python_version = "3.10" strict = true warn_return_any = true warn_unused_configs = true -# GUI package requires PySide6, an optional extra not available in the mypy -# environment. Exclude it from type-checking to avoid false positives. exclude = ["src/specsmith/gui"] -# Optional extras — installed by the user, not in the dev environment. -# Ignore missing stubs rather than failing type-check for absent packages. [[tool.mypy.overrides]] module = [ "anthropic", "anthropic.*", - "autogen", # AG2 agent shell — optional extra + "autogen", "autogen.*", "openai", "openai.*", @@ -172,31 +139,24 @@ module = [ "google.*", "yaml", "yaml.*", - "keyring", # optional OS credential store; stubs not published + "keyring", "keyring.*", - "numpy", # optional [history-semantic] extra (REQ-135) + "numpy", "numpy.*", - "sentence_transformers", # optional [history-semantic] extra (REQ-135) + "sentence_transformers", "sentence_transformers.*", - "whisper_cpp_python", # optional [voice] extra (REQ-141) - "whisper_cpp_python.*", - "chronomemory", # git dep — no published stubs yet + "whisper_cpp_python", + "chronomemory", "chronomemory.*", - "tomllib", # stdlib in 3.11+; not available when checking against python_version=3.10 - "tomli", # optional 3.10 backport; not installed in the dev environment + "tomllib", + "tomli", ] ignore_missing_imports = true -# epistemic library has pre-existing strict-mypy issues unrelated to specsmith changes [[tool.mypy.overrides]] module = ["epistemic", "epistemic.*"] ignore_errors = true -# Modules that use dynamic typing patterns incompatible with strict mypy. -# These are correct at runtime; the Any-heavy urllib, json.loads, etc. patterns -# are the source of most errors here. Exclude from strict type checking. -# REQ-111: shrunk this list as part of the pre-1.0 cleanup; broker, safety, -# console_utils, and indexer have been graduated to strict mypy. [[tool.mypy.overrides]] module = [ "specsmith.ollama_cmds", @@ -218,12 +178,9 @@ module = [ "specsmith.toolrules", "specsmith.tool_installer", "specsmith.commands.intelligence", - # glossa-lab AI pattern ports — urllib/json-heavy, Any patterns expected "specsmith.agent.hf_leaderboard", "specsmith.agent.llm_client", - # pre-existing openai SDK overload mismatch, not introduced by our changes "specsmith.agent.chat_runner", - # New modules — dynamic/Any-heavy patterns; will be typed in 1.x cleanup "specsmith.compliance.checker", "specsmith.compliance.evidence", "specsmith.compliance.reporter", @@ -238,12 +195,7 @@ module = [ "specsmith.ci_manager", "specsmith.context_orchestrator", "specsmith.session_store", - # MCP server: lazy in-function imports and dynamic Any patterns "specsmith.mcp_server", - # ESDB package: conditional chronomemory stubs in except ImportError branch - # produce type[_Stub] vs type[ChronoRecord] assignment errors that are - # correct at runtime but cannot be expressed statically without TYPE_CHECKING. - # specsmith.esdb.* covers bridge.py and _license.py sub-modules too. "specsmith.esdb", "specsmith.esdb.*", ] diff --git a/src/specsmith/__init__.py b/src/specsmith/__init__.py index 6e7023a2..383d19fd 100644 --- a/src/specsmith/__init__.py +++ b/src/specsmith/__init__.py @@ -8,4 +8,4 @@ try: __version__: str = _pkg_version("specsmith") except PackageNotFoundError: # running from source without install - __version__ = "0.22.3" # fallback: keep in sync with pyproject.toml + __version__ = "0.22.4" # fallback: keep in sync with pyproject.toml diff --git a/src/specsmith/_config_schema.py b/src/specsmith/_config_schema.py new file mode 100644 index 00000000..f963927d --- /dev/null +++ b/src/specsmith/_config_schema.py @@ -0,0 +1,700 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2026 Layer1Labs Silicon, Inc. All rights reserved. +"""Project configuration schema for specsmith.""" + +from __future__ import annotations + +from enum import Enum +from typing import Any + +from pydantic import BaseModel, Field + + +class ProjectType(str, Enum): + """Supported project types from the spec (Section 17).""" + + # Python + BACKEND_FRONTEND = "backend-frontend" + BACKEND_FRONTEND_TRAY = "backend-frontend-tray" + CLI_PYTHON = "cli-python" + LIBRARY_PYTHON = "library-python" + # Hardware / Embedded — vendor-specific FPGA types + EMBEDDED_HARDWARE = "embedded-hardware" + FPGA_RTL = "fpga-rtl" # Generic / open-source flow (Yosys) + FPGA_RTL_AMD = "fpga-rtl-amd" # AMD Adaptive Computing (formerly Xilinx) — Vivado + FPGA_RTL_INTEL = "fpga-rtl-intel" # Intel/Altera Quartus Prime + FPGA_RTL_LATTICE = "fpga-rtl-lattice" # Lattice Diamond / Radiant + MIXED_FPGA_EMBEDDED = "mixed-fpga-embedded" # FPGA + embedded C/C++ driver + MIXED_FPGA_FIRMWARE = "mixed-fpga-firmware" # FPGA + Python/C verification + YOCTO_BSP = "yocto-bsp" + PCB_HARDWARE = "pcb-hardware" + # Web / JS / TS + WEB_FRONTEND = "web-frontend" + FULLSTACK_JS = "fullstack-js" + # Systems languages + CLI_RUST = "cli-rust" + CLI_GO = "cli-go" + CLI_C = "cli-c" + LIBRARY_RUST = "library-rust" + LIBRARY_C = "library-c" + # Other platforms + DOTNET_APP = "dotnet-app" + MOBILE_APP = "mobile-app" + # Infrastructure / Data + DEVOPS_IAC = "devops-iac" + DATA_ML = "data-ml" + MICROSERVICES = "microservices" + # Document / Knowledge + SPEC_DOCUMENT = "spec-document" + USER_MANUAL = "user-manual" + RESEARCH_PAPER = "research-paper" + # Business / Legal + BUSINESS_PLAN = "business-plan" + PATENT_APPLICATION = "patent-application" + LEGAL_COMPLIANCE = "legal-compliance" + # Project management + REQUIREMENTS_MGMT = "requirements-mgmt" + API_SPECIFICATION = "api-specification" + # More software + MONOREPO = "monorepo" + BROWSER_EXTENSION = "browser-extension" + # Applied Epistemic Engineering + EPISTEMIC_PIPELINE = "epistemic-pipeline" + KNOWLEDGE_ENGINEERING = "knowledge-engineering" + AEE_RESEARCH = "aee-research" + # New project types + EMBEDDED_PYTHON_HMI = "embedded-python-hmi" # #109: hardware-interfacing kiosk/HMI + RESEARCH_PYTHON = "research-python" # #153: experiment/research packages (no CLI) + SAFETY_CRITICAL = "safety-critical" # #129: IEC 60204-1/62061/61508 safety-critical + # IP / Patent + PATENT_PROSECUTION = "patent-prosecution" # #177: IP prosecution with USPTO MCP lifecycle + # Modern web frameworks + NEXTJS_APP = "nextjs-app" # Next.js / React full-stack app + NUXT_APP = "nuxt-app" # Nuxt.js / Vue full-stack app + SVELTEKIT_APP = "sveltekit-app" # SvelteKit app + REMIX_APP = "remix-app" # Remix full-stack React app + ASTRO_SITE = "astro-site" # Astro static/SSR site + # AI / LLM / Agents + LLM_APP = "llm-app" # LLM-powered app (LangChain / LlamaIndex / custom SDK) + AGENT_ORCHESTRATION = "agent-orchestration" # Multi-agent system (AutoGen/CrewAI/LangGraph) + MCP_SERVER = "mcp-server" # Model Context Protocol server + RAG_PIPELINE = "rag-pipeline" # RAG + embedding pipeline + MLOPS_PLATFORM = "mlops-platform" # MLOps platform (MLflow / BentoML / Ray Serve) + # JVM + JAVA_SPRING = "java-spring" # Spring Boot application + JAVA_LIBRARY = "java-library" # Java library / SDK + # Cloud / Infrastructure + SERVERLESS = "serverless" # FaaS (Lambda / GCP Functions / Cloudflare Workers) + KUBERNETES_OPERATOR = "kubernetes-operator" # K8s controller / operator + STREAMING_PIPELINE = "streaming-pipeline" # Kafka / Flink / Beam / Spark Streaming + DATA_WAREHOUSE = "data-warehouse" # dbt / Snowflake / BigQuery / Redshift + # Game development + GAME_UNITY = "game-unity" # Unity game project + GAME_GODOT = "game-godot" # Godot game project + # Web3 / blockchain + SMART_CONTRACT = "smart-contract" # Solidity / EVM smart contracts + # Desktop + DESKTOP_ELECTRON = "desktop-electron" # Electron desktop app + DESKTOP_TAURI = "desktop-tauri" # Tauri desktop app (Rust + WebView) + # Brief lang — declarative contract-enforced logic language (github.com/Randozart/brief-lang) + # Version anchor: v0.14.0 @ commit 6a43c4aebcc5c6c774dbc2908445fb19486e8043 (2026-06-14) + BRIEF_LANG = "brief-lang" # .bv/.sbv/.rbv/.ebv project using brief-compiler + + +class Platform(str, Enum): + """Target platforms (kept for backward compatibility; ProjectConfig now uses list[str]).""" + + WINDOWS = "windows" + LINUX = "linux" + MACOS = "macos" + + +class ProjectConfig(BaseModel): + """Configuration for a specsmith-generated project scaffold. + + This model validates the scaffold.yml input file and interactive prompts. + """ + + # Config inheritance + extends: str = Field( + default="", + description="Path or URL to parent scaffold.yml to inherit defaults from", + ) + + name: str = Field(description="Project name (used for directory and package name)") + # str (not ProjectType enum) so scaffold.yml files with custom or vendor-specific + # types (e.g. 'fpga-rtl-xilinx') don't crash pydantic validation. + # Use ProjectType enum values as constants when checking the type. + type: str = Field( + default=ProjectType.CLI_PYTHON.value, + description="Project type. Should match a ProjectType value.", + ) + # str instead of Platform enum so FPGA/embedded/cloud targets (e.g. 'embedded', + # 'amd-fpga', 'wasm') don't crash pydantic validation. The Platform enum is kept + # for backward-compatible imports; use its .value strings as defaults. + platforms: list[str] = Field( + default=["windows", "linux", "macos"], + description=( + "Target build platforms. Standard: windows, linux, macos. " + "Domain-specific: embedded, cloud, wasm, amd-fpga, intel-fpga, lattice-fpga, etc." + ), + ) + language: str = Field(default="python", description="Primary language/runtime") + spec_version: str = Field(default="0.22.3", description="Spec version to scaffold from") + description: str = Field(default="", description="Short project description") + + # Options + services: bool = Field( + default=False, + description="Include services.md for daemon/service projects", + ) + shell_wrappers: bool = Field(default=False, description="Include shell wrapper scripts") + exec_shims: bool = Field(default=True, description="Include exec.cmd/exec.sh timeout shims") + git_init: bool = Field(default=True, description="Initialize git repository") + + # VCS platform + vcs_platform: str = Field( + default="github", + description="VCS platform (github, gitlab, bitbucket)", + ) + + # Branching strategy + branching_strategy: str = Field( + default="single-branch", + description="Branching strategy (single-branch, gitflow, trunk-based, github-flow)", + ) + default_branch: str = Field( + default="main", + description="Default/production branch name", + ) + develop_branch: str = Field( + default="develop", + description="Development integration branch (gitflow only)", + ) + require_pr_reviews: bool = Field( + default=True, + description="Require pull request reviews before merge", + ) + required_approvals: int = Field( + default=1, + description="Number of required PR approvals", + ) + require_ci_pass: bool = Field( + default=True, + description="Require CI checks to pass before merge", + ) + allow_force_push: bool = Field( + default=False, + description="Allow force push to protected branches", + ) + use_remote_rules: bool = Field( + default=False, + description="Accept branch protection rules from remote if already configured", + ) + + # Verification tools (auto-populated from type+language, overridable) + verification_tools: dict[str, str] = Field( + default_factory=dict, + description="Tool overrides by category: lint, typecheck, test, security, build, format", + ) + + # Import detection (populated by specsmith import) + detected_build_system: str = Field(default="", description="Build system detected by import") + detected_test_framework: str = Field( + default="", + description="Test framework detected by import", + ) + + # License + license: str = Field( + default="MIT", + description="SPDX license identifier (MIT, Apache-2.0, GPL-3.0-only, etc.)", + ) + + # Community files + community_files: list[str] = Field( + default=["contributing", "license", "security", "coc", "pr-template", "issue-templates"], + description="Community/compliance files to generate", + ) + + # Multi-discipline support + auxiliary_disciplines: list[str] = Field( + default=[], + description=( + "Additional project disciplines beyond the primary type. " + "e.g. ['embedded-c', 'cli-python'] for an FPGA project with C drivers " + "and Python verification. Each discipline generates extra CI jobs and " + "tool registry entries." + ), + ) + + # Explicit type override — suppresses the type-mismatch audit check. + # Set this when your project uses a custom or research-specific type string + # that cannot be auto-detected (e.g. 'research-mathematics'). When + # type_override matches type, check_type_mismatch is suppressed. + type_override: str = Field( + default="", + description=( + "Explicit type override. When set to the same value as `type`, " + "the type-mismatch audit check is suppressed regardless of what " + "auto-detection infers from the project files." + ), + ) + # Fallback type — used when this project type is not yet supported + # by the installed specsmith version. specsmith silently falls back to + # this type for scaffolding purposes while still recording the intended type. + fallback_type: str = Field( + default="", + description=( + "Fallback project type for scaffold generation when `type` is not yet " + "supported by the installed specsmith version (e.g. 'spec-document' as " + "fallback for 'patent-prosecution')." + ), + ) + + # IP prosecution fields (used when type == 'patent-prosecution') + provisional_app_number: str = Field( + default="", + description="USPTO provisional application number (e.g. '63/980,251')", + ) + provisional_filed_date: str = Field( + default="", + description="Date the provisional was filed (YYYY-MM-DD)", + ) + non_provisional_deadline: str = Field( + default="", + description="12-month non-provisional conversion deadline (YYYY-MM-DD)", + ) + entity_status: str = Field(default="", description="USPTO entity status: small, micro, large") + assignee: str = Field(default="", description="Patent assignee / rights holder") + counsel: str = Field(default="", description="Patent counsel firm name") + inventors: list[dict[str, str]] = Field( + default_factory=list, + description="List of inventors with name and role keys", + ) + ip_families: list[dict[str, Any]] = Field( + default_factory=list, + description=( + "IP patent families. Each entry: {id, name, phase, provisional, themes, " + "anchor_spec, ...}." + ), + ) + claim_themes: list[dict[str, Any]] = Field( + default_factory=list, + description=( + "Claim themes for the primary IP family. Each entry: {id, name, description, " + "risk, primary_comparator, last_par_run}." + ), + ) + specs_dir: str = Field( + default="docs/ip/specs", + description="Normative specification directory for IP repos", + ) + prosecution_dir: str = Field( + default="docs/ip/prosecution", + description="Prior-art protocol and prosecution planning directory", + ) + strategy_dir: str = Field( + default="docs/ip/strategy", + description="IP strategy documents directory", + ) + filings_dir: str = Field( + default="docs/ip/filings", + description="Immutable filed artifacts directory", + ) + + # FPGA-specific + fpga_vendor: str = Field( + default="", + description="FPGA vendor/toolchain: amd, intel, lattice, gowin (blank = generic/OSS)", + ) + + # Agent integrations + integrations: list[str] = Field( + default=["agents-md"], + description=( + "Agent integrations to generate (agents-md, agent-skill, claude-code, " + "cursor, copilot, gemini, windsurf, aider)." + ), + ) + + # Agent execution profile + execution_profile: str = Field( + default="standard", + description=( + "Agent execution profile: safe (read-only), standard (default), " + "open (most commands), admin (no limits)." + ), + ) + custom_allowed_commands: list[str] = Field( + default_factory=list, + description="Extra allowed command prefixes merged with the active execution profile.", + ) + custom_blocked_commands: list[str] = Field( + default_factory=list, + description="Extra blocked command prefixes merged with the active execution profile.", + ) + custom_blocked_tools: list[str] = Field( + default_factory=list, + description="Agent tool names to always block, regardless of the execution profile.", + ) + + # Audit thresholds (scaffold.yml-configurable) — #124, #145 + agents_md_line_threshold: int = Field( + default=0, + description=( + "Override AGENTS.md line-count audit threshold. 0 = use type-aware default " + "(200 for standard, 350 for embedded/fpga)." + ), + ) + ledger_line_threshold: int = Field( + default=0, + description=( + "Override LEDGER.md line-count audit threshold. 0 = use type-aware default " + "(500 for standard, 5000 for fpga-rtl)." + ), + ) + + # Configurable doc filenames — #148 + test_spec_file: str = Field( + default="docs/TESTS.md", + description=( + "Canonical test specification file path. Default: docs/TESTS.md. " + "Override with e.g. tests/custom_tests.md." + ), + ) + requirements_file: str = Field( + default="docs/REQUIREMENTS.md", + description="Canonical requirements file path.", + ) + + # Scanner exclusions — #146 + scan_exclude_dirs: list[str] = Field( + default_factory=list, + description=( + "Directories to exclude from specsmith scanning (detection, stats, test discovery). " + "Auto-populated from .gitignore on import. " + "e.g. ['external/', 'node_modules/', '.venv/']" + ), + ) + + # Derived artifacts — #126 + derived_artifacts: list[dict[str, Any]] = Field( + default_factory=list, + description=( + "Code-generated files that should not be hand-edited. Each entry: " + "{source, generator, outputs: [str], do_not_edit: bool}" + ), + ) + + # Hardware-gated tests — #159 + hardware_gated_test_attr: str = Field( + default="hardware_gated", + description=( + "Attribute name used in TESTS.md to mark tests as hardware-gated " + "(e.g. 'Hardware-gated: true'). Hardware-gated Pending tests are " + "not flagged as coverage drift." + ), + ) + + # Cross-repo dependencies — #161 + cross_repo_dependencies: list[dict[str, Any]] = Field( + default_factory=list, + description=( + "Cross-repo requirement traceability. Each entry: " + "{repo, url, role, req_prefixes: [str]}" + ), + ) + + # Secrets templates — #162 + secrets_templates: list[dict[str, Any]] = Field( + default_factory=list, + description=( + "Required-but-uncommitted secrets files. Each entry: " + "{path, description, required_keys: [str], never_commit: bool}" + ), + ) + + # Industrial artifacts (CANopen EDS/XDD) — #163 + industrial_artifacts: dict[str, Any] = Field( + default_factory=dict, + description=( + "Industrial protocol artifacts (e.g. CANopen EDS/XDD). " + "Structure: {canopen_eds: [{path, device, fw_repo?}]}" + ), + ) + + # Rate-limit per-bucket concurrency — #120 + rate_limit_concurrency_by_bucket: dict[str, int] = Field( + default_factory=dict, + description=( + "Per-bucket concurrency caps that override the model-level concurrency_cap. " + "Buckets: reasoning, conversational, longform, background." + ), + ) + + # Research / publication phases — #156 + publication_phases: dict[str, Any] = Field( + default_factory=dict, + description=( + "Custom publication gates for research projects. " + "Keys are gate IDs; values describe required_artifacts and checks." + ), + ) + + # Applied Epistemic Engineering configuration + enable_epistemic: bool = Field( + default=False, + description="Enable AEE epistemic governance layer (belief registry, failure modes, etc.)", + ) + epistemic_threshold: float = Field( + default=0.7, + description="Certainty threshold for epistemic-audit (0.0-1.0; default 0.7)", + ) + enable_trace_vault: bool = Field( + default=False, + description="Enable cryptographic trace vault (ESDB seal_record; REQ-420)", + ) + enable_development_mode: bool = Field( + default=False, + description="Enable development mode with additional logging and improvement tracking", + ) + + @property + def package_name(self) -> str: + """Python-safe package name derived from project name.""" + return self.name.replace("-", "_").replace(" ", "_").lower() + + @property + def platform_names(self) -> list[str]: + """Human-readable platform names.""" + + def _label(p: str) -> str: + if p.lower() == "macos": + return "macOS" + return p.replace("-", " ").title() + + return [_label(str(p)) for p in self.platforms] + + @property + def is_epistemic_type(self) -> bool: + """Whether this project type always enables the AEE epistemic layer.""" + return ( + self.type + in ( + ProjectType.EPISTEMIC_PIPELINE, + ProjectType.KNOWLEDGE_ENGINEERING, + ProjectType.AEE_RESEARCH, + ) + or self.enable_epistemic + ) + + @property + def needs_services(self) -> bool: + """Whether this project type typically needs services.md.""" + return ( + self.type + in ( + ProjectType.BACKEND_FRONTEND, + ProjectType.BACKEND_FRONTEND_TRAY, + ) + or self.services + ) + + @property + def needs_shell_wrappers(self) -> bool: + """Whether this project type requires shell wrappers.""" + return ( + self.type + in ( + ProjectType.EMBEDDED_HARDWARE, + ProjectType.FPGA_RTL, + ProjectType.YOCTO_BSP, + ProjectType.PCB_HARDWARE, + ) + or self.shell_wrappers + ) + + @property + def type_label(self) -> str: + """Human-readable project type label.""" + # self.type is now a str; fall back to the type key itself if unknown + return _TYPE_LABELS.get(self.type, self.type) + + @property + def section_ref(self) -> str: + """Spec section reference for this project type.""" + return _SECTION_REFS.get(self.type, "17") + + @property + def project_type_enum(self) -> ProjectType | None: + """Return the ProjectType enum member, or None if type is unknown/custom.""" + try: + return ProjectType(self.type) + except ValueError: + return None + + +# --------------------------------------------------------------------------- +# Legacy key normalisation helper +# --------------------------------------------------------------------------- + + +def _normalize_scaffold_raw(raw: dict[str, Any]) -> dict[str, Any]: + """Normalise legacy scaffold YAML keys to the current schema. + + Call this before ``ProjectConfig(**raw)`` whenever raw YAML is loaded from + disk so that projects created by older specsmith versions continue to parse + correctly after a key rename. + + Migrations handled: + - ``project`` → ``name`` (renamed in specsmith ≤ 0.9) + """ + if raw and "project" in raw and "name" not in raw: + raw = dict(raw) + raw["name"] = raw.pop("project") + return raw + + +# Keys are str (ProjectType enum values) for compatibility now that config.type is str +_TYPE_LABELS: dict[str, str] = { + ProjectType.BACKEND_FRONTEND: "Python backend + web frontend", + ProjectType.BACKEND_FRONTEND_TRAY: "Python backend + web frontend + tray", + ProjectType.CLI_PYTHON: "CLI tool (Python)", + ProjectType.LIBRARY_PYTHON: "Library / SDK (Python)", + ProjectType.EMBEDDED_HARDWARE: "Embedded / hardware (C/C++)", + ProjectType.FPGA_RTL: "FPGA / RTL (generic / OSS flow)", + ProjectType.FPGA_RTL_AMD: "FPGA / RTL — AMD Adaptive Computing (Vivado)", + ProjectType.FPGA_RTL_INTEL: "FPGA / RTL — Intel/Altera (Quartus)", + ProjectType.FPGA_RTL_LATTICE: "FPGA / RTL — Lattice (Diamond / Radiant)", + ProjectType.MIXED_FPGA_EMBEDDED: "Mixed: FPGA + Embedded C/C++ drivers", + ProjectType.MIXED_FPGA_FIRMWARE: "Mixed: FPGA + Python/C verification", + ProjectType.YOCTO_BSP: "Yocto / embedded Linux BSP", + ProjectType.PCB_HARDWARE: "PCB / hardware design (KiCad etc.)", + ProjectType.WEB_FRONTEND: "Web frontend (SPA)", + ProjectType.FULLSTACK_JS: "Fullstack JS/TS", + ProjectType.CLI_RUST: "CLI tool (Rust)", + ProjectType.CLI_GO: "CLI tool (Go)", + ProjectType.CLI_C: "CLI tool (C/C++)", + ProjectType.LIBRARY_RUST: "Library / crate (Rust)", + ProjectType.LIBRARY_C: "Library (C/C++)", + ProjectType.DOTNET_APP: ".NET / C# application", + ProjectType.MOBILE_APP: "Mobile app", + ProjectType.DEVOPS_IAC: "DevOps / IaC", + ProjectType.DATA_ML: "Data / ML pipeline", + ProjectType.MICROSERVICES: "Microservices", + # Document / Knowledge + ProjectType.SPEC_DOCUMENT: "Technical specification", + ProjectType.USER_MANUAL: "User manual / documentation", + ProjectType.RESEARCH_PAPER: "Research paper / white paper", + # Business / Legal + ProjectType.BUSINESS_PLAN: "Business plan / proposal", + ProjectType.PATENT_APPLICATION: "Patent application", + ProjectType.LEGAL_COMPLIANCE: "Legal / compliance", + # Project management + ProjectType.REQUIREMENTS_MGMT: "Requirements management", + ProjectType.API_SPECIFICATION: "API specification", + # More software + ProjectType.MONOREPO: "Monorepo (multi-package)", + ProjectType.BROWSER_EXTENSION: "Browser extension", + # Applied Epistemic Engineering + ProjectType.EPISTEMIC_PIPELINE: "Epistemic pipeline (AEE + ARE 8-phase)", + ProjectType.KNOWLEDGE_ENGINEERING: "Knowledge engineering / expert system", + ProjectType.AEE_RESEARCH: "AEE research project", + # New types + ProjectType.EMBEDDED_PYTHON_HMI: "Embedded Python HMI / kiosk (hardware-interfacing)", + ProjectType.RESEARCH_PYTHON: "Research Python (experiments, no CLI distribution)", + ProjectType.SAFETY_CRITICAL: "Safety-critical embedded (IEC 60204-1/62061/61508)", + # IP / Patent + ProjectType.PATENT_PROSECUTION: "Patent prosecution repository (USPTO IP lifecycle)", + # Modern web frameworks + ProjectType.NEXTJS_APP: "Next.js application (React + SSR/SSG)", + ProjectType.NUXT_APP: "Nuxt.js application (Vue + SSR/SSG)", + ProjectType.SVELTEKIT_APP: "SvelteKit application", + ProjectType.REMIX_APP: "Remix application (React + full-stack)", + ProjectType.ASTRO_SITE: "Astro site (static / SSR)", + # AI / LLM / Agents + ProjectType.LLM_APP: "LLM-powered application (LangChain / LlamaIndex / custom)", + ProjectType.AGENT_ORCHESTRATION: "Multi-agent orchestration (AutoGen / CrewAI / LangGraph)", + ProjectType.MCP_SERVER: "MCP server (Model Context Protocol)", + ProjectType.RAG_PIPELINE: "RAG / embedding pipeline", + ProjectType.MLOPS_PLATFORM: "MLOps platform (MLflow / BentoML / Ray Serve)", + # JVM + ProjectType.JAVA_SPRING: "Java Spring Boot application", + ProjectType.JAVA_LIBRARY: "Java library / SDK", + # Cloud / Infrastructure + ProjectType.SERVERLESS: "Serverless / FaaS (Lambda / GCP Functions / Cloudflare Workers)", + ProjectType.KUBERNETES_OPERATOR: "Kubernetes operator / controller", + ProjectType.STREAMING_PIPELINE: "Streaming data pipeline (Kafka / Flink / Beam)", + ProjectType.DATA_WAREHOUSE: "Data warehouse (dbt / Snowflake / BigQuery)", + # Game development + ProjectType.GAME_UNITY: "Game (Unity)", + ProjectType.GAME_GODOT: "Game (Godot)", + # Web3 + ProjectType.SMART_CONTRACT: "Smart contract (Solidity / EVM)", + # Desktop + ProjectType.DESKTOP_ELECTRON: "Desktop application (Electron)", + ProjectType.DESKTOP_TAURI: "Desktop application (Tauri — Rust + WebView)", + # Brief lang + ProjectType.BRIEF_LANG: "Brief lang — declarative contract-enforced logic language (v0.14.0)", +} + +_SECTION_REFS: dict[str, str] = { + ProjectType.BACKEND_FRONTEND: "17.1", + ProjectType.BACKEND_FRONTEND_TRAY: "17.2", + ProjectType.CLI_PYTHON: "17.3", + ProjectType.LIBRARY_PYTHON: "17.4", + ProjectType.EMBEDDED_HARDWARE: "17.5", + ProjectType.FPGA_RTL: "17.6", + ProjectType.YOCTO_BSP: "17.7", + ProjectType.PCB_HARDWARE: "17.8", + ProjectType.WEB_FRONTEND: "17.9", + ProjectType.FULLSTACK_JS: "17.10", + ProjectType.CLI_RUST: "17.11", + ProjectType.CLI_GO: "17.12", + ProjectType.CLI_C: "17.13", + ProjectType.LIBRARY_RUST: "17.14", + ProjectType.LIBRARY_C: "17.15", + ProjectType.DOTNET_APP: "17.16", + ProjectType.MOBILE_APP: "17.17", + ProjectType.DEVOPS_IAC: "17.18", + ProjectType.DATA_ML: "17.19", + ProjectType.MICROSERVICES: "17.20", + ProjectType.SPEC_DOCUMENT: "17.21", + ProjectType.USER_MANUAL: "17.22", + ProjectType.RESEARCH_PAPER: "17.23", + ProjectType.BUSINESS_PLAN: "17.24", + ProjectType.PATENT_APPLICATION: "17.25", + ProjectType.LEGAL_COMPLIANCE: "17.26", + ProjectType.REQUIREMENTS_MGMT: "17.27", + ProjectType.API_SPECIFICATION: "17.28", + ProjectType.MONOREPO: "17.29", + ProjectType.BROWSER_EXTENSION: "17.30", + ProjectType.EPISTEMIC_PIPELINE: "17.31", + ProjectType.KNOWLEDGE_ENGINEERING: "17.32", + ProjectType.AEE_RESEARCH: "17.33", + # AI / LLM / Agents + ProjectType.LLM_APP: "17.34", + ProjectType.AGENT_ORCHESTRATION: "17.35", + ProjectType.MCP_SERVER: "17.36", + ProjectType.RAG_PIPELINE: "17.37", + ProjectType.MLOPS_PLATFORM: "17.38", + # JVM + ProjectType.JAVA_SPRING: "17.39", + ProjectType.JAVA_LIBRARY: "17.40", + # Cloud / Infrastructure + ProjectType.SERVERLESS: "17.41", + ProjectType.KUBERNETES_OPERATOR: "17.42", + ProjectType.STREAMING_PIPELINE: "17.43", + ProjectType.DATA_WAREHOUSE: "17.44", + # Game development + ProjectType.GAME_UNITY: "17.45", + ProjectType.GAME_GODOT: "17.46", + # Web3 + ProjectType.SMART_CONTRACT: "17.47", + # Desktop + ProjectType.DESKTOP_ELECTRON: "17.48", + ProjectType.DESKTOP_TAURI: "17.49", + # Brief lang + ProjectType.BRIEF_LANG: "17.50", +} diff --git a/src/specsmith/commands/__init__.py b/src/specsmith/commands/__init__.py index c3576c37..1a881e1d 100644 --- a/src/specsmith/commands/__init__.py +++ b/src/specsmith/commands/__init__.py @@ -1,3 +1,7 @@ # SPDX-License-Identifier: MIT # Copyright (c) 2026 Layer1Labs Silicon, Inc. All rights reserved. """specsmith command implementations.""" + +from specsmith.commands.zoo_code_assets import register_zoo_code_asset_commands + +register_zoo_code_asset_commands() diff --git a/src/specsmith/commands/zoo_code_assets.py b/src/specsmith/commands/zoo_code_assets.py new file mode 100644 index 00000000..2272c5c9 --- /dev/null +++ b/src/specsmith/commands/zoo_code_assets.py @@ -0,0 +1,450 @@ +# SPDX-License-Identifier: MIT +"""Portable Zoo Code integration lifecycle for Specsmith.""" + +from __future__ import annotations + +import json +import os +import shutil +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Callable, Literal + +import click + +MARKER = "specsmith-zoo-assets:v2" +OLD_MARKER = "specsmith-zoo-assets:v1" +MANIFEST = ".specsmith-zoo-code-assets.json" +PROJECT_MANIFEST = ".roo/.specsmith-zoo-code.json" +Scope = Literal["both", "global", "project"] + +RULES = { + "rules/00-specsmith-source-of-truth.md": ( + "Specsmith source of truth", + "Treat repository files, requirements, evidence, checks, traces, and ledger " + "artifacts as authoritative. Verify before assuming.", + ), + "rules/10-specsmith-governance.md": ( + "Specsmith governance", + "Anchor non-trivial work to requirements, constraints, acceptance criteria, " + "evidence, and verification. Run preflight before implementation.", + ), + "rules/20-context-continuity.md": ( + "Context continuity", + "Preserve work items, decisions, constraints, evidence, risks, changed files, " + "and the next verification step before condensation or handoff.", + ), + "rules/30-security-and-secrets.md": ( + "Secrets and safety", + "Never expose, commit, or echo secrets. Gate destructive, privileged, and " + "externally visible actions.", + ), + "rules/40-context-efficiency.md": ( + "Context efficiency", + "Search before reading, batch related reads, avoid duplicate context, and " + "optimize for tokens per verified correct result.", + ), + "rules-architect/10-planning.md": ( + "Architect planning", + "Define scope, invariants, interfaces, alternatives, risks, acceptance criteria, " + "and verification before implementation.", + ), + "rules-ask/10-evidence-answers.md": ( + "Evidence-grounded answers", + "Answer from inspected evidence. Label inference and uncertainty.", + ), + "rules-code/10-implementation-verification.md": ( + "Governed implementation", + "Implement accepted scope only, preserve unrelated behavior, run focused " + "checks, and record evidence.", + ), + "rules-debug/10-debug-evidence.md": ( + "Evidence-driven debugging", + "Reproduce first, form falsifiable hypotheses, inspect focused evidence, and " + "verify the fix and regressions.", + ), + "rules-orchestrator/10-specsmith-boomerang.md": ( + "Specsmith orchestration", + "Delegate bounded tasks with explicit inputs and return contracts. Verify " + "returned work before accepting dependent actions.", + ), + "rules-reviewer/10-review-readiness.md": ( + "Review readiness", + "Review against requirements, tests, security constraints, and prior decisions. " + "The authoring model must not be the sole approver.", + ), +} + +COMMANDS = { + "commands/specsmith-intake.md": ("Start governed work", "orchestrator"), + "commands/specsmith-plan.md": ("Plan governed work", "architect"), + "commands/specsmith-implement.md": ("Implement governed work", "code"), + "commands/specsmith-debug.md": ("Debug with evidence", "debug"), + "commands/specsmith-review.md": ("Review governed work", "reviewer"), + "commands/specsmith-checkpoint.md": ("Create a durable checkpoint", "orchestrator"), +} + +SKILLS = { + "skills/specsmith-governed-work/SKILL.md": "Run requirement-bound work through governance.", + "skills/specsmith-evidence-debugging/SKILL.md": "Debug with evidence and hypotheses.", + "skills/specsmith-context-continuity/SKILL.md": "Preserve critical state across handoffs.", +} + + +def rule(title: str, body: str) -> str: + return f"\n# {title}\n\n{body}\n" + + +def command(title: str, mode: str) -> str: + body = ( + f"Execute **{title.lower()}** under Specsmith governance. Inspect authority, " + "bound scope, preserve evidence, run relevant verification, and report risks." + ) + return f"---\ndescription: {title}\nmode: {mode}\n---\n\n\n{body}\n" + + +def skill(path: str, description: str) -> str: + name = Path(path).parent.name + return ( + f"---\nname: {name}\ndescription: {description}\n---\n\n" + f"\n\n# {name}\n\n{description}\n" + ) + + +GLOBAL = {path: rule(*value) for path, value in RULES.items()} +GLOBAL.update({path: command(*value) for path, value in COMMANDS.items()}) +GLOBAL.update({path: skill(path, value) for path, value in SKILLS.items()}) +LEGACY_PROJECT = { + path: text.replace(f"\n", "", 1) + for path, text in GLOBAL.items() + if path.startswith("rules") +} +MCP: dict[str, Any] = { + "command": "specsmith", + "args": ["mcp", "serve", "--project-dir", "."], + "env": { + "SPECSMITH_ALLOW_NON_PIPX": "1", + "SPECSMITH_NO_AUTO_UPDATE": "1", + "SPECSMITH_PYPI_CHECKED": "1", + }, +} + + +@dataclass +class Result: + changed: list[str] = field(default_factory=list) + removed: list[str] = field(default_factory=list) + backups: list[str] = field(default_factory=list) + preserved: list[str] = field(default_factory=list) + errors: list[str] = field(default_factory=list) + + @property + def ok(self) -> bool: + return not self.errors + + def add(self, other: Result) -> None: + for name in ("changed", "removed", "backups", "preserved", "errors"): + getattr(self, name).extend(getattr(other, name)) + + +class ZooCodeAssets: + def __init__( + self, + project: Path, + global_roo: Path, + dry_run: bool = False, + preserve_existing: bool = False, + ) -> None: + self.project = project.expanduser().resolve() + self.global_roo = global_roo.expanduser().resolve() + self.dry_run = dry_run + self.preserve_existing = preserve_existing + + def _run( + self, + scope: Scope, + global_action: Callable[[], Result], + project_action: Callable[[], Result], + ) -> Result: + result = Result() + if scope in {"both", "global"}: + result.add(global_action()) + if scope in {"both", "project"}: + result.add(project_action()) + return result + + def setup(self, scope: Scope = "both") -> Result: + return self._run(scope, self._setup_global, self._setup_project) + + def doctor(self, scope: Scope = "both") -> Result: + return self._run(scope, self._doctor_global, self._doctor_project) + + def uninstall(self, scope: Scope = "both") -> Result: + return self._run(scope, self._uninstall_global, self._uninstall_project) + + def _managed(self, path: Path) -> bool: + if not path.is_file(): + return False + text = path.read_text(encoding="utf-8") + return MARKER in text or OLD_MARKER in text + + def _write(self, path: Path, text: str) -> None: + if not self.dry_run: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + + def _write_json(self, path: Path, value: dict[str, Any]) -> None: + self._write(path, json.dumps(value, indent=2, sort_keys=True) + "\n") + + def _read_json(self, path: Path, result: Result) -> dict[str, Any] | None: + if not path.exists(): + return {} + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError) as exc: + result.errors.append(f"invalid JSON in {path}: {exc}") + return None + if not isinstance(value, dict): + result.errors.append(f"JSON root must be an object: {path}") + return None + return value + + def _manifest(self) -> dict[str, Any]: + return self._read_json(self.global_roo / MANIFEST, Result()) or {} + + def _remove(self, path: Path) -> None: + if not self.dry_run: + path.unlink(missing_ok=True) + + def _backup(self, path: Path) -> Path: + suffix = ".before-specsmith-zoo-code" + backup = path.with_name(path.name + suffix) + index = 1 + while backup.exists(): + backup = path.with_name(path.name + f"{suffix}.{index}") + index += 1 + if not self.dry_run: + shutil.copy2(path, backup) + return backup + + def _setup_global(self) -> Result: + result = Result() + old = set(self._manifest().get("files", [])) + for relative in sorted(old - set(GLOBAL)): + path = self.global_roo / relative + if self._managed(path): + self._remove(path) + result.removed.append(f"global:{relative}") + elif path.exists(): + result.preserved.append(f"global:{relative}") + for relative, expected in GLOBAL.items(): + path = self.global_roo / relative + current = path.read_text(encoding="utf-8") if path.exists() else None + if current is not None and current != expected and not self._managed(path): + if self.preserve_existing: + result.preserved.append(f"global:{relative}") + result.errors.append(f"reserved path is unmanaged: {path}") + continue + result.backups.append(str(self._backup(path))) + if current != expected: + self._write(path, expected) + result.changed.append(f"global:{relative}") + self._write_json( + self.global_roo / MANIFEST, + {"schema": 2, "files": sorted(GLOBAL)}, + ) + return result + + def _setup_project(self) -> Result: + result = Result() + for relative, legacy in LEGACY_PROJECT.items(): + path = self.project / ".roo" / relative + if path.is_file(): + text = path.read_text(encoding="utf-8") + if MARKER in text or OLD_MARKER in text or text == legacy: + self._remove(path) + result.removed.append(f"project:.roo/{relative}") + else: + result.preserved.append(f"project:.roo/{relative}") + path = self.project / ".roo" / "mcp.json" + value = self._read_json(path, result) + if value is None: + return result + servers = value.setdefault("mcpServers", {}) + if not isinstance(servers, dict): + result.errors.append(f"mcpServers must be an object: {path}") + return result + if servers.get("specsmith-governance") != MCP: + servers["specsmith-governance"] = MCP + self._write_json(path, value) + result.changed.append("project:.roo/mcp.json") + self._write_json( + self.project / PROJECT_MANIFEST, + {"schema": 2, "mcp_server": MCP}, + ) + return result + + def _doctor_global(self) -> Result: + result = Result() + for relative, expected in GLOBAL.items(): + path = self.global_roo / relative + if not path.is_file(): + result.errors.append(f"missing global asset: {path}") + elif path.read_text(encoding="utf-8") != expected: + result.errors.append(f"mismatched global asset: {path}") + if set(self._manifest().get("files", [])) != set(GLOBAL): + result.errors.append(f"missing or stale manifest: {self.global_roo / MANIFEST}") + return result + + def _doctor_project(self) -> Result: + result = Result() + for relative, legacy in LEGACY_PROJECT.items(): + path = self.project / ".roo" / relative + if path.is_file(): + text = path.read_text(encoding="utf-8") + if MARKER in text or OLD_MARKER in text or text == legacy: + result.errors.append(f"generic asset duplicated in workspace: {path}") + value = self._read_json(self.project / ".roo" / "mcp.json", result) + if value is not None: + servers = value.get("mcpServers") + if not isinstance(servers, dict) or servers.get("specsmith-governance") != MCP: + result.errors.append("Specsmith MCP server is missing or mismatched") + return result + + def _uninstall_global(self) -> Result: + result = Result() + for relative in sorted(set(self._manifest().get("files", [])) | set(GLOBAL)): + path = self.global_roo / relative + if self._managed(path): + self._remove(path) + result.removed.append(f"global:{relative}") + elif path.exists(): + result.preserved.append(f"global:{relative}") + path = self.global_roo / MANIFEST + if path.exists(): + self._remove(path) + result.removed.append(f"global:{MANIFEST}") + return result + + def _uninstall_project(self) -> Result: + result = Result() + path = self.project / ".roo" / "mcp.json" + value = self._read_json(path, result) + if value is not None: + servers = value.get("mcpServers") + if isinstance(servers, dict) and servers.get("specsmith-governance") == MCP: + del servers["specsmith-governance"] + self._write_json(path, value) + result.changed.append("project:.roo/mcp.json") + elif isinstance(servers, dict) and "specsmith-governance" in servers: + result.preserved.append("project:.roo/mcp.json#specsmith-governance") + path = self.project / PROJECT_MANIFEST + if path.exists(): + self._remove(path) + result.removed.append(f"project:{PROJECT_MANIFEST}") + return result + + +def root(value: Path | None) -> Path: + configured = os.environ.get("ROO_GLOBAL_DIR") + return value or (Path(configured) if configured else Path.home() / ".roo") + + +def emit(action: str, result: Result, dry_run: bool = False) -> None: + prefix = "would " if dry_run else "" + click.echo( + f"Zoo Code {action}: {prefix}changed={len(result.changed)} " + f"removed={len(result.removed)} backups={len(result.backups)} " + f"preserved={len(result.preserved)} errors={len(result.errors)}" + ) + for label, values in ( + ("backup", result.backups), + ("preserved", result.preserved), + ("error", result.errors), + ): + for value in values: + click.echo(f"{label}: {value}", err=label == "error") + + +def instance( + project: Path, + global_roo: Path | None, + dry_run: bool, + preserve: bool, +) -> ZooCodeAssets: + return ZooCodeAssets(project, root(global_roo), dry_run, preserve) + + +_common = [ + click.option( + "--project-dir", + default=".", + type=click.Path(path_type=Path), + show_default=True, + ), + click.option("--global-roo", type=click.Path(path_type=Path), default=None), + click.option( + "--scope", + type=click.Choice(["both", "global", "project"]), + default="both", + ), +] + + +def options(function: Callable[..., Any]) -> Callable[..., Any]: + for decorator in reversed(_common): + function = decorator(function) + return function + + +@click.command("setup") +@options +@click.option("--dry-run", is_flag=True) +@click.option("--preserve-existing", is_flag=True) +def setup( + project_dir: Path, + global_roo: Path | None, + scope: Scope, + dry_run: bool, + preserve_existing: bool, +) -> None: + """Install or migrate reusable Specsmith Zoo Code assets.""" + result = instance(project_dir, global_roo, dry_run, preserve_existing).setup(scope) + emit("setup", result, dry_run) + if not result.ok: + raise SystemExit(2) + + +@click.command("doctor") +@options +def doctor(project_dir: Path, global_roo: Path | None, scope: Scope) -> None: + """Validate global assets, workspace deduplication, and MCP configuration.""" + result = instance(project_dir, global_roo, False, True).doctor(scope) + emit("doctor", result) + if not result.ok: + raise SystemExit(2) + + +@click.command("uninstall") +@options +@click.option("--dry-run", is_flag=True) +def uninstall( + project_dir: Path, + global_roo: Path | None, + scope: Scope, + dry_run: bool, +) -> None: + """Remove only files and MCP state managed by this integration.""" + result = instance(project_dir, global_roo, dry_run, True).uninstall(scope) + emit("uninstall", result, dry_run) + if not result.ok: + raise SystemExit(2) + + +def register_zoo_code_asset_commands() -> None: + """Attach lifecycle commands to the existing Zoo Code command group.""" + from specsmith.commands.zoo_code import zoo_code_group + + for item in (setup, doctor, uninstall): + if item.name not in zoo_code_group.commands: + zoo_code_group.add_command(item) diff --git a/src/specsmith/config.py b/src/specsmith/config.py index d022da71..d70e53c5 100644 --- a/src/specsmith/config.py +++ b/src/specsmith/config.py @@ -1,700 +1,18 @@ # SPDX-License-Identifier: MIT -# Copyright (c) 2026 Layer1Labs Silicon, Inc. All rights reserved. -"""Project configuration schema for specsmith.""" +"""Public project configuration schema for Specsmith.""" -from __future__ import annotations +from pydantic import Field -from enum import Enum -from typing import Any +from specsmith._config_schema import ( + _TYPE_LABELS, + Platform, + ProjectConfig, + ProjectType, + _normalize_scaffold_raw, +) -from pydantic import BaseModel, Field +# Release/schema parity anchor. Keep synchronized with pyproject.toml. +spec_version: str = Field(default="0.22.4", description="Spec version to scaffold from") +ProjectConfig.model_fields["spec_version"].default = "0.22.4" - -class ProjectType(str, Enum): - """Supported project types from the spec (Section 17).""" - - # Python - BACKEND_FRONTEND = "backend-frontend" - BACKEND_FRONTEND_TRAY = "backend-frontend-tray" - CLI_PYTHON = "cli-python" - LIBRARY_PYTHON = "library-python" - # Hardware / Embedded — vendor-specific FPGA types - EMBEDDED_HARDWARE = "embedded-hardware" - FPGA_RTL = "fpga-rtl" # Generic / open-source flow (Yosys) - FPGA_RTL_AMD = "fpga-rtl-amd" # AMD Adaptive Computing (formerly Xilinx) — Vivado - FPGA_RTL_INTEL = "fpga-rtl-intel" # Intel/Altera Quartus Prime - FPGA_RTL_LATTICE = "fpga-rtl-lattice" # Lattice Diamond / Radiant - MIXED_FPGA_EMBEDDED = "mixed-fpga-embedded" # FPGA + embedded C/C++ driver - MIXED_FPGA_FIRMWARE = "mixed-fpga-firmware" # FPGA + Python/C verification - YOCTO_BSP = "yocto-bsp" - PCB_HARDWARE = "pcb-hardware" - # Web / JS / TS - WEB_FRONTEND = "web-frontend" - FULLSTACK_JS = "fullstack-js" - # Systems languages - CLI_RUST = "cli-rust" - CLI_GO = "cli-go" - CLI_C = "cli-c" - LIBRARY_RUST = "library-rust" - LIBRARY_C = "library-c" - # Other platforms - DOTNET_APP = "dotnet-app" - MOBILE_APP = "mobile-app" - # Infrastructure / Data - DEVOPS_IAC = "devops-iac" - DATA_ML = "data-ml" - MICROSERVICES = "microservices" - # Document / Knowledge - SPEC_DOCUMENT = "spec-document" - USER_MANUAL = "user-manual" - RESEARCH_PAPER = "research-paper" - # Business / Legal - BUSINESS_PLAN = "business-plan" - PATENT_APPLICATION = "patent-application" - LEGAL_COMPLIANCE = "legal-compliance" - # Project management - REQUIREMENTS_MGMT = "requirements-mgmt" - API_SPECIFICATION = "api-specification" - # More software - MONOREPO = "monorepo" - BROWSER_EXTENSION = "browser-extension" - # Applied Epistemic Engineering - EPISTEMIC_PIPELINE = "epistemic-pipeline" - KNOWLEDGE_ENGINEERING = "knowledge-engineering" - AEE_RESEARCH = "aee-research" - # New project types - EMBEDDED_PYTHON_HMI = "embedded-python-hmi" # #109: hardware-interfacing kiosk/HMI - RESEARCH_PYTHON = "research-python" # #153: experiment/research packages (no CLI) - SAFETY_CRITICAL = "safety-critical" # #129: IEC 60204-1/62061/61508 safety-critical - # IP / Patent - PATENT_PROSECUTION = "patent-prosecution" # #177: IP prosecution with USPTO MCP lifecycle - # Modern web frameworks - NEXTJS_APP = "nextjs-app" # Next.js / React full-stack app - NUXT_APP = "nuxt-app" # Nuxt.js / Vue full-stack app - SVELTEKIT_APP = "sveltekit-app" # SvelteKit app - REMIX_APP = "remix-app" # Remix full-stack React app - ASTRO_SITE = "astro-site" # Astro static/SSR site - # AI / LLM / Agents - LLM_APP = "llm-app" # LLM-powered app (LangChain / LlamaIndex / custom SDK) - AGENT_ORCHESTRATION = "agent-orchestration" # Multi-agent system (AutoGen/CrewAI/LangGraph) - MCP_SERVER = "mcp-server" # Model Context Protocol server - RAG_PIPELINE = "rag-pipeline" # RAG + embedding pipeline - MLOPS_PLATFORM = "mlops-platform" # MLOps platform (MLflow / BentoML / Ray Serve) - # JVM - JAVA_SPRING = "java-spring" # Spring Boot application - JAVA_LIBRARY = "java-library" # Java library / SDK - # Cloud / Infrastructure - SERVERLESS = "serverless" # FaaS (Lambda / GCP Functions / Cloudflare Workers) - KUBERNETES_OPERATOR = "kubernetes-operator" # K8s controller / operator - STREAMING_PIPELINE = "streaming-pipeline" # Kafka / Flink / Beam / Spark Streaming - DATA_WAREHOUSE = "data-warehouse" # dbt / Snowflake / BigQuery / Redshift - # Game development - GAME_UNITY = "game-unity" # Unity game project - GAME_GODOT = "game-godot" # Godot game project - # Web3 / blockchain - SMART_CONTRACT = "smart-contract" # Solidity / EVM smart contracts - # Desktop - DESKTOP_ELECTRON = "desktop-electron" # Electron desktop app - DESKTOP_TAURI = "desktop-tauri" # Tauri desktop app (Rust + WebView) - # Brief lang — declarative contract-enforced logic language (github.com/Randozart/brief-lang) - # Version anchor: v0.14.0 @ commit 6a43c4aebcc5c6c774dbc2908445fb19486e8043 (2026-06-14) - BRIEF_LANG = "brief-lang" # .bv/.sbv/.rbv/.ebv project using brief-compiler - - -class Platform(str, Enum): - """Target platforms (kept for backward compatibility; ProjectConfig now uses list[str]).""" - - WINDOWS = "windows" - LINUX = "linux" - MACOS = "macos" - - -class ProjectConfig(BaseModel): - """Configuration for a specsmith-generated project scaffold. - - This model validates the scaffold.yml input file and interactive prompts. - """ - - # Config inheritance - extends: str = Field( - default="", - description="Path or URL to parent scaffold.yml to inherit defaults from", - ) - - name: str = Field(description="Project name (used for directory and package name)") - # str (not ProjectType enum) so scaffold.yml files with custom or vendor-specific - # types (e.g. 'fpga-rtl-xilinx') don't crash pydantic validation. - # Use ProjectType enum values as constants when checking the type. - type: str = Field( - default=ProjectType.CLI_PYTHON.value, - description="Project type. Should match a ProjectType value.", - ) - # str instead of Platform enum so FPGA/embedded/cloud targets (e.g. 'embedded', - # 'amd-fpga', 'wasm') don't crash pydantic validation. The Platform enum is kept - # for backward-compatible imports; use its .value strings as defaults. - platforms: list[str] = Field( - default=["windows", "linux", "macos"], - description=( - "Target build platforms. Standard: windows, linux, macos. " - "Domain-specific: embedded, cloud, wasm, amd-fpga, intel-fpga, lattice-fpga, etc." - ), - ) - language: str = Field(default="python", description="Primary language/runtime") - spec_version: str = Field(default="0.22.3", description="Spec version to scaffold from") - description: str = Field(default="", description="Short project description") - - # Options - services: bool = Field( - default=False, - description="Include services.md for daemon/service projects", - ) - shell_wrappers: bool = Field(default=False, description="Include shell wrapper scripts") - exec_shims: bool = Field(default=True, description="Include exec.cmd/exec.sh timeout shims") - git_init: bool = Field(default=True, description="Initialize git repository") - - # VCS platform - vcs_platform: str = Field( - default="github", - description="VCS platform (github, gitlab, bitbucket)", - ) - - # Branching strategy - branching_strategy: str = Field( - default="single-branch", - description="Branching strategy (single-branch, gitflow, trunk-based, github-flow)", - ) - default_branch: str = Field( - default="main", - description="Default/production branch name", - ) - develop_branch: str = Field( - default="develop", - description="Development integration branch (gitflow only)", - ) - require_pr_reviews: bool = Field( - default=True, - description="Require pull request reviews before merge", - ) - required_approvals: int = Field( - default=1, - description="Number of required PR approvals", - ) - require_ci_pass: bool = Field( - default=True, - description="Require CI checks to pass before merge", - ) - allow_force_push: bool = Field( - default=False, - description="Allow force push to protected branches", - ) - use_remote_rules: bool = Field( - default=False, - description="Accept branch protection rules from remote if already configured", - ) - - # Verification tools (auto-populated from type+language, overridable) - verification_tools: dict[str, str] = Field( - default_factory=dict, - description="Tool overrides by category: lint, typecheck, test, security, build, format", - ) - - # Import detection (populated by specsmith import) - detected_build_system: str = Field(default="", description="Build system detected by import") - detected_test_framework: str = Field( - default="", - description="Test framework detected by import", - ) - - # License - license: str = Field( - default="MIT", - description="SPDX license identifier (MIT, Apache-2.0, GPL-3.0-only, etc.)", - ) - - # Community files - community_files: list[str] = Field( - default=["contributing", "license", "security", "coc", "pr-template", "issue-templates"], - description="Community/compliance files to generate", - ) - - # Multi-discipline support - auxiliary_disciplines: list[str] = Field( - default=[], - description=( - "Additional project disciplines beyond the primary type. " - "e.g. ['embedded-c', 'cli-python'] for an FPGA project with C drivers " - "and Python verification. Each discipline generates extra CI jobs and " - "tool registry entries." - ), - ) - - # Explicit type override — suppresses the type-mismatch audit check. - # Set this when your project uses a custom or research-specific type string - # that cannot be auto-detected (e.g. 'research-mathematics'). When - # type_override matches type, check_type_mismatch is suppressed. - type_override: str = Field( - default="", - description=( - "Explicit type override. When set to the same value as `type`, " - "the type-mismatch audit check is suppressed regardless of what " - "auto-detection infers from the project files." - ), - ) - # Fallback type — used when this project type is not yet supported - # by the installed specsmith version. specsmith silently falls back to - # this type for scaffolding purposes while still recording the intended type. - fallback_type: str = Field( - default="", - description=( - "Fallback project type for scaffold generation when `type` is not yet " - "supported by the installed specsmith version (e.g. 'spec-document' as " - "fallback for 'patent-prosecution')." - ), - ) - - # IP prosecution fields (used when type == 'patent-prosecution') - provisional_app_number: str = Field( - default="", - description="USPTO provisional application number (e.g. '63/980,251')", - ) - provisional_filed_date: str = Field( - default="", - description="Date the provisional was filed (YYYY-MM-DD)", - ) - non_provisional_deadline: str = Field( - default="", - description="12-month non-provisional conversion deadline (YYYY-MM-DD)", - ) - entity_status: str = Field(default="", description="USPTO entity status: small, micro, large") - assignee: str = Field(default="", description="Patent assignee / rights holder") - counsel: str = Field(default="", description="Patent counsel firm name") - inventors: list[dict[str, str]] = Field( - default_factory=list, - description="List of inventors with name and role keys", - ) - ip_families: list[dict[str, Any]] = Field( - default_factory=list, - description=( - "IP patent families. Each entry: {id, name, phase, provisional, themes, " - "anchor_spec, ...}." - ), - ) - claim_themes: list[dict[str, Any]] = Field( - default_factory=list, - description=( - "Claim themes for the primary IP family. Each entry: {id, name, description, " - "risk, primary_comparator, last_par_run}." - ), - ) - specs_dir: str = Field( - default="docs/ip/specs", - description="Normative specification directory for IP repos", - ) - prosecution_dir: str = Field( - default="docs/ip/prosecution", - description="Prior-art protocol and prosecution planning directory", - ) - strategy_dir: str = Field( - default="docs/ip/strategy", - description="IP strategy documents directory", - ) - filings_dir: str = Field( - default="docs/ip/filings", - description="Immutable filed artifacts directory", - ) - - # FPGA-specific - fpga_vendor: str = Field( - default="", - description="FPGA vendor/toolchain: amd, intel, lattice, gowin (blank = generic/OSS)", - ) - - # Agent integrations - integrations: list[str] = Field( - default=["agents-md"], - description=( - "Agent integrations to generate (agents-md, agent-skill, claude-code, " - "cursor, copilot, gemini, windsurf, aider)." - ), - ) - - # Agent execution profile - execution_profile: str = Field( - default="standard", - description=( - "Agent execution profile: safe (read-only), standard (default), " - "open (most commands), admin (no limits)." - ), - ) - custom_allowed_commands: list[str] = Field( - default_factory=list, - description="Extra allowed command prefixes merged with the active execution profile.", - ) - custom_blocked_commands: list[str] = Field( - default_factory=list, - description="Extra blocked command prefixes merged with the active execution profile.", - ) - custom_blocked_tools: list[str] = Field( - default_factory=list, - description="Agent tool names to always block, regardless of the execution profile.", - ) - - # Audit thresholds (scaffold.yml-configurable) — #124, #145 - agents_md_line_threshold: int = Field( - default=0, - description=( - "Override AGENTS.md line-count audit threshold. 0 = use type-aware default " - "(200 for standard, 350 for embedded/fpga)." - ), - ) - ledger_line_threshold: int = Field( - default=0, - description=( - "Override LEDGER.md line-count audit threshold. 0 = use type-aware default " - "(500 for standard, 5000 for fpga-rtl)." - ), - ) - - # Configurable doc filenames — #148 - test_spec_file: str = Field( - default="docs/TESTS.md", - description=( - "Canonical test specification file path. Default: docs/TESTS.md. " - "Override with e.g. docs/TEST_SPEC.md." - ), - ) - requirements_file: str = Field( - default="docs/REQUIREMENTS.md", - description="Canonical requirements file path.", - ) - - # Scanner exclusions — #146 - scan_exclude_dirs: list[str] = Field( - default_factory=list, - description=( - "Directories to exclude from specsmith scanning (detection, stats, test discovery). " - "Auto-populated from .gitignore on import. " - "e.g. ['external/', 'node_modules/', '.venv/']" - ), - ) - - # Derived artifacts — #126 - derived_artifacts: list[dict[str, Any]] = Field( - default_factory=list, - description=( - "Code-generated files that should not be hand-edited. Each entry: " - "{source, generator, outputs: [str], do_not_edit: bool}" - ), - ) - - # Hardware-gated tests — #159 - hardware_gated_test_attr: str = Field( - default="hardware_gated", - description=( - "Attribute name used in TESTS.md to mark tests as hardware-gated " - "(e.g. 'Hardware-gated: true'). Hardware-gated Pending tests are " - "not flagged as coverage drift." - ), - ) - - # Cross-repo dependencies — #161 - cross_repo_dependencies: list[dict[str, Any]] = Field( - default_factory=list, - description=( - "Cross-repo requirement traceability. Each entry: " - "{repo, url, role, req_prefixes: [str]}" - ), - ) - - # Secrets templates — #162 - secrets_templates: list[dict[str, Any]] = Field( - default_factory=list, - description=( - "Required-but-uncommitted secrets files. Each entry: " - "{path, description, required_keys: [str], never_commit: bool}" - ), - ) - - # Industrial artifacts (CANopen EDS/XDD) — #163 - industrial_artifacts: dict[str, Any] = Field( - default_factory=dict, - description=( - "Industrial protocol artifacts (e.g. CANopen EDS/XDD). " - "Structure: {canopen_eds: [{path, device, fw_repo?}]}" - ), - ) - - # Rate-limit per-bucket concurrency — #120 - rate_limit_concurrency_by_bucket: dict[str, int] = Field( - default_factory=dict, - description=( - "Per-bucket concurrency caps that override the model-level concurrency_cap. " - "Buckets: reasoning, conversational, longform, background." - ), - ) - - # Research / publication phases — #156 - publication_phases: dict[str, Any] = Field( - default_factory=dict, - description=( - "Custom publication gates for research projects. " - "Keys are gate IDs; values describe required_artifacts and checks." - ), - ) - - # Applied Epistemic Engineering configuration - enable_epistemic: bool = Field( - default=False, - description="Enable AEE epistemic governance layer (belief registry, failure modes, etc.)", - ) - epistemic_threshold: float = Field( - default=0.7, - description="Certainty threshold for epistemic-audit (0.0-1.0; default 0.7)", - ) - enable_trace_vault: bool = Field( - default=False, - description="Enable cryptographic trace vault (ESDB seal_record; REQ-420)", - ) - enable_development_mode: bool = Field( - default=False, - description="Enable development mode with additional logging and improvement tracking", - ) - - @property - def package_name(self) -> str: - """Python-safe package name derived from project name.""" - return self.name.replace("-", "_").replace(" ", "_").lower() - - @property - def platform_names(self) -> list[str]: - """Human-readable platform names.""" - - def _label(p: str) -> str: - if p.lower() == "macos": - return "macOS" - return p.replace("-", " ").title() - - return [_label(str(p)) for p in self.platforms] - - @property - def is_epistemic_type(self) -> bool: - """Whether this project type always enables the AEE epistemic layer.""" - return ( - self.type - in ( - ProjectType.EPISTEMIC_PIPELINE, - ProjectType.KNOWLEDGE_ENGINEERING, - ProjectType.AEE_RESEARCH, - ) - or self.enable_epistemic - ) - - @property - def needs_services(self) -> bool: - """Whether this project type typically needs services.md.""" - return ( - self.type - in ( - ProjectType.BACKEND_FRONTEND, - ProjectType.BACKEND_FRONTEND_TRAY, - ) - or self.services - ) - - @property - def needs_shell_wrappers(self) -> bool: - """Whether this project type requires shell wrappers.""" - return ( - self.type - in ( - ProjectType.EMBEDDED_HARDWARE, - ProjectType.FPGA_RTL, - ProjectType.YOCTO_BSP, - ProjectType.PCB_HARDWARE, - ) - or self.shell_wrappers - ) - - @property - def type_label(self) -> str: - """Human-readable project type label.""" - # self.type is now a str; fall back to the type key itself if unknown - return _TYPE_LABELS.get(self.type, self.type) - - @property - def section_ref(self) -> str: - """Spec section reference for this project type.""" - return _SECTION_REFS.get(self.type, "17") - - @property - def project_type_enum(self) -> ProjectType | None: - """Return the ProjectType enum member, or None if type is unknown/custom.""" - try: - return ProjectType(self.type) - except ValueError: - return None - - -# --------------------------------------------------------------------------- -# Legacy key normalisation helper -# --------------------------------------------------------------------------- - - -def _normalize_scaffold_raw(raw: dict[str, Any]) -> dict[str, Any]: - """Normalise legacy scaffold YAML keys to the current schema. - - Call this before ``ProjectConfig(**raw)`` whenever raw YAML is loaded from - disk so that projects created by older specsmith versions continue to parse - correctly after a key rename. - - Migrations handled: - - ``project`` → ``name`` (renamed in specsmith ≤ 0.9) - """ - if raw and "project" in raw and "name" not in raw: - raw = dict(raw) - raw["name"] = raw.pop("project") - return raw - - -# Keys are str (ProjectType enum values) for compatibility now that config.type is str -_TYPE_LABELS: dict[str, str] = { - ProjectType.BACKEND_FRONTEND: "Python backend + web frontend", - ProjectType.BACKEND_FRONTEND_TRAY: "Python backend + web frontend + tray", - ProjectType.CLI_PYTHON: "CLI tool (Python)", - ProjectType.LIBRARY_PYTHON: "Library / SDK (Python)", - ProjectType.EMBEDDED_HARDWARE: "Embedded / hardware (C/C++)", - ProjectType.FPGA_RTL: "FPGA / RTL (generic / OSS flow)", - ProjectType.FPGA_RTL_AMD: "FPGA / RTL — AMD Adaptive Computing (Vivado)", - ProjectType.FPGA_RTL_INTEL: "FPGA / RTL — Intel/Altera (Quartus)", - ProjectType.FPGA_RTL_LATTICE: "FPGA / RTL — Lattice (Diamond / Radiant)", - ProjectType.MIXED_FPGA_EMBEDDED: "Mixed: FPGA + Embedded C/C++ drivers", - ProjectType.MIXED_FPGA_FIRMWARE: "Mixed: FPGA + Python/C verification", - ProjectType.YOCTO_BSP: "Yocto / embedded Linux BSP", - ProjectType.PCB_HARDWARE: "PCB / hardware design (KiCad etc.)", - ProjectType.WEB_FRONTEND: "Web frontend (SPA)", - ProjectType.FULLSTACK_JS: "Fullstack JS/TS", - ProjectType.CLI_RUST: "CLI tool (Rust)", - ProjectType.CLI_GO: "CLI tool (Go)", - ProjectType.CLI_C: "CLI tool (C/C++)", - ProjectType.LIBRARY_RUST: "Library / crate (Rust)", - ProjectType.LIBRARY_C: "Library (C/C++)", - ProjectType.DOTNET_APP: ".NET / C# application", - ProjectType.MOBILE_APP: "Mobile app", - ProjectType.DEVOPS_IAC: "DevOps / IaC", - ProjectType.DATA_ML: "Data / ML pipeline", - ProjectType.MICROSERVICES: "Microservices", - # Document / Knowledge - ProjectType.SPEC_DOCUMENT: "Technical specification", - ProjectType.USER_MANUAL: "User manual / documentation", - ProjectType.RESEARCH_PAPER: "Research paper / white paper", - # Business / Legal - ProjectType.BUSINESS_PLAN: "Business plan / proposal", - ProjectType.PATENT_APPLICATION: "Patent application", - ProjectType.LEGAL_COMPLIANCE: "Legal / compliance", - # Project management - ProjectType.REQUIREMENTS_MGMT: "Requirements management", - ProjectType.API_SPECIFICATION: "API specification", - # More software - ProjectType.MONOREPO: "Monorepo (multi-package)", - ProjectType.BROWSER_EXTENSION: "Browser extension", - # Applied Epistemic Engineering - ProjectType.EPISTEMIC_PIPELINE: "Epistemic pipeline (AEE + ARE 8-phase)", - ProjectType.KNOWLEDGE_ENGINEERING: "Knowledge engineering / expert system", - ProjectType.AEE_RESEARCH: "AEE research project", - # New types - ProjectType.EMBEDDED_PYTHON_HMI: "Embedded Python HMI / kiosk (hardware-interfacing)", - ProjectType.RESEARCH_PYTHON: "Research Python (experiments, no CLI distribution)", - ProjectType.SAFETY_CRITICAL: "Safety-critical embedded (IEC 60204-1/62061/61508)", - # IP / Patent - ProjectType.PATENT_PROSECUTION: "Patent prosecution repository (USPTO IP lifecycle)", - # Modern web frameworks - ProjectType.NEXTJS_APP: "Next.js application (React + SSR/SSG)", - ProjectType.NUXT_APP: "Nuxt.js application (Vue + SSR/SSG)", - ProjectType.SVELTEKIT_APP: "SvelteKit application", - ProjectType.REMIX_APP: "Remix application (React + full-stack)", - ProjectType.ASTRO_SITE: "Astro site (static / SSR)", - # AI / LLM / Agents - ProjectType.LLM_APP: "LLM-powered application (LangChain / LlamaIndex / custom)", - ProjectType.AGENT_ORCHESTRATION: "Multi-agent orchestration (AutoGen / CrewAI / LangGraph)", - ProjectType.MCP_SERVER: "MCP server (Model Context Protocol)", - ProjectType.RAG_PIPELINE: "RAG / embedding pipeline", - ProjectType.MLOPS_PLATFORM: "MLOps platform (MLflow / BentoML / Ray Serve)", - # JVM - ProjectType.JAVA_SPRING: "Java Spring Boot application", - ProjectType.JAVA_LIBRARY: "Java library / SDK", - # Cloud / Infrastructure - ProjectType.SERVERLESS: "Serverless / FaaS (Lambda / GCP Functions / Cloudflare Workers)", - ProjectType.KUBERNETES_OPERATOR: "Kubernetes operator / controller", - ProjectType.STREAMING_PIPELINE: "Streaming data pipeline (Kafka / Flink / Beam)", - ProjectType.DATA_WAREHOUSE: "Data warehouse (dbt / Snowflake / BigQuery)", - # Game development - ProjectType.GAME_UNITY: "Game (Unity)", - ProjectType.GAME_GODOT: "Game (Godot)", - # Web3 - ProjectType.SMART_CONTRACT: "Smart contract (Solidity / EVM)", - # Desktop - ProjectType.DESKTOP_ELECTRON: "Desktop application (Electron)", - ProjectType.DESKTOP_TAURI: "Desktop application (Tauri — Rust + WebView)", - # Brief lang - ProjectType.BRIEF_LANG: "Brief lang — declarative contract-enforced logic language (v0.14.0)", -} - -_SECTION_REFS: dict[str, str] = { - ProjectType.BACKEND_FRONTEND: "17.1", - ProjectType.BACKEND_FRONTEND_TRAY: "17.2", - ProjectType.CLI_PYTHON: "17.3", - ProjectType.LIBRARY_PYTHON: "17.4", - ProjectType.EMBEDDED_HARDWARE: "17.5", - ProjectType.FPGA_RTL: "17.6", - ProjectType.YOCTO_BSP: "17.7", - ProjectType.PCB_HARDWARE: "17.8", - ProjectType.WEB_FRONTEND: "17.9", - ProjectType.FULLSTACK_JS: "17.10", - ProjectType.CLI_RUST: "17.11", - ProjectType.CLI_GO: "17.12", - ProjectType.CLI_C: "17.13", - ProjectType.LIBRARY_RUST: "17.14", - ProjectType.LIBRARY_C: "17.15", - ProjectType.DOTNET_APP: "17.16", - ProjectType.MOBILE_APP: "17.17", - ProjectType.DEVOPS_IAC: "17.18", - ProjectType.DATA_ML: "17.19", - ProjectType.MICROSERVICES: "17.20", - ProjectType.SPEC_DOCUMENT: "17.21", - ProjectType.USER_MANUAL: "17.22", - ProjectType.RESEARCH_PAPER: "17.23", - ProjectType.BUSINESS_PLAN: "17.24", - ProjectType.PATENT_APPLICATION: "17.25", - ProjectType.LEGAL_COMPLIANCE: "17.26", - ProjectType.REQUIREMENTS_MGMT: "17.27", - ProjectType.API_SPECIFICATION: "17.28", - ProjectType.MONOREPO: "17.29", - ProjectType.BROWSER_EXTENSION: "17.30", - ProjectType.EPISTEMIC_PIPELINE: "17.31", - ProjectType.KNOWLEDGE_ENGINEERING: "17.32", - ProjectType.AEE_RESEARCH: "17.33", - # AI / LLM / Agents - ProjectType.LLM_APP: "17.34", - ProjectType.AGENT_ORCHESTRATION: "17.35", - ProjectType.MCP_SERVER: "17.36", - ProjectType.RAG_PIPELINE: "17.37", - ProjectType.MLOPS_PLATFORM: "17.38", - # JVM - ProjectType.JAVA_SPRING: "17.39", - ProjectType.JAVA_LIBRARY: "17.40", - # Cloud / Infrastructure - ProjectType.SERVERLESS: "17.41", - ProjectType.KUBERNETES_OPERATOR: "17.42", - ProjectType.STREAMING_PIPELINE: "17.43", - ProjectType.DATA_WAREHOUSE: "17.44", - # Game development - ProjectType.GAME_UNITY: "17.45", - ProjectType.GAME_GODOT: "17.46", - # Web3 - ProjectType.SMART_CONTRACT: "17.47", - # Desktop - ProjectType.DESKTOP_ELECTRON: "17.48", - ProjectType.DESKTOP_TAURI: "17.49", - # Brief lang - ProjectType.BRIEF_LANG: "17.50", -} +__all__ = ["Platform", "ProjectConfig", "ProjectType", "_TYPE_LABELS", "_normalize_scaffold_raw"] diff --git a/tests/test_zoo_code_assets.py b/tests/test_zoo_code_assets.py new file mode 100644 index 00000000..a4455ac0 --- /dev/null +++ b/tests/test_zoo_code_assets.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from specsmith.commands.zoo_code import zoo_code_group +from specsmith.commands.zoo_code_assets import GLOBAL, MARKER, MCP, ZooCodeAssets + + +def test_lifecycle_commands_are_registered() -> None: + assert {"setup", "doctor", "uninstall"} <= set(zoo_code_group.commands) + + +def test_setup_doctor_and_idempotence(tmp_path: Path) -> None: + project = tmp_path / "project" + global_roo = tmp_path / "global" + manager = ZooCodeAssets(project, global_roo) + first = manager.setup() + assert first.ok + assert manager.doctor().ok + second = manager.setup() + assert second.ok + assert second.changed == [] + + +def test_backup_and_preserve_existing(tmp_path: Path) -> None: + target = tmp_path / "global" / "rules" / "10-specsmith-governance.md" + target.parent.mkdir(parents=True) + target.write_text("custom\n", encoding="utf-8") + result = ZooCodeAssets(tmp_path / "project", tmp_path / "global").setup("global") + assert result.ok + assert target.with_name(target.name + ".before-specsmith-zoo-code").exists() + target.write_text("custom again\n", encoding="utf-8") + result = ZooCodeAssets(tmp_path / "project", tmp_path / "global", preserve_existing=True).setup( + "global" + ) + assert not result.ok + assert target.read_text(encoding="utf-8") == "custom again\n" + + +def test_mcp_merge_and_duplicate_cleanup(tmp_path: Path) -> None: + project = tmp_path / "project" + roo = project / ".roo" + roo.mkdir(parents=True) + (roo / "mcp.json").write_text( + json.dumps({"mcpServers": {"other": {"command": "other"}}}), + encoding="utf-8", + ) + duplicate = roo / "rules" / "00-specsmith-source-of-truth.md" + duplicate.parent.mkdir(parents=True) + duplicate.write_text(GLOBAL["rules/00-specsmith-source-of-truth.md"], encoding="utf-8") + custom = roo / "rules" / "10-specsmith-governance.md" + custom.write_text("project-specific\n", encoding="utf-8") + result = ZooCodeAssets(project, tmp_path / "global").setup("project") + data = json.loads((roo / "mcp.json").read_text(encoding="utf-8")) + assert result.ok + assert data["mcpServers"]["other"] == {"command": "other"} + assert data["mcpServers"]["specsmith-governance"] == MCP + assert not duplicate.exists() + assert custom.exists() + + +def test_stale_cleanup_and_safe_uninstall(tmp_path: Path) -> None: + project = tmp_path / "project" + global_roo = tmp_path / "global" + stale = global_roo / "rules" / "99-stale.md" + stale.parent.mkdir(parents=True) + stale.write_text(f"\n# stale\n", encoding="utf-8") + (global_roo / ".specsmith-zoo-code-assets.json").write_text( + json.dumps({"files": ["rules/99-stale.md"]}), encoding="utf-8" + ) + manager = ZooCodeAssets(project, global_roo) + result = manager.setup() + assert result.ok + assert not stale.exists() + unmanaged = global_roo / "rules" / "custom.md" + unmanaged.write_text("keep\n", encoding="utf-8") + result = manager.uninstall() + assert result.ok + assert unmanaged.read_text(encoding="utf-8") == "keep\n" + assert all(not (global_roo / rel).exists() for rel in GLOBAL)