claude-dev-env 1.65.1 → 1.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/agents/plan-packet-validator.md +34 -0
  2. package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +6 -0
  3. package/commands/plan.md +6 -52
  4. package/hooks/blocking/code_rules_enforcer.py +2 -0
  5. package/hooks/blocking/code_rules_test_assertions.py +123 -1
  6. package/hooks/blocking/open_questions_in_plans_blocker.py +8 -1
  7. package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +90 -0
  8. package/hooks/blocking/test_open_questions_in_plans_blocker.py +43 -0
  9. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  10. package/hooks/hooks_constants/open_questions_in_plans_blocker_constants.py +4 -0
  11. package/hooks/hooks_constants/test_open_questions_in_plans_blocker_constants.py +13 -1
  12. package/package.json +1 -1
  13. package/skills/anthropic-plan/SKILL.md +46 -85
  14. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/__init__.py +0 -0
  15. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/validate_packet_constants.py +33 -0
  16. package/skills/anthropic-plan/scripts/test_validate_packet.py +405 -0
  17. package/skills/anthropic-plan/scripts/validate_packet.py +397 -0
  18. package/skills/anthropic-plan/templates/README.md +20 -0
  19. package/skills/anthropic-plan/templates/build-prompt.md +9 -0
  20. package/skills/anthropic-plan/templates/source-map.md +5 -0
  21. package/skills/anthropic-plan/test_skill_contract.py +53 -0
  22. package/skills/anthropic-plan/workflow/plan-packet.contract.test.mjs +79 -0
  23. package/skills/anthropic-plan/workflow/plan-packet.mjs +299 -0
@@ -1,107 +1,68 @@
1
1
  ---
2
2
  name: anthropic-plan
3
- description: Structured implementation planning through readonly codebase exploration before any code changes. Produces a plan file for approval. Use when the user says /anthropic-plan, "plan this first", "think before coding", "explore before implementing", "make a plan", or when approaching non-trivial tasks that benefit from upfront exploration and design. Also triggers on "what would the approach be", "scope this out", or "don't code yet, just plan".
3
+ description: Workflow-backed implementation planning that creates a deep repo-local packet under docs/plans/<slug>/ before any code changes. Use for /anthropic-plan, /plan, "plan this first", "think before coding", "make a plan", "scope this out", "don't code yet", and non-trivial implementation requests that need source-grounded design, TDD steps, and validator approval before build work.
4
4
  ---
5
5
 
6
- # Claude Plan
6
+ # Anthropic Plan
7
7
 
8
- Explore the codebase, design an approach, and write a plan file -- all without touching production code.
8
+ Create a source-grounded plan packet through the Claude Code Workflow runtime. The output is a repo-local `docs/plans/<slug>/` folder with context, spec, implementation, validation, and handoff docs. Stop before implementation.
9
9
 
10
- ## Why
10
+ ## Launch
11
11
 
12
- Jumping straight to code on non-trivial tasks leads to wasted effort when the approach conflicts with existing patterns, misses reusable code, or misunderstands the user's intent. This skill enforces "look before you leap": explore first, design second, write the plan third, get approval last. No code changes until the user says go.
12
+ Call the workflow with the user request and current working directory:
13
13
 
14
- ## Constraints
15
-
16
- Treat the codebase as readonly throughout this skill. The only file you may create or edit is the plan file.
17
-
18
- **Allowed:** Read files, Grep, Glob, launch Explore agents, launch Plan agents, write/edit the plan file, AskUserQuestion for clarification.
19
-
20
- **Not allowed:** Edit source files, Write new source files, run tests, install packages, run non-readonly Bash commands, or make any system changes.
21
-
22
- This discipline exists because the user invoked this skill specifically to understand the approach before committing to it. Violating readonly would undermine the whole point.
23
-
24
- ## Plan File
25
-
26
- Write to `~/.claude/plans/<slug>.md`.
27
-
28
- Generate the slug from the task -- descriptive, kebab-case, 2-4 words. Examples: `add-user-auth.md`, `fix-payment-retry.md`, `refactor-config-loading.md`. Avoid the random-word convention used by built-in plan mode.
29
-
30
- **Announce at start:** "Planning: `<slug>` -- exploring before writing code."
31
-
32
- ## Workflow
33
-
34
- ### Phase 1: Explore
35
-
36
- Understand the problem space before proposing solutions. Launch Explore agents in parallel -- up to 3, but use the minimum needed. Quality over quantity.
37
-
38
- What to look for:
39
- - Files and modules the task will touch
40
- - Existing patterns for similar functionality
41
- - Utilities, helpers, constants, and shared code to reuse
42
- - Test patterns already in place
43
-
44
- Skip this phase only if the task is trivial and full context already exists in the conversation.
45
-
46
- ### Phase 2: Design
47
-
48
- Launch Plan agent(s) to design the implementation -- up to 3 for complex or multi-area tasks, skip entirely for trivial tasks. Feed them comprehensive context from Phase 1; they cannot explore on their own, so everything they need must come from you.
49
-
50
- ### Phase 3: Review
51
-
52
- Before committing to the plan:
53
- - Read the critical files yourself to verify the agents got it right
54
- - Check alignment with what the user actually asked for
55
- - If requirements are ambiguous, use AskUserQuestion now -- not after writing the plan
56
-
57
- ### Phase 4: Write the Plan
14
+ ```js
15
+ Workflow({
16
+ scriptPath: "$HOME/.claude/skills/anthropic-plan/workflow/plan-packet.mjs",
17
+ input: {
18
+ task: "$ARGUMENTS",
19
+ cwd: "<current working directory>"
20
+ }
21
+ })
22
+ ```
58
23
 
59
- Write incrementally as you learn things in Phases 1-3, then refine here. The plan file has these sections:
24
+ If the Workflow tool is unavailable, say `anthropic-plan requires the Workflow tool; aborting` and stop.
60
25
 
61
- ```markdown
62
- ## Context
63
- Why this change is needed, what problem it solves, what the outcome looks like.
26
+ ## Workflow Contract
64
27
 
65
- ## Approach
66
- The recommended implementation. One approach, not a menu of alternatives.
67
- Concise but detailed enough to execute without re-exploring.
28
+ The workflow handles the full planning loop:
68
29
 
69
- ## Files
70
- Critical file paths that will be created or modified.
30
+ 1. Resolve repo root and packet path.
31
+ 2. Read project instructions, rules, relevant skills, manifests, docs, tests, hooks, agents, commands, configs, and workflows.
32
+ 3. Build a source inventory and extract source facts into `context/source-map.md`.
33
+ 4. Write the packet under `docs/plans/<slug>/`.
34
+ 5. Run `scripts/validate_packet.py`.
35
+ 6. Spawn `plan-packet-validator` in fresh context.
36
+ 7. Repair packet findings up to the workflow cap.
37
+ 8. Return packet path, validation state, and findings.
38
+ 9. Stop before implementation.
71
39
 
72
- ## Reuse
73
- Existing functions, utilities, constants, or patterns to leverage.
74
- Include file:line references so the implementer can find them instantly.
40
+ ## Packet Shape
75
41
 
76
- ## Steps
77
- Ordered implementation steps. Each step is a discrete, testable unit of work.
42
+ Required root: `docs/plans/<slug>/`
78
43
 
79
- ## Verification
80
- How to confirm end-to-end that the implementation works.
81
- Specific commands, test files, or manual verification steps.
44
+ Required top-level files and folders:
82
45
 
83
- ## Bash Permissions
84
- Semantic descriptions of bash actions the implementation will need:
85
- - "run tests"
86
- - "install dependencies"
87
- - "start dev server"
88
- These are action descriptions, not specific commands.
89
- ```
46
+ - `README.md`
47
+ - `packet.json`
48
+ - `context/`
49
+ - `spec/`
50
+ - `implementation/`
51
+ - `validation/`
52
+ - `handoff/`
90
53
 
91
- ### Phase 5: Present for Approval
54
+ The packet depth rule is strict: `README.md` is a thin hub, first-level folders group purpose, and second-level files carry detail. Add `context/subsystems/<name>.md` only when the planner finds more than twelve source files or more than three subsystems.
92
55
 
93
- Use AskUserQuestion to present the completed plan. Do not ask about approval in regular text -- always use AskUserQuestion so the user gets a clear, structured decision point.
56
+ ## Validation
94
57
 
95
- Options:
96
- - **Approve** -- proceed with implementation
97
- - **Revise** -- user has feedback to incorporate
98
- - **Cancel** -- abandon the plan
58
+ The deterministic validator checks required files, placeholders, `Open Questions`, source-map strength, TDD coverage, standalone handoff prompts, and `packet.json` consistency.
99
59
 
100
- ## Scaling
60
+ The `plan-packet-validator` agent checks source accuracy, scope, enough implementation detail for a blind build agent, real TDD order, invented APIs or commands, and end-to-end acceptance criteria.
101
61
 
102
- Not every task needs all five phases. Match effort to complexity:
62
+ ## Rules
103
63
 
104
- - **Trivial** (rename, typo fix): Ask if a formal plan is even wanted. If yes, skip Phases 1-2, write a minimal plan.
105
- - **Small** (single-file change, clear scope): One Explore agent, skip Design phase, concise plan.
106
- - **Medium** (multi-file feature, some ambiguity): Full workflow, 1-2 agents per phase.
107
- - **Large** (cross-cutting change, architectural): Full workflow, max agents, thorough Review phase.
64
+ - Write docs only.
65
+ - Do not edit production code.
66
+ - Do not run implementation commands.
67
+ - Ask the user only for product choices that cannot be derived from local context.
68
+ - Fold resolved answers into the packet. Never leave an `Open Questions` section.
@@ -0,0 +1,33 @@
1
+ """Constants for the plan packet validator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ ALL_REQUIRED_RELATIVE_PATHS: tuple[str, ...] = (
6
+ "README.md",
7
+ "packet.json",
8
+ "context/user-request.md",
9
+ "context/source-map.md",
10
+ "context/current-state.md",
11
+ "context/existing-patterns.md",
12
+ "context/constraints.md",
13
+ "context/glossary.md",
14
+ "spec/scope.md",
15
+ "spec/behavior.md",
16
+ "spec/interfaces.md",
17
+ "spec/data-flow.md",
18
+ "spec/failure-modes.md",
19
+ "spec/acceptance.md",
20
+ "implementation/strategy.md",
21
+ "implementation/steps.md",
22
+ "implementation/tdd-plan.md",
23
+ "implementation/file-plan.md",
24
+ "implementation/refactor-checkpoints.md",
25
+ "validation/validator-report.md",
26
+ "validation/deterministic-checks.md",
27
+ "validation/unresolved-risks.md",
28
+ "handoff/build-prompt.md",
29
+ "handoff/review-prompt.md",
30
+ "handoff/verification-commands.md",
31
+ )
32
+ MARKDOWN_FILE_SUFFIX: str = ".md"
33
+ EXIT_CODE_VALIDATION_FAILED: int = 2
@@ -0,0 +1,405 @@
1
+ """Tests for the plan packet validator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import json
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+ from types import ModuleType
11
+
12
+ import pytest
13
+
14
+
15
+ SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
16
+ VALIDATOR_PATH = SCRIPTS_DIRECTORY / "validate_packet.py"
17
+
18
+
19
+ def load_validator_module() -> ModuleType:
20
+ if str(SCRIPTS_DIRECTORY) not in sys.path:
21
+ sys.path.insert(0, str(SCRIPTS_DIRECTORY))
22
+ spec = importlib.util.spec_from_file_location("validate_packet", VALIDATOR_PATH)
23
+ assert spec is not None
24
+ assert spec.loader is not None
25
+ validator_module = importlib.util.module_from_spec(spec)
26
+ spec.loader.exec_module(validator_module)
27
+ return validator_module
28
+
29
+
30
+ def write_valid_packet(packet_directory: Path) -> None:
31
+ all_relative_paths = load_validator_module().required_relative_paths()
32
+ for each_relative_path in all_relative_paths:
33
+ target_path = packet_directory / each_relative_path
34
+ target_path.parent.mkdir(parents=True, exist_ok=True)
35
+ target_path.write_text(valid_markdown_for(each_relative_path), encoding="utf-8")
36
+
37
+ packet_payload = {
38
+ "schemaVersion": 1,
39
+ "slug": "add-login",
40
+ "repoRoot": str(packet_directory.parent.parent.parent),
41
+ "packetPath": str(packet_directory),
42
+ "sourceFiles": ["src/auth.py"],
43
+ "assumptions": ["No migration is needed."],
44
+ "validator": {"deterministic": "pending", "semantic": "pending"},
45
+ }
46
+ (packet_directory / "packet.json").write_text(
47
+ json.dumps(packet_payload, indent=2),
48
+ encoding="utf-8",
49
+ )
50
+
51
+
52
+ def valid_markdown_for(relative_path: str) -> str:
53
+ if relative_path == "context/source-map.md":
54
+ return (
55
+ "# Source Map\n\n"
56
+ "| Source | Why it matters | Facts extracted | Plan implication |\n"
57
+ "|---|---|---|---|\n"
58
+ "| src/auth.py | Login flow entrypoint | Existing authenticate_user function handles password checks. | Reuse authenticate_user in the implementation. |\n"
59
+ )
60
+ if relative_path == "implementation/tdd-plan.md":
61
+ return (
62
+ "# TDD Plan\n\n"
63
+ "1. Failing test: add test_auth_login_success before production edits.\n"
64
+ "2. Production code: wire the existing authenticate_user call.\n"
65
+ "3. Refactor after green: remove duplicated setup.\n"
66
+ )
67
+ if relative_path == "implementation/steps.md":
68
+ return (
69
+ "# Steps\n\n"
70
+ "1. Test first: add login success coverage.\n"
71
+ "2. Production change: add the route handler; covered by test_auth_login_success.\n"
72
+ )
73
+ if relative_path == "handoff/build-prompt.md":
74
+ return (
75
+ "# Build Prompt\n\n"
76
+ "Use only this packet. Read README.md, then context/source-map.md, then implementation/steps.md. "
77
+ "Do not rely on prior chat history."
78
+ )
79
+ return f"# {relative_path}\n\nGrounded implementation detail for this packet file.\n"
80
+
81
+
82
+ def run_validator(packet_directory: Path) -> subprocess.CompletedProcess[str]:
83
+ return subprocess.run(
84
+ [sys.executable, str(VALIDATOR_PATH), str(packet_directory)],
85
+ capture_output=True,
86
+ text=True,
87
+ check=False,
88
+ )
89
+
90
+
91
+ def test_valid_packet_passes(tmp_path: Path) -> None:
92
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
93
+ write_valid_packet(packet_directory)
94
+
95
+ validator_run = run_validator(packet_directory)
96
+
97
+ assert validator_run.returncode == 0, validator_run.stderr
98
+ assert "packet validation passed" in validator_run.stdout
99
+
100
+
101
+ def test_missing_required_file_fails(tmp_path: Path) -> None:
102
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
103
+ write_valid_packet(packet_directory)
104
+ (packet_directory / "spec" / "behavior.md").unlink()
105
+
106
+ validator_run = run_validator(packet_directory)
107
+
108
+ assert validator_run.returncode == 2
109
+ assert "missing required file: spec/behavior.md" in validator_run.stderr
110
+
111
+
112
+ def test_placeholder_text_fails(tmp_path: Path) -> None:
113
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
114
+ write_valid_packet(packet_directory)
115
+ (packet_directory / "spec" / "scope.md").write_text("TODO: fill this in", encoding="utf-8")
116
+
117
+ validator_run = run_validator(packet_directory)
118
+
119
+ assert validator_run.returncode == 2
120
+ assert "placeholder text" in validator_run.stderr
121
+
122
+
123
+ @pytest.mark.parametrize(
124
+ "template_placeholder_markdown",
125
+ [
126
+ "# <Plan Title>\n\nThis plan implements the feature.",
127
+ "This plan implements <feature name> for the <component> module.",
128
+ "| <path> | <reason> | <verified fact> | <implementation implication> |",
129
+ ],
130
+ )
131
+ def test_angle_bracket_placeholder_text_fails(
132
+ tmp_path: Path,
133
+ template_placeholder_markdown: str,
134
+ ) -> None:
135
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
136
+ write_valid_packet(packet_directory)
137
+ (packet_directory / "spec" / "scope.md").write_text(
138
+ template_placeholder_markdown,
139
+ encoding="utf-8",
140
+ )
141
+
142
+ validator_run = run_validator(packet_directory)
143
+
144
+ assert validator_run.returncode == 2
145
+ assert "spec/scope.md contains placeholder text" in validator_run.stderr
146
+
147
+
148
+ @pytest.mark.parametrize(
149
+ "non_placeholder_markdown",
150
+ [
151
+ "<details>\n<summary>Notes</summary>\nGrounded detail.\n</details>",
152
+ "Type the annotation as `list[str]` and call `<command>` from a code span.",
153
+ "Reach the endpoint with `curl <url>` inside the inline code span.",
154
+ "The comparison `attempt_count < threshold` must hold before the retry.",
155
+ "The handler accepts a List<Item> of records.",
156
+ "Wrap the field in Optional<User> when it may be absent.",
157
+ "The cache stores an Array<string> of identifiers.",
158
+ "The lookup uses a Map<Key, Value> keyed by request id.",
159
+ ],
160
+ )
161
+ def test_inline_html_and_code_does_not_flag_placeholder(
162
+ tmp_path: Path,
163
+ non_placeholder_markdown: str,
164
+ ) -> None:
165
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
166
+ write_valid_packet(packet_directory)
167
+ (packet_directory / "spec" / "scope.md").write_text(
168
+ non_placeholder_markdown,
169
+ encoding="utf-8",
170
+ )
171
+
172
+ validator_run = run_validator(packet_directory)
173
+
174
+ assert validator_run.returncode == 0, validator_run.stderr
175
+
176
+
177
+ def test_open_questions_heading_fails(tmp_path: Path) -> None:
178
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
179
+ write_valid_packet(packet_directory)
180
+ (packet_directory / "validation" / "unresolved-risks.md").write_text(
181
+ "## Open Questions\n- Which database?",
182
+ encoding="utf-8",
183
+ )
184
+
185
+ validator_run = run_validator(packet_directory)
186
+
187
+ assert validator_run.returncode == 2
188
+ assert "Open Questions" in validator_run.stderr
189
+
190
+
191
+ def test_weak_source_map_fails(tmp_path: Path) -> None:
192
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
193
+ write_valid_packet(packet_directory)
194
+ (packet_directory / "context" / "source-map.md").write_text(
195
+ "# Source Map\n\nNo sources yet.\n",
196
+ encoding="utf-8",
197
+ )
198
+
199
+ validator_run = run_validator(packet_directory)
200
+
201
+ assert validator_run.returncode == 2
202
+ assert "source-map.md must include source-grounded rows" in validator_run.stderr
203
+
204
+
205
+ def test_missing_tdd_plan_fails(tmp_path: Path) -> None:
206
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
207
+ write_valid_packet(packet_directory)
208
+ (packet_directory / "implementation" / "tdd-plan.md").write_text(
209
+ "# TDD Plan\n\nImplementation can start directly.",
210
+ encoding="utf-8",
211
+ )
212
+
213
+ validator_run = run_validator(packet_directory)
214
+
215
+ assert validator_run.returncode == 2
216
+ assert "tdd-plan.md must name failing tests" in validator_run.stderr
217
+
218
+
219
+ @pytest.mark.parametrize(
220
+ "forbidden_phrase",
221
+ ["as discussed above", "from our chat", "previous conversation", "earlier in this thread"],
222
+ )
223
+ def test_handoff_prompt_depending_on_chat_history_fails(
224
+ tmp_path: Path,
225
+ forbidden_phrase: str,
226
+ ) -> None:
227
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
228
+ write_valid_packet(packet_directory)
229
+ (packet_directory / "handoff" / "build-prompt.md").write_text(
230
+ f"# Build Prompt\n\nUse the details {forbidden_phrase}.",
231
+ encoding="utf-8",
232
+ )
233
+
234
+ validator_run = run_validator(packet_directory)
235
+
236
+ assert validator_run.returncode == 2
237
+ assert "build-prompt.md must stand alone" in validator_run.stderr
238
+
239
+
240
+ def test_tdd_plan_with_red_only_as_substring_fails(tmp_path: Path) -> None:
241
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
242
+ write_valid_packet(packet_directory)
243
+ (packet_directory / "implementation" / "tdd-plan.md").write_text(
244
+ "# TDD Plan\n\nImplementation is required. Wire production code directly.",
245
+ encoding="utf-8",
246
+ )
247
+
248
+ validator_run = run_validator(packet_directory)
249
+
250
+ assert validator_run.returncode == 2
251
+ assert "tdd-plan.md must name failing tests" in validator_run.stderr
252
+
253
+
254
+ def test_tdd_plan_naming_red_step_passes(tmp_path: Path) -> None:
255
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
256
+ write_valid_packet(packet_directory)
257
+ (packet_directory / "implementation" / "tdd-plan.md").write_text(
258
+ "# TDD Plan\n\n1. Red step: add the failing coverage.\n2. Green production code follows.",
259
+ encoding="utf-8",
260
+ )
261
+
262
+ validator_run = run_validator(packet_directory)
263
+
264
+ assert validator_run.returncode == 0, validator_run.stderr
265
+
266
+
267
+ def test_packet_path_with_forward_slashes_matches_native_directory(tmp_path: Path) -> None:
268
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
269
+ write_valid_packet(packet_directory)
270
+ packet_file = packet_directory / "packet.json"
271
+ packet_payload = json.loads(packet_file.read_text(encoding="utf-8"))
272
+ packet_payload["packetPath"] = packet_directory.as_posix()
273
+ packet_file.write_text(json.dumps(packet_payload, indent=2), encoding="utf-8")
274
+
275
+ validator_run = run_validator(packet_directory)
276
+
277
+ assert validator_run.returncode == 0, validator_run.stderr
278
+
279
+
280
+ def test_packet_path_with_trailing_separator_matches_directory(tmp_path: Path) -> None:
281
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
282
+ write_valid_packet(packet_directory)
283
+ packet_file = packet_directory / "packet.json"
284
+ packet_payload = json.loads(packet_file.read_text(encoding="utf-8"))
285
+ packet_payload["packetPath"] = str(packet_directory) + "/"
286
+ packet_file.write_text(json.dumps(packet_payload, indent=2), encoding="utf-8")
287
+
288
+ validator_run = run_validator(packet_directory)
289
+
290
+ assert validator_run.returncode == 0, validator_run.stderr
291
+
292
+
293
+ def test_packet_path_mismatch_still_fails(tmp_path: Path) -> None:
294
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
295
+ write_valid_packet(packet_directory)
296
+ packet_file = packet_directory / "packet.json"
297
+ packet_payload = json.loads(packet_file.read_text(encoding="utf-8"))
298
+ packet_payload["packetPath"] = str(packet_directory.parent / "different-slug")
299
+ packet_file.write_text(json.dumps(packet_payload, indent=2), encoding="utf-8")
300
+
301
+ validator_run = run_validator(packet_directory)
302
+
303
+ assert validator_run.returncode == 2
304
+ assert "packet.json packetPath must match the validated packet directory" in validator_run.stderr
305
+
306
+
307
+ def test_source_map_with_bare_non_python_source_passes(tmp_path: Path) -> None:
308
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
309
+ write_valid_packet(packet_directory)
310
+ (packet_directory / "context" / "source-map.md").write_text(
311
+ "# Source Map\n\n"
312
+ "| Source | Why it matters | Facts extracted | Plan implication |\n"
313
+ "|---|---|---|---|\n"
314
+ "| plan-packet.mjs | Workflow entry | Exports the run function. | Reuse the run shape. |\n",
315
+ encoding="utf-8",
316
+ )
317
+
318
+ validator_run = run_validator(packet_directory)
319
+
320
+ assert validator_run.returncode == 0, validator_run.stderr
321
+
322
+
323
+ def test_source_map_with_only_version_number_row_fails(tmp_path: Path) -> None:
324
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
325
+ write_valid_packet(packet_directory)
326
+ (packet_directory / "context" / "source-map.md").write_text(
327
+ "# Source Map\n\n"
328
+ "| Source | Why it matters | Facts extracted | Plan implication |\n"
329
+ "|---|---|---|---|\n"
330
+ "| The login subsystem | We reviewed version 2.0 of the design | No file path named here | Build accordingly |\n",
331
+ encoding="utf-8",
332
+ )
333
+
334
+ validator_run = run_validator(packet_directory)
335
+
336
+ assert validator_run.returncode == 2
337
+ assert "source-map.md must include source-grounded rows" in validator_run.stderr
338
+
339
+
340
+ def test_source_map_data_row_naming_source_and_facts_passes(tmp_path: Path) -> None:
341
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
342
+ write_valid_packet(packet_directory)
343
+ (packet_directory / "context" / "source-map.md").write_text(
344
+ "# Source Map\n\n"
345
+ "| Source | Why it matters | Facts extracted | Plan implication |\n"
346
+ "|---|---|---|---|\n"
347
+ "| src/auth.py | The source of truth for login | Key facts about hashing | Reuse it |\n",
348
+ encoding="utf-8",
349
+ )
350
+
351
+ validator_run = run_validator(packet_directory)
352
+
353
+ assert validator_run.returncode == 0, validator_run.stderr
354
+
355
+
356
+ def test_has_source_table_row_keeps_grounded_row_with_source_and_facts_prose() -> None:
357
+ validator_module = load_validator_module()
358
+ source_map_text = (
359
+ "| Source | Why it matters | Facts extracted | Plan implication |\n"
360
+ "|---|---|---|---|\n"
361
+ "| src/auth.py | The source of truth for login | Key facts about hashing | Reuse it |\n"
362
+ )
363
+
364
+ assert validator_module.has_source_table_row(source_map_text) is True
365
+
366
+
367
+ def test_has_source_table_row_skips_header_only_document() -> None:
368
+ validator_module = load_validator_module()
369
+ source_map_text = (
370
+ "| Source | Why it matters | Facts extracted | Plan implication |\n"
371
+ "|---|---|---|---|\n"
372
+ )
373
+
374
+ assert validator_module.has_source_table_row(source_map_text) is False
375
+
376
+
377
+ def test_bullet_list_steps_without_test_contract_fails(tmp_path: Path) -> None:
378
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
379
+ write_valid_packet(packet_directory)
380
+ (packet_directory / "implementation" / "steps.md").write_text(
381
+ "# Steps\n\n"
382
+ "- Add the route handler\n"
383
+ "- Wire the database call\n",
384
+ encoding="utf-8",
385
+ )
386
+
387
+ validator_run = run_validator(packet_directory)
388
+
389
+ assert validator_run.returncode == 2
390
+ assert "implementation/steps.md has steps without a test or non-code reason" in validator_run.stderr
391
+
392
+
393
+ def test_bullet_list_steps_naming_test_contract_passes(tmp_path: Path) -> None:
394
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
395
+ write_valid_packet(packet_directory)
396
+ (packet_directory / "implementation" / "steps.md").write_text(
397
+ "# Steps\n\n"
398
+ "- Test first: add login success coverage.\n"
399
+ "- Production change: add the route handler; covered by test_auth_login_success.\n",
400
+ encoding="utf-8",
401
+ )
402
+
403
+ validator_run = run_validator(packet_directory)
404
+
405
+ assert validator_run.returncode == 0, validator_run.stderr