diff --git a/openspec/changes/3456-the-plan-check-refuses-two-tasks/.openspec.yaml b/openspec/changes/3456-the-plan-check-refuses-two-tasks/.openspec.yaml new file mode 100644 index 000000000..4ccb2b41a --- /dev/null +++ b/openspec/changes/3456-the-plan-check-refuses-two-tasks/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-11-04 diff --git a/openspec/changes/3456-the-plan-check-refuses-two-tasks/design.md b/openspec/changes/3456-the-plan-check-refuses-two-tasks/design.md new file mode 100644 index 000000000..76e80866a --- /dev/null +++ b/openspec/changes/3456-the-plan-check-refuses-two-tasks/design.md @@ -0,0 +1,18 @@ +# Design + +## Decisions + +- **Put the rule in `content_problems` in `src/devops_cli/roadmap/stage_change.py`, beside the placeholder and message-file rules.**: `check_proposal` calls `content_problems`, and the replay of `stage_eval_gates` calls `check_proposal`, so one rule set judges the job and the replay. +- **Compare the work text that each task states, and mirror the order of `_task_texts`: the test task, the implementation task and the documentation task of a group.**: The number of a task in `tasks.md` comes from that order, so the problem names the numbers that the file shows. +- **Normalize each work text with `" ".join(text.split()).casefold()` before the comparison.**: The model can change spacing or case without changing the work, and the check must still refuse it. +- **Keep the `Verification:` line, the `process` check and the fixed `uv run devops docs generate --sync-readme` suffix out of the comparison.**: Two tasks may share a command and stay different tasks, and a documentation task repeats a work text only when its paragraph is the same. +- **Format the group phrase from the catalog, with one entry for one group and one for two groups, and one entry for the problem itself.**: The single-group case reads as `group 2` and the two-group case as `groups 2 and 4`, and no user-facing English sits in the code. +- **Add the tests to `tests/roadmap/test_roadmap_stage_content.py`.**: That module already tests the content rules of `check_proposal`, so the new rule belongs with them. + +## Rejected alternatives + +- **Parse the rendered `tasks.md` and compare its task lines.**: `_task_texts` in `src/devops_cli/roadmap/stage_change.py` already renders the tasks from the group fields, so a second parse can drift from the renderer. +- **Compare the whole rendered task line, `Verification:` line and docs command included.**: The `_task_texts` code appends the fixed `REGENERATE_DOCUMENTS` command and the `verify` command to a task, so equal work would look different and a duplicate would pass. +- **Refuse two tasks that share a verification command.**: The acceptance criteria of #3456 require the check to accept group 3 task 3.1 and task 3.2 that differ in work text and share a `Verification:` command. +- **Add a fuzzy match of task text.**: A fuzzy match refuses distinct tasks, and `src/devops_cli/roadmap/stage_change.py` compares text with no such dependency today. +- **Hard-code the problem text in `stage_change.py`.**: `src/devops_cli/lang/en/messages.py` holds every `stage_problem_*` entry that `content_problems` formats, so the text belongs there. diff --git a/openspec/changes/3456-the-plan-check-refuses-two-tasks/proposal.md b/openspec/changes/3456-the-plan-check-refuses-two-tasks/proposal.md new file mode 100644 index 000000000..3f6975250 --- /dev/null +++ b/openspec/changes/3456-the-plan-check-refuses-two-tasks/proposal.md @@ -0,0 +1,48 @@ +# Proposal + +## Why + +The plan check of `devops roadmap stage` tests the form, the paths, the criteria and the content of a plan, but it never compares two tasks of that plan (#3456). The plan of #3417 held task 3.1 and task 3.2 that added the same paragraph to `docs/roadmap/implement.md`, and every check accepted it. A step runs one task group with a model and a token budget, so a duplicate task can make the model repeat an edit, spend the group on no change, or stop the group when the second edit writes no file. + +## What Changes + +- `check_proposal` refuses two tasks whose normalized work text is the same, and names both task numbers and the group or groups. +- The duplicate check reads the work text of a task only: `test`, `implementation` and `documentation`. A shared `Verification:` line, a `process` check and the fixed `uv run devops docs generate --sync-readme` suffix never make a duplicate. +- The work text of each task is collapsed to one line and casefolded before it is compared. +- A new message in `src/devops_cli/lang/en/messages.py` carries the duplicate-task problem, with one phrase for a group and one for two groups. +- `tests/roadmap/test_roadmap_stage_content.py` gains a test for a duplicate in one group, a test for a duplicate across two groups, a test for two tasks that share a verification command, a test for a documentation task that repeats a work text, and a test for work text that differs only in spacing and case. +- The change carries a spec delta on `roadmap-stage`, because the plan check is behavior that a consumer observes. +- The change fits one pull request. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `roadmap-stage`: Two tasks of a plan state different work + +## Impact + +- Code: `src/devops_cli/roadmap/stage_change.py`, `src/devops_cli/lang/en/messages.py` +- Tests: `tests/roadmap/test_roadmap_stage_content.py` + +## Feasibility + +- The probes confirm the premise: `The path `src/devops_cli/roadmap/stage_change.py` is a file of the staging commit.` and `The path `tests/roadmap/test_roadmap_stage_content.py` is a file of the staging commit.` The code of `src/devops_cli/roadmap/stage_change.py` in the context holds `def check_proposal(` and `def _task_texts(group: StageGroup) -> list[str]:`, so the rule has a place to live and the task order has one renderer. +- The probes confirm the message catalog: `The path `src/devops_cli/lang/en/messages.py` is a file of the staging commit.` That file holds the `stage_problem_*` entries that `content_problems` formats, so the new problem joins them. +- The probes confirm that the task text is already parsed and rendered: `The path `src/devops_cli/roadmap/step_files.py` is a file of the staging commit.` and `_task_texts` renders the tasks of a group from `test`, `implementation`, `documentation` and `process`, so the check needs no parser. +- First check of the real system: `uv run devops ci test tests/roadmap/test_roadmap_stage_content.py` on the branch tip runs the module as it stands and shows the existing content tests pass, before any test of this change is written. +- Second check of the real system: after the new test is written and before the rule, the same command `uv run devops ci test tests/roadmap/test_roadmap_stage_content.py` fails on the new duplicate-task test and passes on the rest. +- Third check: `uv run devops ci test tests/roadmap/` runs the roadmap stage and replay modules that call `check_proposal`, so a new problem on a plan that no fixture duplicates shows up there. +- Fourth check: `uv run devops ci` on the pull request runs the static checks, the type check and the full suite over the new rule. + +## Existing Tool Considered + +`TaskGroup.tasks` in `src/devops_cli/roadmap/step_files.py` and the renderer `_task_texts` in `src/devops_cli/roadmap/stage_change.py` already hold and order the task text of a group, so the check compares fields that a parser gives it and adds no parser. The cost is about 500 tokens to build and test, and about 15 minutes elapsed. + +## Dependencies + +None. diff --git a/openspec/changes/3456-the-plan-check-refuses-two-tasks/specs/roadmap-stage/spec.md b/openspec/changes/3456-the-plan-check-refuses-two-tasks/specs/roadmap-stage/spec.md new file mode 100644 index 000000000..d18a60b3e --- /dev/null +++ b/openspec/changes/3456-the-plan-check-refuses-two-tasks/specs/roadmap-stage/spec.md @@ -0,0 +1,24 @@ +## ADDED Requirements + +### Requirement: Two tasks of a plan state different work +`check_proposal` SHALL refuse two tasks whose normalized work text is the same, and SHALL name both task numbers and the group or groups of those tasks. + +#### Scenario: Duplicate task in one group +- **WHEN** group 2 holds task 2.1 and task 2.3 with the same normalized work text +- **THEN** `check_proposal` returns a problem that names task 2.1, task 2.3 and group 2 + +#### Scenario: Duplicate task across two groups +- **WHEN** group 2 task 2.1 and group 4 task 4.2 hold the same normalized work text +- **THEN** `check_proposal` returns a problem that names task 2.1, task 4.2, group 2 and group 4 + +#### Scenario: Two tasks share a verification command +- **WHEN** group 3 task 3.1 and task 3.2 hold different work text and the same `Verification:` command +- **THEN** `check_proposal` returns no duplicate-task problem for task 3.1 and task 3.2 + +#### Scenario: A documentation task repeats an implementation task +- **WHEN** an `other` group 3 holds task 3.1 that adds a paragraph and task 3.2 that adds the same paragraph with the docs command +- **THEN** `check_proposal` returns a duplicate-task problem that names task 3.1 and task 3.2 + +#### Scenario: Work text that differs only in spacing and case +- **WHEN** group 1 task 1.1 and task 1.2 hold the same words with different runs of spaces and different case +- **THEN** `check_proposal` returns a duplicate-task problem for task 1.1 and task 1.2 diff --git a/openspec/changes/3456-the-plan-check-refuses-two-tasks/tasks.md b/openspec/changes/3456-the-plan-check-refuses-two-tasks/tasks.md new file mode 100644 index 000000000..09689e45e --- /dev/null +++ b/openspec/changes/3456-the-plan-check-refuses-two-tasks/tasks.md @@ -0,0 +1,14 @@ +# Tasks + +## 1. Refuse two plan tasks that state the same work + +Reads: `src/devops_cli/roadmap/stage_change.py`, `src/devops_cli/roadmap/step_files.py`, `tests/roadmap/test_roadmap_stage_content.py`, `tests/support/roadmap_stage.py`. Changes: `src/devops_cli/roadmap/stage_change.py`, `src/devops_cli/lang/en/messages.py`, `tests/roadmap/test_roadmap_stage_content.py`. + +- [ ] 1.1 Add to `tests/roadmap/test_roadmap_stage_content.py` a test for a duplicate in one group, a test for a duplicate across two groups, a test for two tasks that share a `Verification:` command, a test for a documentation task that repeats a work text, and a test for work text that differs only in spacing and case; each test builds a `StageProposal` and calls `check_proposal` + - Verification: `uv run devops ci test tests/roadmap/test_roadmap_stage_content.py` fails on the base +- [ ] 1.2 Add `stage_problem_duplicate_task`, `stage_group_one` and `stage_group_two` to the roadmap messages of `src/devops_cli/lang/en/messages.py`. Add a helper in `src/devops_cli/roadmap/stage_change.py` that returns the work text of each task of a group in the order of `_task_texts`: the test, the implementation and the documentation. Normalize each text with `" ".join(text.split()).casefold()`, compare every task of the plan with every later task, and append the formatted problem to `content_problems`. Name both task numbers and the group or groups, and update the docstring of `content_problems` + - Verification: `uv run devops ci test tests/roadmap/test_roadmap_stage_content.py` passes + +## Workflow follow-up + +- Pending a person: review the diff in tuicr (tuicr -r ..HEAD).