mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
feat(presets): add opt-in constitution-sync preset (#3873)
* feat(presets): add opt-in constitution-sync preset Follow-up to #3790, which removed the consistency-propagation pass from the core /constitution command in favor of runtime resolution. Teams that treat materialized plan/spec/tasks templates as reviewed, committed artifacts lost the auto-sync of amended constitutional guidance on a non-forced upgrade. Add a bundled, opt-in `constitution-sync` preset that restores that behavior via a wrap-strategy override of speckit.constitution (composes on {CORE_TEMPLATE} so it stays forward-compatible). It only writes into the project's own .specify/templates scaffolds and installed command files, never into stack-owned template layers. - presets/constitution-sync/: preset.yml (requires >=0.14.4), wrap command, README documenting the tension between auto-propagation and the resolution stack - presets/catalog.json: bundled entry - docs/upgrade.md: document the 0.14.4 behavior change and the opt-in - tests/test_presets.py: structural + composition coverage (TestConstitutionSyncPreset) Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afa7c1d2-147b-4f62-a6fc-a2cc824cfa3e * fix(presets): ship constitution-sync in wheel, clarify scope guard, assert composition Address review feedback on #3873: - pyproject.toml: force-include presets/constitution-sync into the wheel's core_pack so `_locate_bundled_preset` resolves it in a released install; the bundled advertisement was otherwise unshippable. - tests/contract/test_wheel_bundled_presets.py: new contract test asserting every bundled preset in presets/catalog.json is force-included (guards lean too). - commands/speckit.constitution.md: explicitly state the propagation section supersedes the core Scope Guard, which otherwise says dependent templates are not modified here. - tests/test_presets.py: assert resolve_content substitutes {CORE_TEMPLATE} and the effective command embeds both the core body and the sync pass. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afa7c1d2-147b-4f62-a6fc-a2cc824cfa3e * test(presets): parse frontmatter as YAML in constitution-sync wrapper test Address review feedback on #3873: assert `strategy: wrap` structurally by parsing the Markdown frontmatter as YAML (instead of a substring match that could false-positive on body text), and assert {CORE_TEMPLATE} in the body section only. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afa7c1d2-147b-4f62-a6fc-a2cc824cfa3e * docs(presets): reframe constitution-sync README around behavior and caveats Rework the preset's user-facing docs to describe what it does, what it does not do, and the caveats you take on — rather than leading with version/origin history. The preset stack is the project's forward direction, so the README no longer positions this as "restoring pre-0.14.4 behavior." Also make the edit-in-place vs. composition conflict explicit and consistent across the wrapper command and docs: propagation into command files/templates that are provided or wrapped by a preset/extension is clobbered on stack reconciliation (integration use/upgrade, preset/extension install/remove), so the wrapper restricts propagation to project-local artifacts the team owns. - README: forward-looking "What it does / does not do / When to use / Caveats" - speckit.constitution.md: step 4 no longer hand-edits composed command files; closing caveat covers command files too - docs/upgrade.md: note the composition-model conflict in the opt-in section - tests: assert the updated closing-caveat wording Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afa7c1d2-147b-4f62-a6fc-a2cc824cfa3e * docs(presets): tweak constitution-sync README default-behavior wording Phrase the default-behavior note as "the current version of Spec Kit" and rewrap the opening paragraph. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afa7c1d2-147b-4f62-a6fc-a2cc824cfa3e * docs(presets): keep emphasis spans on one line in constitution-sync README Avoid **bold** spans broken across soft line breaks (runtime resolution, reviewed committed artifacts) so they render consistently. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afa7c1d2-147b-4f62-a6fc-a2cc824cfa3e * docs(presets): refocus constitution-sync README on what it restores Reframe the intro around what the preset restores and what the user opts into, rather than describing current Spec Kit default behavior. Be honest that propagation was removed deliberately (duplicates the source of truth, fights composition) and this preset knowingly reintroduces it and its tradeoffs. Minor flow fixes (comma splice, terse bullet). Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afa7c1d2-147b-4f62-a6fc-a2cc824cfa3e * docs(upgrade): de-pin version from /constitution behavior-change section The upgrade guide always describes the latest version, so hard-pinning "0.14.4" in the heading and "Starting in 0.14.4" in the body added no value. Keep the #3790 provenance link and the "no longer propagates" framing; the machine-readable version gate stays in preset.yml. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afa7c1d2-147b-4f62-a6fc-a2cc824cfa3e * docs(upgrade): clarify non-breaking nuance and presets direction Note that the /constitution scope change is only noticeable if you relied on the old edit-in-place behavior, and add the forward-looking framing: presets and extensions — not in-place file edits — are how Spec Kit now governs, versions, and audits shared assets across repositories. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afa7c1d2-147b-4f62-a6fc-a2cc824cfa3e --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afa7c1d2-147b-4f62-a6fc-a2cc824cfa3e
This commit is contained in:
@@ -208,6 +208,74 @@ Restart your IDE to refresh the command list.
|
||||
|
||||
---
|
||||
|
||||
## Behavior change: `/constitution` no longer propagates into templates
|
||||
|
||||
The `/constitution` command ([#3790](https://github.com/github/spec-kit/pull/3790)) is scoped to
|
||||
its own artifact. It updates
|
||||
`.specify/memory/constitution.md` and writes a Sync Impact Report, and **no longer edits**
|
||||
`plan-template.md`, `spec-template.md`, `tasks-template.md`, installed command files, or
|
||||
guidance docs.
|
||||
|
||||
### Why
|
||||
|
||||
Spec Kit uses **runtime resolution**: `plan`, `tasks`, and `analyze` read
|
||||
`.specify/memory/constitution.md` live on every run, and `analyze` is the dedicated drift
|
||||
checker. The governed templates carry a pointer, not a copy — `plan-template.md` ships
|
||||
`[Gates determined based on constitution file]`, and `/plan` fills that section from the live
|
||||
constitution each run. Propagation duplicated the single source of truth and fought the
|
||||
preset/override composition system (a `replace` preset shadows an edited core template).
|
||||
|
||||
More broadly, presets and extensions — not in-place file edits — are how Spec Kit now governs
|
||||
shared assets. Composing policy through the resolution stack keeps it centrally owned, versioned,
|
||||
and auditable across repositories, instead of frozen into per-repo copies no core team can see.
|
||||
|
||||
### Is this a breaking change for existing projects?
|
||||
|
||||
**No — your workflow keeps working.** You would only notice a difference if you relied on
|
||||
`/constitution` editing those files in place. The templates are scaffolds, not authorities. When you
|
||||
run `/plan`, it copies the template into a per-feature `plan.md` and re-derives the Constitution
|
||||
Check from the live constitution; `/analyze` validates against it. Even if a previous
|
||||
`/constitution` run materialized concrete gate text into `.specify/templates/plan-template.md`,
|
||||
the live constitution remains the source of truth at runtime.
|
||||
|
||||
On a **non-forced upgrade**, a materialized template is *preserved* (its hash diverges from the
|
||||
recorded managed copy, so the refresh treats it as a customization and does not overwrite it).
|
||||
Nothing regresses.
|
||||
|
||||
### Optional cleanup — return to the runtime pointer
|
||||
|
||||
A frozen, pre-filled Constitution Check is a slightly misleading scaffold and can bias the first
|
||||
`/plan` pass. To move fully back to runtime resolution, reset the section body in
|
||||
`.specify/templates/plan-template.md` to the pointer:
|
||||
|
||||
```text
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
[Gates determined based on constitution file]
|
||||
```
|
||||
|
||||
Leave the rest of the file untouched. This is cleanup, not a required migration.
|
||||
|
||||
### Keeping the old behavior (opt-in)
|
||||
|
||||
If your team treats the materialized templates as **reviewed, committed artifacts** and wants
|
||||
`/constitution` to keep propagating, install the bundled **`constitution-sync`** preset:
|
||||
|
||||
```bash
|
||||
specify preset add constitution-sync
|
||||
```
|
||||
|
||||
It wraps the core `/constitution` command and re-adds the propagation pass. It does **not** edit
|
||||
versioned preset- or extension-provided templates or command files (those are owned by their
|
||||
packages and are recomposed on reconciliation). Note that this edit-in-place propagation model
|
||||
conflicts with the composition model used by the rest of the SDD commands when they are
|
||||
preset/extension-managed — see the "Interaction with the resolution stack" section in
|
||||
`presets/constitution-sync/README.md` for the tradeoffs and when to prefer the default instead.
|
||||
|
||||
---
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
### Scenario 1: "I just want new slash commands"
|
||||
|
||||
@@ -25,6 +25,29 @@
|
||||
"workflow",
|
||||
"core"
|
||||
]
|
||||
},
|
||||
"constitution-sync": {
|
||||
"name": "Constitution Template Sync",
|
||||
"id": "constitution-sync",
|
||||
"version": "1.0.0",
|
||||
"description": "Opt-in: restores /constitution propagation of amended guidance into plan/spec/tasks templates and installed command files, for teams that treat materialized templates as reviewed artifacts.",
|
||||
"author": "github",
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"license": "MIT",
|
||||
"bundled": true,
|
||||
"requires": {
|
||||
"speckit_version": ">=0.14.4"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 1,
|
||||
"templates": 0
|
||||
},
|
||||
"tags": [
|
||||
"constitution",
|
||||
"governance",
|
||||
"templates",
|
||||
"compatibility"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
126
presets/constitution-sync/README.md
Normal file
126
presets/constitution-sync/README.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Constitution Template Sync
|
||||
|
||||
An **opt-in** preset that restores `/constitution`'s ability to propagate amended guidance into your
|
||||
project's own templates and command files. After you update the constitution, it aligns
|
||||
`plan-template.md`, `spec-template.md`, `tasks-template.md`, project-local command files, and
|
||||
guidance docs so they reflect the current principles.
|
||||
|
||||
This propagation used to be built into `/constitution`; it was dropped when the command moved to the
|
||||
preset model. Installing this preset opts you back into it: you get the guidance materialized into
|
||||
reviewed, committed artifacts instead of relying on runtime resolution alone.
|
||||
|
||||
> **What you're opting into.** Propagation was removed deliberately — it duplicates the constitution
|
||||
> as the source of truth and can fight the composition stack (materialized edits get shadowed or
|
||||
> clobbered on the next recompose). This preset knowingly **reintroduces** that behavior, and those
|
||||
> tradeoffs, for teams that want it. Read the [caveats](#caveats-you-take-on) before installing.
|
||||
|
||||
For most projects the default composable stack is the **recommended** approach, and at organization
|
||||
scale it is usually the stronger governance model. Runtime resolution keeps the live constitution as
|
||||
the single source of truth (nothing to re-sync, so nothing drifts), and the stack composes the
|
||||
**entire** Spec Kit ecosystem — not just the SDD commands, but every command, template, script and
|
||||
extension — with explicit priority levels, strategies, and independent versioning. It is a
|
||||
capability, not automatic governance: a core team authors its own organizational presets and
|
||||
extensions, then owns, versions, and audits that policy in one place and rolls it across many
|
||||
repositories, instead of scattering frozen, per-repo copies no central team can see. This preset is
|
||||
a supported escape hatch for teams whose workflow depends on reviewing materialized artifacts
|
||||
directly — useful as a bridge, though for org-wide policy the better long-term path is usually a
|
||||
versioned preset a core team maintains.
|
||||
|
||||
## What it does
|
||||
|
||||
Ships a single `wrap`-strategy override of `speckit.constitution`. It composes on top of the
|
||||
current core command (via `{CORE_TEMPLATE}`), so it stays forward-compatible with core changes, and
|
||||
appends a propagation pass that, after the constitution is written:
|
||||
|
||||
- Aligns `plan/spec/tasks-template.md` in `.specify/templates/` with the updated principles.
|
||||
- Updates **project-local** command files and guidance docs to correct stale references.
|
||||
- Extends the Sync Impact Report in `.specify/memory/constitution.md` with the files it touched.
|
||||
|
||||
## What it does not do
|
||||
|
||||
- It does **not** change behavior for anyone who does not install it — the default runtime
|
||||
resolution model is untouched.
|
||||
- It does **not** disable runtime resolution. `plan`, `tasks`, and `analyze` still read the live
|
||||
constitution every run; this preset adds materialized copies on top — it does not replace the
|
||||
source of truth.
|
||||
- It does **not** edit versioned, package-owned files — templates or command files provided or
|
||||
wrapped by another preset or extension. Those are recomposed from the resolution stack, so it
|
||||
only ever writes into your project's own `.specify/templates/` scaffolds and command files that
|
||||
are not managed by a preset/extension.
|
||||
|
||||
## When to use it
|
||||
|
||||
Install it **only** if your team treats the materialized templates and commands as
|
||||
**reviewed, committed artifacts** — for example, if `plan-template.md`'s Constitution Check is
|
||||
read in PRs as "here are our current gates" and is expected to track the constitution.
|
||||
|
||||
If you rely on the default runtime-resolution model, you do **not** need this preset: the live
|
||||
constitution is already the single source of truth and there is nothing to sync.
|
||||
|
||||
## Caveats you take on
|
||||
|
||||
The preset resolution stack is how Spec Kit composes templates and commands going forward: they are
|
||||
**layered, package-owned artifacts recomposed on demand**, not frozen files you edit in place.
|
||||
Propagation is the opposite idea — it **materializes** guidance into files and freezes it. That
|
||||
tension is the main thing to understand before installing:
|
||||
|
||||
- **Materialized copies can drift.** Anything propagated is a snapshot; if you amend the
|
||||
constitution and do not re-run `/constitution`, the copies fall out of sync. The default runtime
|
||||
model has no drift because it reads the live constitution every run.
|
||||
|
||||
- **Edits to composed files do not survive reconciliation.** If the rest of your SDD flow is
|
||||
preset/extension-managed, the commands it materializes (`speckit.plan`, `speckit.specify`,
|
||||
`speckit.tasks`, `speckit.analyze`, `speckit.implement`, …) are recomputed from the stack. Any
|
||||
guidance propagated into them is clobbered the next time the stack reconciles — on
|
||||
`specify integration use <key>` / `switch`, `specify integration upgrade`, or any preset/extension
|
||||
install or remove. The same applies to templates owned by another preset/extension. This is why
|
||||
the preset restricts itself to project-local files; propagation is reliable **only** for
|
||||
artifacts you own outright.
|
||||
|
||||
- **A pre-filled Constitution Check can bias `/plan`.** Materializing concrete gates into
|
||||
`plan-template.md` replaces the runtime pointer, so the first `/plan` pass may anchor on the
|
||||
frozen text. Keep the pointer unless you specifically want committed gates.
|
||||
|
||||
**Bottom line:** this preset fits projects whose governed templates and commands are project-local
|
||||
artifacts they review, with the rest of the SDD flow on the plain bundled core. If your
|
||||
`plan`/`specify`/`tasks`/`analyze` commands or templates come from other presets or extensions,
|
||||
prefer the default runtime-resolution model.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# constitution-sync is a bundled preset — no download needed
|
||||
specify preset add constitution-sync
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Test from local directory
|
||||
specify preset add --dev ./presets/constitution-sync
|
||||
|
||||
# Verify the wrapped command resolves
|
||||
specify preset resolve speckit.constitution
|
||||
|
||||
# Remove when done
|
||||
specify preset remove constitution-sync
|
||||
```
|
||||
|
||||
## Migrating back to the default
|
||||
|
||||
To move back to runtime resolution, reset each materialized `## Constitution Check` section in
|
||||
`.specify/templates/plan-template.md` to the pointer:
|
||||
|
||||
```text
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
[Gates determined based on constitution file]
|
||||
```
|
||||
|
||||
Then remove this preset. See `docs/upgrade.md` for details.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
54
presets/constitution-sync/commands/speckit.constitution.md
Normal file
54
presets/constitution-sync/commands/speckit.constitution.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
description: Create or update the project constitution, then propagate the amended guidance into dependent templates and installed command files (opt-in template sync).
|
||||
strategy: wrap
|
||||
handoffs:
|
||||
- label: Build Specification
|
||||
agent: speckit.specify
|
||||
prompt: Implement the feature specification based on the updated constitution. I want to build...
|
||||
---
|
||||
|
||||
{CORE_TEMPLATE}
|
||||
|
||||
## Constitution Template Sync
|
||||
|
||||
> **This section supersedes the "Scope Guard" above for template and command propagation.**
|
||||
> The core Scope Guard states that dependent templates and commands "are not modified here" —
|
||||
> that restriction is intentionally lifted by this preset. Propagating amended guidance into the
|
||||
> project's own dependent scaffolds and installed command files (as described below) **is in
|
||||
> scope** whenever this preset is installed. The Scope Guard's other constraints still apply:
|
||||
> do not implement features, generate application code, or touch artifacts unrelated to the
|
||||
> constitution/template workflow.
|
||||
|
||||
After you have written the updated constitution above, perform a consistency propagation pass
|
||||
so the dependent artifacts reflect the amended principles:
|
||||
|
||||
1. Read `.specify/templates/plan-template.md` and ensure any "Constitution Check" or rules align
|
||||
with the updated principles. Only materialize concrete gate text here if your team intends to
|
||||
review it as committed content; otherwise leave the runtime pointer
|
||||
`[Gates determined based on constitution file]` in place so `/plan` fills it from the live
|
||||
constitution.
|
||||
2. Read `.specify/templates/spec-template.md` for scope/requirements alignment — update if the
|
||||
constitution adds/removes mandatory sections or constraints.
|
||||
3. Read `.specify/templates/tasks-template.md` and ensure task categorization reflects new or
|
||||
removed principle-driven task types (e.g., observability, versioning, testing discipline).
|
||||
4. Read each installed Spec Kit command file for your agent (including this one) — named
|
||||
`speckit.*` or `speckit-*` (dot or hyphen depending on the agent), or laid out as
|
||||
`speckit-<name>/SKILL.md` for skills-based integrations, e.g. in `.github/agents/`,
|
||||
`.github/skills/`, `.claude/skills/`, or your agent's equivalent commands directory — to verify
|
||||
no outdated references (CLAUDE-only or other agent-specific names) remain when generic guidance
|
||||
is required. **Only hand-edit a command file if it is a project-local file not managed by a
|
||||
preset or extension.** Command files that are composed from the resolution stack (anything
|
||||
provided or wrapped by a preset/extension) must be regenerated through the stack — do **not**
|
||||
edit them in place, because reconciliation (`specify integration use`, `specify integration
|
||||
upgrade`, or any preset/extension install/remove) will clobber the edits.
|
||||
5. Read any runtime guidance docs (e.g., `README.md`, `docs/quickstart.md`, or agent-specific
|
||||
guidance files if present) and update references to principles that changed.
|
||||
|
||||
Then extend the Sync Impact Report at the top of `.specify/memory/constitution.md` with:
|
||||
|
||||
- Templates requiring updates (✅ updated / ⚠ pending) with file paths.
|
||||
|
||||
**Do not edit versioned preset- or extension-provided template or command files directly.** Those
|
||||
artifacts are owned by their packages and are recomposed on the package's next update or on stack
|
||||
reconciliation — hand edits are clobbered. Limit propagation to the project's own
|
||||
`.specify/templates/` scaffolds and to command files that are not managed by a preset or extension.
|
||||
30
presets/constitution-sync/preset.yml
Normal file
30
presets/constitution-sync/preset.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
preset:
|
||||
id: "constitution-sync"
|
||||
name: "Constitution Template Sync"
|
||||
version: "1.0.0"
|
||||
description: "Opt-in: restores /constitution propagation of amended guidance into plan/spec/tasks templates and installed command files, for teams that treat materialized templates as reviewed artifacts."
|
||||
author: "github"
|
||||
repository: "https://github.com/github/spec-kit"
|
||||
license: "MIT"
|
||||
|
||||
requires:
|
||||
# Requires the runtime-resolution baseline (#3790, shipped in 0.14.4) where the
|
||||
# core /constitution command no longer propagates. Installing this preset on an
|
||||
# older core would double-apply propagation.
|
||||
speckit_version: ">=0.14.4"
|
||||
|
||||
provides:
|
||||
templates:
|
||||
- type: "command"
|
||||
name: "speckit.constitution"
|
||||
file: "commands/speckit.constitution.md"
|
||||
description: "Wrap /constitution to also propagate guidance into dependent templates and command files"
|
||||
strategy: "wrap"
|
||||
|
||||
tags:
|
||||
- "constitution"
|
||||
- "governance"
|
||||
- "templates"
|
||||
- "compatibility"
|
||||
@@ -49,6 +49,7 @@ packages = ["src/specify_cli"]
|
||||
"workflows/speckit" = "specify_cli/core_pack/workflows/speckit"
|
||||
# Bundled presets (installable via `specify preset add <name>` or `specify init --preset <name>`)
|
||||
"presets/lean" = "specify_cli/core_pack/presets/lean"
|
||||
"presets/constitution-sync" = "specify_cli/core_pack/presets/constitution-sync"
|
||||
# Community bundle catalog snapshot (used for offline discovery)
|
||||
"bundles/catalog.community.json" = "specify_cli/core_pack/bundles/catalog.community.json"
|
||||
|
||||
|
||||
53
tests/contract/test_wheel_bundled_presets.py
Normal file
53
tests/contract/test_wheel_bundled_presets.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Contract tests: every bundled preset must ship inside the wheel's core_pack.
|
||||
|
||||
``specify preset add <id>`` resolves a bundled preset via
|
||||
``specify_cli._assets._locate_bundled_preset``, which checks the wheel's
|
||||
``specify_cli/core_pack/presets/<id>/`` directory first. Any preset marked
|
||||
``bundled: true`` in ``presets/catalog.json`` must therefore be force-included
|
||||
at build time; otherwise the released wheel advertises a bundled preset it does
|
||||
not actually ship, and ``specify preset add <id>`` falls through and reports the
|
||||
preset as missing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).parents[2]
|
||||
|
||||
|
||||
def _force_include() -> dict[str, str]:
|
||||
with (REPO_ROOT / "pyproject.toml").open("rb") as pyproject_file:
|
||||
pyproject = tomllib.load(pyproject_file)
|
||||
return pyproject["tool"]["hatch"]["build"]["targets"]["wheel"]["force-include"]
|
||||
|
||||
|
||||
def _bundled_preset_ids() -> list[str]:
|
||||
catalog = json.loads((REPO_ROOT / "presets" / "catalog.json").read_text())
|
||||
return sorted(
|
||||
preset_id
|
||||
for preset_id, entry in catalog["presets"].items()
|
||||
if entry.get("bundled")
|
||||
)
|
||||
|
||||
|
||||
def test_every_bundled_preset_is_force_included():
|
||||
force_include = _force_include()
|
||||
bundled = _bundled_preset_ids()
|
||||
|
||||
assert bundled, "expected at least one bundled preset in presets/catalog.json"
|
||||
for preset_id in bundled:
|
||||
assert force_include.get(f"presets/{preset_id}") == (
|
||||
f"specify_cli/core_pack/presets/{preset_id}"
|
||||
), f"bundled preset '{preset_id}' is missing from the wheel force-include list"
|
||||
|
||||
|
||||
def test_constitution_sync_is_bundled_and_shipped():
|
||||
# Explicit regression guard: constitution-sync was advertised as bundled
|
||||
# before it was added to the wheel force-include list.
|
||||
assert "constitution-sync" in _bundled_preset_ids()
|
||||
assert _force_include()["presets/constitution-sync"] == (
|
||||
"specify_cli/core_pack/presets/constitution-sync"
|
||||
)
|
||||
@@ -12493,3 +12493,86 @@ class TestInstalledPresetRichMarkup:
|
||||
assert "Composition chain" in output, output
|
||||
assert "[base]" in output, output
|
||||
assert "[append]" in output, output
|
||||
|
||||
|
||||
class TestConstitutionSyncPreset:
|
||||
"""The bundled opt-in ``constitution-sync`` preset re-adds propagation.
|
||||
|
||||
Follow-up to #3790: core ``/constitution`` no longer propagates guidance
|
||||
into templates. This preset restores that behavior for teams that treat
|
||||
materialized templates as reviewed artifacts, delivered as a ``wrap`` of
|
||||
the core command so it stays forward-compatible with core changes.
|
||||
"""
|
||||
|
||||
PRESET_DIR = Path(__file__).parent.parent / "presets" / "constitution-sync"
|
||||
|
||||
def test_manifest_provides_wrap_of_constitution(self):
|
||||
manifest = yaml.safe_load((self.PRESET_DIR / "preset.yml").read_text())
|
||||
assert manifest["preset"]["id"] == "constitution-sync"
|
||||
entries = manifest["provides"]["templates"]
|
||||
assert len(entries) == 1
|
||||
entry = entries[0]
|
||||
assert entry["type"] == "command"
|
||||
assert entry["name"] == "speckit.constitution"
|
||||
assert entry["strategy"] == "wrap"
|
||||
# Must target the post-#3790 baseline so propagation is not double-applied.
|
||||
assert manifest["requires"]["speckit_version"] == ">=0.14.4"
|
||||
|
||||
def test_wrapper_uses_core_template_and_propagates(self):
|
||||
text = (self.PRESET_DIR / "commands" / "speckit.constitution.md").read_text()
|
||||
|
||||
# Parse the Markdown frontmatter as YAML rather than substring-matching,
|
||||
# so `strategy: wrap` is asserted structurally (not as text that could
|
||||
# appear in the body) and {CORE_TEMPLATE} is asserted in the body only.
|
||||
assert text.startswith("---\n")
|
||||
_, frontmatter_block, body = text.split("---", 2)
|
||||
frontmatter = yaml.safe_load(frontmatter_block)
|
||||
assert frontmatter["strategy"] == "wrap"
|
||||
|
||||
assert "{CORE_TEMPLATE}" in body
|
||||
assert "strategy: wrap" not in body # only in frontmatter
|
||||
# The three governed scaffolds the old checklist propagated into.
|
||||
assert "plan-template.md" in body
|
||||
assert "spec-template.md" in body
|
||||
assert "tasks-template.md" in body
|
||||
# Must not mutate versioned preset/extension artifacts.
|
||||
assert "Do not edit versioned preset- or extension-provided template or command files" in body
|
||||
|
||||
def test_catalog_lists_bundled_preset(self):
|
||||
manifest = yaml.safe_load((self.PRESET_DIR / "preset.yml").read_text())
|
||||
catalog = json.loads((self.PRESET_DIR.parent / "catalog.json").read_text())
|
||||
entry = catalog["presets"]["constitution-sync"]
|
||||
assert entry["bundled"] is True
|
||||
assert entry["version"] == manifest["preset"]["version"]
|
||||
assert entry["provides"]["commands"] == 1
|
||||
assert entry["provides"]["templates"] == 0
|
||||
|
||||
def test_wrap_composes_over_core_constitution(self, project_dir):
|
||||
"""Installing the preset yields a wrap layer atop the bundled core."""
|
||||
manager = PresetManager(project_dir)
|
||||
manager.install_from_directory(self.PRESET_DIR, "0.15.0")
|
||||
|
||||
resolver = PresetResolver(project_dir)
|
||||
layers = resolver.collect_all_layers("speckit.constitution", "command")
|
||||
assert len(layers) >= 2, "expected preset wrap layer plus a core base"
|
||||
assert layers[0]["strategy"] == "wrap"
|
||||
assert any("constitution-sync" in str(layer["path"]) for layer in layers)
|
||||
assert layers[-1]["source"] == "core (bundled)"
|
||||
|
||||
def test_resolved_content_embeds_core_and_sync_pass(self, project_dir):
|
||||
"""resolve_content substitutes {CORE_TEMPLATE} so the effective command
|
||||
contains both the bundled core body and the propagation pass."""
|
||||
manager = PresetManager(project_dir)
|
||||
manager.install_from_directory(self.PRESET_DIR, "0.15.0")
|
||||
|
||||
resolver = PresetResolver(project_dir)
|
||||
content = resolver.resolve_content("speckit.constitution", "command")
|
||||
assert content is not None
|
||||
# {CORE_TEMPLATE} must be replaced, not left literal.
|
||||
assert "{CORE_TEMPLATE}" not in content
|
||||
# Core body is present (distinctive core-only heading).
|
||||
assert "## Scope Guard" in content
|
||||
# The wrapper's propagation pass is present and supersedes the guard.
|
||||
assert "## Constitution Template Sync" in content
|
||||
assert "supersedes the \"Scope Guard\" above" in content
|
||||
assert "plan-template.md" in content
|
||||
|
||||
Reference in New Issue
Block a user