From d18387849d2c70806b97f85c9086644dc00652fd Mon Sep 17 00:00:00 2001 From: "Jucheng (Jim) Zhao" Date: Thu, 11 Jun 2026 20:59:15 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20add=20/speckit-delta=20=E2=80=94=20cons?= =?UTF-8?q?titution-driven=20vision-gap=20analysis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the loop between a project's constitution and its feature pipeline. /speckit-delta reads the constitution's Vision & Direction section (North Star, Target Users & Value, Long-Term Objectives, Non-Goals), inspects the current repo state (code + specs), and recommends the next feature with a handoff into /speckit-specify. Included: - templates/commands/delta.md β€” the command (registers across integrations) - templates/constitution-template.md β€” adds the Vision & Direction section the command depends on - templates/plan-template.md β€” adds a Vision Alignment Check gate so plans are checked against the Vision, not just the principles - templates/commands/constitution.md β€” guidance for populating Vision (infer from README/docs when absent; TODO(VISION) markers, never bare placeholders) - one-line registrations (SKILL_DESCRIPTIONS, _FALLBACK_CORE_COMMAND_NAMES, claude ARGUMENT_HINTS) + tests/test_delta_and_vision.py + expected-file inventory updates All additions are backward-compatible: existing projects are unaffected until they regenerate their constitution. Co-Authored-By: Claude Fable 5 --- README.md | 1 + src/specify_cli/__init__.py | 1 + src/specify_cli/extensions.py | 1 + .../integrations/claude/__init__.py | 1 + templates/commands/constitution.md | 7 + templates/commands/delta.md | 200 ++++++++++++++++++ templates/constitution-template.md | 48 +++++ templates/plan-template.md | 25 +++ .../test_integration_base_markdown.py | 2 +- .../test_integration_base_skills.py | 4 +- .../test_integration_base_toml.py | 1 + .../test_integration_base_yaml.py | 1 + .../integrations/test_integration_copilot.py | 10 +- .../integrations/test_integration_generic.py | 2 + tests/test_delta_and_vision.py | 122 +++++++++++ 15 files changed, 420 insertions(+), 6 deletions(-) create mode 100644 templates/commands/delta.md create mode 100644 tests/test_delta_and_vision.py diff --git a/README.md b/README.md index 0a0b4119b2..3217c842ce 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,7 @@ Additional commands for enhanced quality and validation: | `/speckit.clarify` | `speckit-clarify` | Clarify underspecified areas (recommended before `/speckit.plan`; formerly `/quizme`) | | `/speckit.analyze` | `speckit-analyze` | Cross-artifact consistency & coverage analysis (run after `/speckit.tasks`, before `/speckit.implement`) | | `/speckit.checklist` | `speckit-checklist` | Generate custom quality checklists that validate requirements completeness, clarity, and consistency (like "unit tests for English") | +| `/speckit.delta` | `speckit-delta` | Compute the gap between the constitution's Vision and current repo state, then recommend the next feature to specify | ## πŸ”§ Specify CLI Reference diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 186593000c..8448741876 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -433,6 +433,7 @@ def _print_cli_warning( "clarify": "Structured clarification workflow for underspecified requirements.", "constitution": "Create or update project governing principles and development guidelines.", "checklist": "Generate custom quality checklists for validating requirements completeness and clarity.", + "delta": "Compute the gap between the constitution's Vision and current repo state to recommend the next feature.", "taskstoissues": "Convert tasks from tasks.md into GitHub issues.", } diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index db53b7997f..db00389093 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -32,6 +32,7 @@ "analyze", "clarify", "constitution", + "delta", "implement", "plan", "checklist", diff --git a/src/specify_cli/integrations/claude/__init__.py b/src/specify_cli/integrations/claude/__init__.py index 57ecb354ad..33bc7cc1e4 100644 --- a/src/specify_cli/integrations/claude/__init__.py +++ b/src/specify_cli/integrations/claude/__init__.py @@ -21,6 +21,7 @@ "clarify": "Optional areas to clarify in the spec", "constitution": "Principles or values for the project constitution", "checklist": "Domain or focus area for the checklist", + "delta": "Optional focus hint for the next feature recommendation", "taskstoissues": "Optional filter or label for GitHub issues", } diff --git a/templates/commands/constitution.md b/templates/commands/constitution.md index 29ae9a09e2..12e8f0c912 100644 --- a/templates/commands/constitution.md +++ b/templates/commands/constitution.md @@ -73,8 +73,15 @@ Follow this execution flow: 3. Draft the updated constitution content: - Replace every placeholder with concrete text (no bracketed tokens left except intentionally retained template slots that the project has chosen not to define yetβ€”explicitly justify any left). - Preserve heading hierarchy and comments can be removed once replaced unless they still add clarifying guidance. + - Ensure the **Vision & Direction** section is populated with: + - A single, durable **North Star** statement (1–2 sentences). + - **Target Users & Value** β€” who the project serves and the enduring value delivered. + - **Long-Term Objectives** β€” 3–7 outcome-oriented, observable objectives (not feature lists). Each must be phrased so progress against it can be assessed by inspecting repo state (`__SPECKIT_COMMAND_DELTA__` depends on this). + - **Non-Goals** β€” explicit out-of-scope statements that bound the project. + If user input does not supply Vision content, infer it from `README.md`, existing docs, and the prior constitution. If still indeterminate, mark with `TODO(VISION): ...` and include in the Sync Impact Report β€” do **not** leave bracketed placeholders. - Ensure each Principle section: succinct name line, paragraph (or bullet list) capturing non‑negotiable rules, explicit rationale if not obvious. - Ensure Governance section lists amendment procedure, versioning policy, and compliance review expectations. + - Vision amendments are governed by the same Governance rules: changes to **North Star** or **Long-Term Objectives** are MAJOR bumps; adding/removing **Non-Goals** or refining **Target Users & Value** is MINOR; wording polish is PATCH. 4. Consistency propagation checklist (convert prior checklist into active validations): - Read `.specify/templates/plan-template.md` and ensure any "Constitution Check" or rules align with updated principles. diff --git a/templates/commands/delta.md b/templates/commands/delta.md new file mode 100644 index 0000000000..b66ede0ccd --- /dev/null +++ b/templates/commands/delta.md @@ -0,0 +1,200 @@ +--- +description: Compute the gap between the constitution's Vision & Direction and the current repo state (code + specs) to recommend the next feature. +handoffs: + - label: Specify Next Feature + agent: speckit.specify + prompt: Specify the next feature recommended by the delta report. I want to build... +--- + +## User Input + +```text +$ARGUMENTS +``` + +User input is optional. When provided, treat it as a focus hint (e.g., a specific +Long-Term Objective to prioritize, a constraint like "no infra work this cycle", +or a candidate feature to evaluate against the Vision). + +## Pre-Execution Checks + +**Check for extension hooks (before delta)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_delta` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Outline. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + +## Outline + +The `__SPECKIT_COMMAND_DELTA__` command pairs with the Vision & Direction section +of the constitution. `__SPECKIT_COMMAND_SPECIFY__` adds work *incrementally*; +this command answers the orthogonal question: **given everything we have shipped +so far, what should we build next to close the gap to the Vision?** + +It is a read-only analysis (plus a single write to `.specify/memory/delta.md`). +Do **not** create specs, branches, plans, or tasks here. + +Follow this execution flow: + +1. **Load the Vision**: + - Read `.specify/memory/constitution.md`. + - Extract the **Vision & Direction** section: North Star, Target Users & Value, + **Long-Term Objectives** (the primary unit of comparison), and **Non-Goals**. + - If the section is missing or still contains bracketed placeholders, abort with + an error instructing the user to run `__SPECKIT_COMMAND_CONSTITUTION__` first. + +2. **Inventory cumulative work** (what has been built so far): + - List every directory under `specs/` (skip `_delta/` if present). For each: + - Read `spec.md` (title, primary requirement, success criteria). + - Read `plan.md` if present (technical approach, scope). + - Read `tasks.md` if present and count completed vs total tasks (look for + `- [x]` vs `- [ ]` markers, or whatever convention the project uses). + - This is the **spec-side** view of cumulative work. + +3. **Inspect repo state** (what is actually in the code): + - Use `git log --oneline -n 50` to sample recent activity. + - Read top-level `README.md` and any `docs/` index for the project's + self-described capabilities. + - Walk the primary source tree (e.g., `src/`, `app/`, `lib/`, or whatever the + repo uses) at one level deep to identify shipped modules/packages. + - Read `CHANGELOG.md` if it exists β€” this is often the highest-signal source. + - This is the **reality-side** view; it catches drift between specs and code. + +4. **Compute the delta** β€” for each Long-Term Objective, classify status: + - **Met** β€” clear evidence in code + specs that the objective holds today. + - **Partial** β€” some progress, with specific gaps (name them). + - **Untouched** β€” no meaningful progress. + - **Drift** β€” code has diverged from what specs claim, or the objective has + been undermined by recent work. + + For each Partial / Untouched objective, identify the smallest concrete + feature that would move it forward. Cross-check candidates against + **Non-Goals** and drop any that violate them. + +5. **Rank candidate next features** by: + - **Leverage** β€” how many objectives the feature advances at once. + - **Unblocking** β€” does it unlock other roadmap items? + - **Cost/risk** β€” rough size, and whether it sits in a fragile area. + - **Sequencing** β€” does it depend on something not yet shipped? + + Pick the top 1 as the **primary recommendation** and keep up to 2 alternates. + If the user supplied a focus hint in `$ARGUMENTS`, bias ranking accordingly + and note the bias in the report. + +6. **Write the delta report** to `.specify/memory/delta.md` (overwrite). Use this + structure exactly: + + ```markdown + # Delta Report + + **Generated**: YYYY-MM-DD + **Constitution Version**: + **Focus Hint**: + + ## Vision Snapshot + - **North Star**: + - **Long-Term Objectives**: + + ## Cumulative Build + + + ## Objective Status + | Objective | Status | Evidence | Gap | + |-----------|--------|----------|-----| + | | Met / Partial / Untouched / Drift | | | + ... + + ## Recommended Next Feature + **Title**: + **Why now**: + **Scope sketch**: <2–4 bullets β€” enough to feed into `__SPECKIT_COMMAND_SPECIFY__`> + **Non-Goals respected**: + **Estimated size**: S / M / L + **Dependencies**: + + ## Alternates + 1. β€” <one-line rationale> + 2. <Title> β€” <one-line rationale> + + ## Drift & Risks + <Anything noticed during inspection that does NOT belong in the next feature + but should be flagged: spec/code divergence, Non-Goal violations in flight, + stalled features.> + ``` + +7. **Report completion** to the user with: + - Path: `.specify/memory/delta.md` + - One-line summary of the recommended next feature. + - Count of objectives at each status (Met / Partial / Untouched / Drift). + - Suggested next command: `__SPECKIT_COMMAND_SPECIFY__ <the recommended feature>`. + +## Constraints + +- **Read-mostly**: the only file this command writes is `.specify/memory/delta.md`. + Do not create spec directories, branches, or modify the constitution. +- **No new objectives**: if you believe the Vision itself is wrong or stale, + surface it in the *Drift & Risks* section and recommend the user run + `__SPECKIT_COMMAND_CONSTITUTION__` β€” do not silently invent objectives here. +- **Respect Non-Goals**: a recommendation that violates a stated Non-Goal is a + bug in this command, not a feature. +- **Honest "Untouched"**: do not pad evidence to make objectives look further + along than they are. The value of this command is calibration, not optimism. + +## Post-Execution Checks + +**Check for extension hooks (after delta)**: +Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.after_delta` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/templates/constitution-template.md b/templates/constitution-template.md index a4670ff469..56e829f691 100644 --- a/templates/constitution-template.md +++ b/templates/constitution-template.md @@ -1,6 +1,54 @@ # [PROJECT_NAME] Constitution <!-- Example: Spec Constitution, TaskFlow Constitution, etc. --> +## Vision & Direction +<!-- + The Vision is the stable, fixed objective for the project. It does not change + with every feature. It answers: WHY this project exists, WHO it serves, and + WHERE it is heading in the long term. Individual features added via + `__SPECKIT_COMMAND_SPECIFY__` are incremental steps toward this Vision; the + `__SPECKIT_COMMAND_DELTA__` command computes the gap between this Vision and + current repo state to suggest the next feature. + + Keep this section terse, declarative, and durable β€” amendments here should be + rare and follow the Governance procedure below. +--> + +### North Star +<!-- One or two sentences. The single, enduring purpose of the project. --> +[NORTH_STAR_STATEMENT] +<!-- Example: Make spec-driven development the default way teams ship reliable software. --> + +### Target Users & Value +<!-- Who this serves and the durable value delivered to them. --> +[TARGET_USERS_AND_VALUE] +<!-- Example: Engineering teams who want auditable, reproducible feature delivery; value = traceable path from intent to code. --> + +### Long-Term Objectives +<!-- + A small set (3–7) of durable, outcome-oriented objectives. These are NOT a + feature list β€” they describe *states the project should reach*. Each must be + observable so `__SPECKIT_COMMAND_DELTA__` can assess progress against the + current repo state. +--> +- [OBJECTIVE_1] +- [OBJECTIVE_2] +- [OBJECTIVE_3] +<!-- Example: +- Every shipped feature is traceable to a spec, a plan, and a verifiable test. +- Onboarding a new contributor to a workflow takes under 30 minutes. +- The framework supports at least three coding-agent integrations without core changes. +--> + +### Non-Goals +<!-- Explicit statements of what the project will NOT pursue, to bound scope. --> +- [NON_GOAL_1] +- [NON_GOAL_2] +<!-- Example: +- Becoming a general-purpose project management tool. +- Replacing human review in the spec β†’ implementation pipeline. +--> + ## Core Principles ### [PRINCIPLE_1_NAME] diff --git a/templates/plan-template.md b/templates/plan-template.md index 4fe6c8844b..8250319ca6 100644 --- a/templates/plan-template.md +++ b/templates/plan-template.md @@ -42,6 +42,31 @@ [Gates determined based on constitution file] +## Vision Alignment Check + +*GATE: Must pass before Phase 0 research. Independent of Constitution Check β€” +Constitution gates evaluate **principles** (HOW we build); this gates the feature +against **Vision & Direction** (WHY and WHERE we are heading).* + +Read `.specify/memory/constitution.md` β†’ `Vision & Direction` section. For each +item, record a one-line judgement: + +| Vision Element | Verdict | Notes | +|----------------|---------|-------| +| North Star β€” does this feature move toward it? | Aligned / Neutral / Misaligned | <why> | +| Target Users & Value β€” does this serve the stated users with the stated value? | Yes / Partial / No | <which user, which value> | +| Long-Term Objective(s) advanced | List the specific objective IDs/titles | <how this advances them> | +| Non-Goals β€” does this respect every stated Non-Goal? | Pass / Violation | <which non-goal, and why this still respects it> | + +**Pass criteria** (all must hold): +- At least one Long-Term Objective is materially advanced. +- No Non-Goal is violated. +- North Star verdict is Aligned (Neutral is acceptable only with explicit justification in the Complexity Tracking section). + +**On failure**: stop. Either narrow the feature scope until it passes, or run +`__SPECKIT_COMMAND_DELTA__` to identify a better-aligned next feature, or run +`__SPECKIT_COMMAND_CONSTITUTION__` if the Vision itself needs amendment. + ## Project Structure ### Documentation (this feature) diff --git a/tests/integrations/test_integration_base_markdown.py b/tests/integrations/test_integration_base_markdown.py index 19b52167a9..0750a34f7d 100644 --- a/tests/integrations/test_integration_base_markdown.py +++ b/tests/integrations/test_integration_base_markdown.py @@ -254,7 +254,7 @@ def test_init_options_includes_context_file(self, tmp_path): COMMAND_STEMS = [ "agent-context.update", - "analyze", "clarify", "constitution", "implement", + "analyze", "clarify", "constitution", "delta", "implement", "plan", "checklist", "specify", "tasks", "taskstoissues", ] diff --git a/tests/integrations/test_integration_base_skills.py b/tests/integrations/test_integration_base_skills.py index 8a3f9d0f34..b11b3aee9b 100644 --- a/tests/integrations/test_integration_base_skills.py +++ b/tests/integrations/test_integration_base_skills.py @@ -100,7 +100,7 @@ def test_skill_directory_structure(self, tmp_path): skill_files = [f for f in created if "scripts" not in f.parts] expected_commands = { - "analyze", "clarify", "constitution", "implement", + "analyze", "clarify", "constitution", "delta", "implement", "plan", "checklist", "specify", "tasks", "taskstoissues", } @@ -393,7 +393,7 @@ def test_options_include_skills_flag(self): # -- Complete file inventory ------------------------------------------ _SKILL_COMMANDS = [ - "analyze", "clarify", "constitution", "implement", + "analyze", "clarify", "constitution", "delta", "implement", "plan", "checklist", "specify", "tasks", "taskstoissues", ] diff --git a/tests/integrations/test_integration_base_toml.py b/tests/integrations/test_integration_base_toml.py index 37f6966e35..2679680576 100644 --- a/tests/integrations/test_integration_base_toml.py +++ b/tests/integrations/test_integration_base_toml.py @@ -486,6 +486,7 @@ def test_init_options_includes_context_file(self, tmp_path): "analyze", "clarify", "constitution", + "delta", "implement", "plan", "checklist", diff --git a/tests/integrations/test_integration_base_yaml.py b/tests/integrations/test_integration_base_yaml.py index 7814844c51..2d2332b72a 100644 --- a/tests/integrations/test_integration_base_yaml.py +++ b/tests/integrations/test_integration_base_yaml.py @@ -365,6 +365,7 @@ def test_init_options_includes_context_file(self, tmp_path): "analyze", "clarify", "constitution", + "delta", "implement", "plan", "checklist", diff --git a/tests/integrations/test_integration_copilot.py b/tests/integrations/test_integration_copilot.py index d5b3c1deeb..3a73bc8be0 100644 --- a/tests/integrations/test_integration_copilot.py +++ b/tests/integrations/test_integration_copilot.py @@ -125,9 +125,9 @@ def test_directory_structure(self, tmp_path): agents_dir = tmp_path / ".github" / "agents" assert agents_dir.is_dir() agent_files = sorted(agents_dir.glob("speckit.*.agent.md")) - assert len(agent_files) == 9 + assert len(agent_files) == 10 expected_commands = { - "analyze", "clarify", "constitution", "implement", + "analyze", "clarify", "constitution", "delta", "implement", "plan", "checklist", "specify", "tasks", "taskstoissues", } actual_commands = {f.name.removeprefix("speckit.").removesuffix(".agent.md") for f in agent_files} @@ -198,6 +198,7 @@ def test_complete_file_inventory_sh(self, tmp_path): ".github/agents/speckit.checklist.agent.md", ".github/agents/speckit.clarify.agent.md", ".github/agents/speckit.constitution.agent.md", + ".github/agents/speckit.delta.agent.md", ".github/agents/speckit.implement.agent.md", ".github/agents/speckit.plan.agent.md", ".github/agents/speckit.specify.agent.md", @@ -208,6 +209,7 @@ def test_complete_file_inventory_sh(self, tmp_path): ".github/prompts/speckit.checklist.prompt.md", ".github/prompts/speckit.clarify.prompt.md", ".github/prompts/speckit.constitution.prompt.md", + ".github/prompts/speckit.delta.prompt.md", ".github/prompts/speckit.implement.prompt.md", ".github/prompts/speckit.plan.prompt.md", ".github/prompts/speckit.specify.prompt.md", @@ -268,6 +270,7 @@ def test_complete_file_inventory_ps(self, tmp_path): ".github/agents/speckit.checklist.agent.md", ".github/agents/speckit.clarify.agent.md", ".github/agents/speckit.constitution.agent.md", + ".github/agents/speckit.delta.agent.md", ".github/agents/speckit.implement.agent.md", ".github/agents/speckit.plan.agent.md", ".github/agents/speckit.specify.agent.md", @@ -278,6 +281,7 @@ def test_complete_file_inventory_ps(self, tmp_path): ".github/prompts/speckit.checklist.prompt.md", ".github/prompts/speckit.clarify.prompt.md", ".github/prompts/speckit.constitution.prompt.md", + ".github/prompts/speckit.delta.prompt.md", ".github/prompts/speckit.implement.prompt.md", ".github/prompts/speckit.plan.prompt.md", ".github/prompts/speckit.specify.prompt.md", @@ -321,7 +325,7 @@ class TestCopilotSkillsMode: """Tests for Copilot integration in --skills mode.""" _SKILL_COMMANDS = [ - "analyze", "clarify", "constitution", "implement", + "analyze", "clarify", "constitution", "delta", "implement", "plan", "checklist", "specify", "tasks", "taskstoissues", ] diff --git a/tests/integrations/test_integration_generic.py b/tests/integrations/test_integration_generic.py index b7c64cdf67..f2d04000f1 100644 --- a/tests/integrations/test_integration_generic.py +++ b/tests/integrations/test_integration_generic.py @@ -306,6 +306,7 @@ def test_complete_file_inventory_sh(self, tmp_path): ".myagent/commands/speckit.checklist.md", ".myagent/commands/speckit.clarify.md", ".myagent/commands/speckit.constitution.md", + ".myagent/commands/speckit.delta.md", ".myagent/commands/speckit.implement.md", ".myagent/commands/speckit.plan.md", ".myagent/commands/speckit.specify.md", @@ -370,6 +371,7 @@ def test_complete_file_inventory_ps(self, tmp_path): ".myagent/commands/speckit.checklist.md", ".myagent/commands/speckit.clarify.md", ".myagent/commands/speckit.constitution.md", + ".myagent/commands/speckit.delta.md", ".myagent/commands/speckit.implement.md", ".myagent/commands/speckit.plan.md", ".myagent/commands/speckit.specify.md", diff --git a/tests/test_delta_and_vision.py b/tests/test_delta_and_vision.py new file mode 100644 index 0000000000..b0351533f0 --- /dev/null +++ b/tests/test_delta_and_vision.py @@ -0,0 +1,122 @@ +"""Tests for the /delta command and the Vision & Direction section. + +The delta command itself is markdown executed by an LLM; these tests cover the +*scaffolding* contracts that the rest of the system relies on: + +- delta is a recognized core command (registered alongside specify/plan/tasks). +- The delta template file ships under templates/commands/ and respects its + read-mostly contract (single canonical write target). +- The constitution template carries the Vision & Direction section with the + subsections the /constitution command is instructed to populate. +- The plan template carries a Vision Alignment Check gate that references the + Vision section. +""" + +from pathlib import Path + +from specify_cli import SKILL_DESCRIPTIONS +from specify_cli.extensions import _FALLBACK_CORE_COMMAND_NAMES +from specify_cli.integrations.claude import ARGUMENT_HINTS + +REPO_ROOT = Path(__file__).resolve().parent.parent +TEMPLATES = REPO_ROOT / "templates" + + +class TestDeltaCommandRegistration: + """The /delta command must be wired into every place core commands are listed.""" + + def test_delta_in_skill_descriptions(self): + assert "delta" in SKILL_DESCRIPTIONS + # Description should mention the two things /delta uniquely promises: + # Vision and a recommendation/next-feature direction. + desc = SKILL_DESCRIPTIONS["delta"].lower() + assert "vision" in desc + assert "next" in desc or "recommend" in desc + + def test_delta_in_core_command_fallback(self): + """Extensions must not be allowed to shadow /delta.""" + assert "delta" in _FALLBACK_CORE_COMMAND_NAMES + + def test_delta_has_claude_argument_hint(self): + assert "delta" in ARGUMENT_HINTS + assert ARGUMENT_HINTS["delta"].strip() != "" + + def test_delta_template_file_exists(self): + assert (TEMPLATES / "commands" / "delta.md").is_file() + + +class TestDeltaTemplateContract: + """Read-only audit of templates/commands/delta.md's behavior contract.""" + + def setup_method(self): + self.content = (TEMPLATES / "commands" / "delta.md").read_text() + + def test_writes_to_canonical_memory_path(self): + # The report has exactly one canonical write location. + assert ".specify/memory/delta.md" in self.content + + def test_does_not_create_spec_directories(self): + # /delta is a recommender, not a creator. It must explicitly disclaim + # spec/branch creation so future edits don't drift into that territory. + lowered = self.content.lower() + assert "do not create spec" in lowered or "do **not** create spec" in lowered + + def test_has_frontmatter_and_handoff_to_specify(self): + # YAML frontmatter present. + assert self.content.startswith("---") + # Recommends the natural next command. + assert "speckit.specify" in self.content + + def test_references_constitution_and_specs(self): + # Both inputs must be named in the instructions. + assert ".specify/memory/constitution.md" in self.content + assert "specs/" in self.content + + +class TestConstitutionVisionSection: + """The constitution template must expose the Vision & Direction structure.""" + + def setup_method(self): + self.content = (TEMPLATES / "constitution-template.md").read_text() + + def test_vision_section_present(self): + assert "## Vision & Direction" in self.content + + def test_vision_above_core_principles(self): + vision_idx = self.content.index("## Vision & Direction") + principles_idx = self.content.index("## Core Principles") + assert vision_idx < principles_idx, ( + "Vision & Direction must precede Core Principles so it reads as the " + "fixed objective above the principles that govern HOW." + ) + + def test_required_subsections_present(self): + for heading in ( + "### North Star", + "### Target Users & Value", + "### Long-Term Objectives", + "### Non-Goals", + ): + assert heading in self.content, f"missing subsection: {heading}" + + +class TestPlanTemplateVisionGate: + """The plan template must gate on Vision alignment, not just Constitution.""" + + def setup_method(self): + self.content = (TEMPLATES / "plan-template.md").read_text() + + def test_vision_alignment_gate_present(self): + assert "## Vision Alignment Check" in self.content + + def test_vision_gate_after_constitution_gate(self): + # Order matters: Constitution Check stays first (existing contract), + # Vision Alignment Check is the new sibling gate that follows it. + const_idx = self.content.index("## Constitution Check") + vision_idx = self.content.index("## Vision Alignment Check") + assert const_idx < vision_idx + + def test_vision_gate_references_delta_and_constitution_commands(self): + # Failure path should route the user to /delta or /constitution. + assert "__SPECKIT_COMMAND_DELTA__" in self.content + assert "__SPECKIT_COMMAND_CONSTITUTION__" in self.content