claude-dev-env 1.50.4 → 1.52.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 (73) hide show
  1. package/CLAUDE.md +0 -8
  2. package/_shared/pr-loop/audit-contract.md +3 -3
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/preflight_self_heal_constants.py +28 -0
  4. package/_shared/pr-loop/scripts/preflight.py +18 -6
  5. package/_shared/pr-loop/scripts/preflight_self_heal.py +164 -0
  6. package/_shared/pr-loop/scripts/tests/test_preflight.py +39 -0
  7. package/_shared/pr-loop/scripts/tests/test_preflight_self_heal.py +273 -0
  8. package/agents/clean-coder.md +1 -1
  9. package/agents/code-quality-agent.md +7 -5
  10. package/audit-rubrics/category_rubrics/category-a-api-contracts.md +3 -0
  11. package/audit-rubrics/category_rubrics/category-f-silent-failures.md +3 -0
  12. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +8 -2
  13. package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +3 -0
  14. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +39 -0
  15. package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +40 -0
  16. package/audit-rubrics/prompts/category-a-api-contracts.md +11 -4
  17. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  18. package/audit-rubrics/prompts/category-c-resource-cleanup.md +1 -1
  19. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +1 -1
  20. package/audit-rubrics/prompts/category-e-dead-code.md +1 -1
  21. package/audit-rubrics/prompts/category-f-silent-failures.md +13 -2
  22. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +1 -1
  23. package/audit-rubrics/prompts/category-h-security-boundaries.md +1 -1
  24. package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
  25. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -1
  26. package/audit-rubrics/prompts/category-k-codebase-conflicts.md +15 -5
  27. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +1 -1
  28. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +1 -1
  29. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +10 -3
  30. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +74 -0
  31. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +75 -0
  32. package/docs/CODE_RULES.md +24 -346
  33. package/hooks/blocking/code_rules_enforcer.py +367 -42
  34. package/hooks/blocking/tdd_enforcer.py +211 -19
  35. package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +519 -0
  36. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +1 -1
  37. package/hooks/blocking/test_tdd_enforcer.py +399 -0
  38. package/hooks/hooks.json +0 -15
  39. package/hooks/hooks_constants/code_rules_enforcer_constants.py +5 -0
  40. package/package.json +1 -1
  41. package/rules/ask-user-question-required.md +2 -41
  42. package/rules/confirm-implementation-forks.md +3 -44
  43. package/rules/gh-body-file.md +2 -78
  44. package/rules/gh-paginate.md +2 -78
  45. package/rules/plain-language.md +2 -41
  46. package/rules/prompt-workflow-context-controls.md +9 -38
  47. package/rules/shell-invocation-policy.md +2 -141
  48. package/rules/testing.md +10 -0
  49. package/rules/vault-context.md +3 -32
  50. package/rules/windows-filesystem-safe.md +3 -87
  51. package/scripts/sync_to_cursor/rules.py +201 -79
  52. package/scripts/tests/test_sync_to_cursor.py +122 -26
  53. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +2 -0
  54. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +51 -4
  55. package/skills/auditing-claude-config/SKILL.md +6 -1
  56. package/skills/bugteam/CONSTRAINTS.md +1 -1
  57. package/skills/bugteam/PROMPTS.md +8 -6
  58. package/skills/bugteam/SKILL.md +5 -5
  59. package/skills/bugteam/reference/audit-and-teammates.md +1 -1
  60. package/skills/bugteam/reference/audit-contract.md +4 -4
  61. package/skills/bugteam/reference/design-rationale.md +1 -1
  62. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
  63. package/skills/bugteam/reference/team-setup.md +17 -5
  64. package/skills/bugteam/scripts/bugteam_preflight.py +22 -10
  65. package/skills/bugteam/scripts/test_bugteam_preflight.py +32 -0
  66. package/skills/copilot-review/SKILL.md +5 -8
  67. package/skills/doc-gist/SKILL.md +5 -8
  68. package/skills/fixbugs/SKILL.md +1 -1
  69. package/skills/gh-paginate/SKILL.md +84 -0
  70. package/skills/pre-compact/SKILL.md +4 -9
  71. package/skills/refine/SKILL.md +8 -2
  72. package/skills/structure-prompt/SKILL.md +5 -10
  73. package/skills/update/SKILL.md +143 -0
@@ -13,18 +13,18 @@ from sync_to_cursor.config import MAX_RULE_BODY_LINES
13
13
  def _parse_h2_sections(markdown: str) -> dict[str, str]:
14
14
  parts = re.split(r"^## ", markdown, flags=re.MULTILINE)
15
15
  sections: dict[str, str] = {}
16
- for part in parts[1:]:
17
- title_line, _, body = part.partition("\n")
16
+ for each_part in parts[1:]:
17
+ title_line, _, body = each_part.partition("\n")
18
18
  sections[title_line.strip()] = body.strip()
19
19
  return sections
20
20
 
21
21
 
22
22
  def _filter_core_principles(body: str) -> str:
23
23
  lines = []
24
- for line in body.splitlines():
25
- if "readability standard" in line:
24
+ for each_line in body.splitlines():
25
+ if "readability standard" in each_line:
26
26
  continue
27
- lines.append(line)
27
+ lines.append(each_line)
28
28
  return "\n".join(lines).strip()
29
29
 
30
30
 
@@ -56,14 +56,54 @@ def _strip_code_standards_blockquote(markdown: str) -> str:
56
56
  return "\n".join(out).strip()
57
57
 
58
58
 
59
- def merge_code_standards(sources: tuple[Path, ...]) -> str:
59
+ _merged_mapping_key_order = (
60
+ "code-standards",
61
+ "tasklings-preferences",
62
+ "right-sized-engineering",
63
+ "bdd",
64
+ "test-quality",
65
+ "research-mode",
66
+ "conservative-action",
67
+ "explore-thoroughly",
68
+ )
69
+
70
+ _code_standards_section_order = (
71
+ "COMMENT PRESERVATION (ABSOLUTE RULE)",
72
+ "CORE PRINCIPLES",
73
+ "⚡ HOOK-ENFORCED RULES",
74
+ "3. REUSE CONSTANTS / 4. CONFIG LOCATIONS",
75
+ "5. NO ABBREVIATIONS",
76
+ "6. COMPLETE TYPE HINTS",
77
+ "9. SELF-CONTAINED COMPONENTS",
78
+ )
79
+
80
+ _test_quality_section_order = (
81
+ "Delete Useless Tests",
82
+ "Test Dependencies MUST FAIL",
83
+ "Core Testing Principles",
84
+ "React Testing Patterns",
85
+ "Test File Organization",
86
+ )
87
+
88
+
89
+ def merge_code_standards(all_sources: tuple[Path, ...]) -> str:
90
+ """Merge the code-standards rule and CODE_RULES doc into one Cursor rule body.
91
+
92
+ Args:
93
+ all_sources: The code-standards rule path then the CODE_RULES doc path.
94
+
95
+ Returns:
96
+ The merged Cursor rule body, truncated to the maximum rule body length.
97
+ """
60
98
  code_standards_markdown = _strip_code_standards_blockquote(
61
- sources[0].read_text(encoding="utf-8")
99
+ all_sources[0].read_text(encoding="utf-8")
62
100
  )
63
101
  code_standards_markdown = "\n".join(
64
- line for line in code_standards_markdown.splitlines() if not line.strip().startswith("- TDD ")
102
+ each_line
103
+ for each_line in code_standards_markdown.splitlines()
104
+ if not each_line.strip().startswith("- TDD ")
65
105
  )
66
- code_rules_markdown = sources[1].read_text(encoding="utf-8")
106
+ code_rules_markdown = all_sources[1].read_text(encoding="utf-8")
67
107
  sections_by_heading = _parse_h2_sections(code_rules_markdown)
68
108
  if not sections_by_heading:
69
109
  pointer_fallback_note = (
@@ -72,63 +112,71 @@ def merge_code_standards(sources: tuple[Path, ...]) -> str:
72
112
  )
73
113
  chunks = [code_standards_markdown, "", pointer_fallback_note]
74
114
  return _limit_lines("\n\n".join(chunks), MAX_RULE_BODY_LINES)
75
- include_order = [
76
- "COMMENT PRESERVATION (ABSOLUTE RULE)",
77
- "CORE PRINCIPLES",
78
- "⚡ HOOK-ENFORCED RULES",
79
- "4. CONFIG LOCATIONS",
80
- "5. NO ABBREVIATIONS",
81
- "6. COMPLETE TYPE HINTS",
82
- "9. SELF-CONTAINED COMPONENTS",
83
- ]
84
115
  chunks = [
85
116
  code_standards_markdown,
86
117
  "",
87
118
  "## Reference (full text: `.cursor/docs/CODE_RULES.md`)",
88
119
  ]
89
- for title in include_order:
90
- body = sections_by_heading.get(title, "")
91
- if title == "CORE PRINCIPLES":
120
+ for each_title in _code_standards_section_order:
121
+ assert each_title in sections_by_heading, (
122
+ f"merge_code_standards: expected section absent from CODE_RULES.md: {each_title}"
123
+ )
124
+ body = sections_by_heading[each_title]
125
+ if each_title == "CORE PRINCIPLES":
92
126
  body = _filter_core_principles(body)
93
127
  if body:
94
- chunks.append(f"## {title}\n\n{body}")
128
+ chunks.append(f"## {each_title}\n\n{body}")
95
129
  merged = "\n\n".join(chunks)
96
130
  return _limit_lines(merged, MAX_RULE_BODY_LINES)
97
131
 
98
132
 
99
- def merge_test_quality(sources: tuple[Path, ...]) -> str:
100
- testing = sources[0].read_text(encoding="utf-8").strip()
101
- test_quality_markdown = sources[1].read_text(encoding="utf-8")
133
+ def merge_test_quality(all_sources: tuple[Path, ...]) -> str:
134
+ """Merge the testing rule and TEST_QUALITY doc into one Cursor rule body.
135
+
136
+ Args:
137
+ all_sources: The testing rule path then the TEST_QUALITY doc path.
138
+
139
+ Returns:
140
+ The merged Cursor rule body, truncated to the maximum rule body length.
141
+ """
142
+ testing = strip_leading_yaml_frontmatter(
143
+ all_sources[0].read_text(encoding="utf-8")
144
+ ).strip()
145
+ test_quality_markdown = all_sources[1].read_text(encoding="utf-8")
102
146
  sections_by_heading = _parse_h2_sections(test_quality_markdown)
103
- include_order = [
104
- "Delete Useless Tests",
105
- "Test Dependencies MUST FAIL",
106
- "Core Testing Principles",
107
- "React Testing Patterns",
108
- "Test File Organization",
109
- ]
110
147
  chunks = [testing, "", "## Reference (full text: `.cursor/docs/TEST_QUALITY.md`)"]
111
- for title in include_order:
112
- body = sections_by_heading.get(title, "")
148
+ for each_title in _test_quality_section_order:
149
+ assert each_title in sections_by_heading, (
150
+ f"merge_test_quality: expected section absent from TEST_QUALITY.md: {each_title}"
151
+ )
152
+ body = sections_by_heading[each_title]
113
153
  if body:
114
- chunks.append(f"## {title}\n\n{body}")
154
+ chunks.append(f"## {each_title}\n\n{body}")
115
155
  merged = "\n\n".join(chunks)
116
156
  return _limit_lines(merged, MAX_RULE_BODY_LINES)
117
157
 
118
158
 
119
159
  def strip_anthropic_refs(text: str) -> str:
160
+ """Remove Anthropic source citations and the do-not-act wrapper from rule text.
161
+
162
+ Args:
163
+ text: The rule body to clean.
164
+
165
+ Returns:
166
+ The rule body without Anthropic citation lines or the wrapper tags.
167
+ """
120
168
  out_lines: list[str] = []
121
- for line in text.splitlines():
122
- s = line.strip()
123
- if s.startswith("Source:") and (
124
- "anthropic" in s.lower()
125
- or "claude.com" in s.lower()
126
- or "docs.anthropic" in s.lower()
169
+ for each_line in text.splitlines():
170
+ stripped_line = each_line.strip()
171
+ if stripped_line.startswith("Source:") and (
172
+ "anthropic" in stripped_line.lower()
173
+ or "claude.com" in stripped_line.lower()
174
+ or "docs.anthropic" in stripped_line.lower()
127
175
  ):
128
176
  continue
129
- if "docs.anthropic.com" in line:
177
+ if "docs.anthropic.com" in each_line:
130
178
  continue
131
- out_lines.append(line)
179
+ out_lines.append(each_line)
132
180
  text = "\n".join(out_lines)
133
181
  text = re.sub(
134
182
  r"<do_not_act_before_instructions>\s*",
@@ -149,7 +197,14 @@ def near_verbatim(text: str) -> str:
149
197
 
150
198
 
151
199
  def strip_leading_yaml_frontmatter(text: str) -> str:
152
- """Remove leading `---` ... `---` block (e.g. Claude `paths:`) so Cursor `.mdc` uses its own frontmatter."""
200
+ """Remove a leading `---` ... `---` block so Cursor `.mdc` keeps its own frontmatter.
201
+
202
+ Args:
203
+ text: The rule body that may open with a Claude YAML frontmatter block.
204
+
205
+ Returns:
206
+ The rule body with any leading frontmatter block removed.
207
+ """
153
208
  lines = text.splitlines()
154
209
  if not lines or lines[0].strip() != "---":
155
210
  return text
@@ -164,15 +219,29 @@ TransformName = Literal["verbatim", "near_verbatim", "merge_code_standards", "me
164
219
 
165
220
  def apply_transform(
166
221
  name: TransformName,
167
- sources: tuple[Path, ...],
222
+ all_sources: tuple[Path, ...],
168
223
  *,
169
224
  strip_leading_frontmatter: bool = False,
170
225
  ) -> str:
226
+ """Run the named transform over the source files and return the rule body.
227
+
228
+ Args:
229
+ name: The transform identifier selecting how the sources combine.
230
+ all_sources: The source files the transform reads.
231
+ strip_leading_frontmatter: When True, drop a leading YAML frontmatter
232
+ block from the raw concatenation before a verbatim transform.
233
+
234
+ Returns:
235
+ The transformed Cursor rule body.
236
+
237
+ Raises:
238
+ AssertionError: When *name* is not a recognized transform identifier.
239
+ """
171
240
  if name == "merge_code_standards":
172
- return merge_code_standards(sources)
241
+ return merge_code_standards(all_sources)
173
242
  if name == "merge_test_quality":
174
- return merge_test_quality(sources)
175
- raw = "\n\n".join(p.read_text(encoding="utf-8") for p in sources)
243
+ return merge_test_quality(all_sources)
244
+ raw = "\n\n".join(each_source.read_text(encoding="utf-8") for each_source in all_sources)
176
245
  if strip_leading_frontmatter:
177
246
  raw = strip_leading_yaml_frontmatter(raw)
178
247
  if name == "verbatim":
@@ -216,7 +285,14 @@ def _full_mdc(mapping: RuleMapping, body: str) -> str:
216
285
 
217
286
 
218
287
  def _read_paths_glob(rule_file: Path) -> str | None:
219
- """Read `paths:` list from a Claude rule's YAML frontmatter; return as comma-separated Cursor glob string."""
288
+ """Read the `paths:` list from a Claude rule's YAML frontmatter as a Cursor glob.
289
+
290
+ Args:
291
+ rule_file: The Claude rule file whose frontmatter may declare `paths:`.
292
+
293
+ Returns:
294
+ The comma-separated glob string, or None when the rule declares no paths.
295
+ """
220
296
  if not rule_file.is_file():
221
297
  return None
222
298
  lines = rule_file.read_text(encoding="utf-8").splitlines()
@@ -224,26 +300,46 @@ def _read_paths_glob(rule_file: Path) -> str | None:
224
300
  return None
225
301
  is_in_paths = False
226
302
  all_paths: list[str] = []
227
- for line in lines[1:]:
228
- if line.strip() == "---":
303
+ for each_line in lines[1:]:
304
+ if each_line.strip() == "---":
229
305
  break
230
- if line.startswith("paths:"):
306
+ if each_line.startswith("paths:"):
231
307
  is_in_paths = True
232
308
  continue
233
309
  if is_in_paths:
234
- if line.startswith(" ") or line.startswith("\t"):
235
- path_value = line.strip().lstrip("-").strip().strip('"').strip("'")
236
- if path_value:
237
- all_paths.append(path_value)
310
+ if each_line.startswith(" ") or each_line.startswith("\t"):
311
+ stripped_path = each_line.strip().lstrip("-").strip().strip('"').strip("'")
312
+ if stripped_path:
313
+ all_paths.append(stripped_path)
238
314
  else:
239
315
  is_in_paths = False
240
316
  return ",".join(all_paths) if all_paths else None
241
317
 
242
318
 
243
- def build_mappings(claude: Path) -> tuple[RuleMapping, ...]:
244
- rules_directory = claude / "rules"
245
- docs_directory = claude / "docs"
246
- test_globs = "**/*.test.*,**/*.spec.*,**/test_*,**/*_test.*"
319
+ def _require_paths_glob(rule_file: Path) -> str | None:
320
+ """Return the path glob for an optional rule, requiring frontmatter when it exists.
321
+
322
+ Args:
323
+ rule_file: The Claude rule file whose frontmatter may declare `paths:`.
324
+
325
+ Returns:
326
+ The comma-separated glob string, or None when the rule file is absent.
327
+
328
+ Raises:
329
+ AssertionError: When the rule file exists but declares no `paths:`
330
+ frontmatter, which would silently disable the rule in Cursor.
331
+ """
332
+ if not rule_file.is_file():
333
+ return None
334
+ paths_glob = _read_paths_glob(rule_file)
335
+ assert paths_glob is not None, (
336
+ f"{rule_file.name}: path-scoped rule exists but declares no `paths:` frontmatter; "
337
+ "add a `paths:` list or remove the file"
338
+ )
339
+ return paths_glob
340
+
341
+
342
+ def _always_apply_mappings(rules_directory: Path, docs_directory: Path) -> tuple[RuleMapping, ...]:
247
343
  return (
248
344
  RuleMapping(
249
345
  "code-standards",
@@ -254,16 +350,6 @@ def build_mappings(claude: Path) -> tuple[RuleMapping, ...]:
254
350
  "Core code standards: naming, types, config, hook-enforced rules",
255
351
  "merge_code_standards",
256
352
  ),
257
- RuleMapping(
258
- "tasklings-preferences",
259
- (rules_directory / "tasklings-preferences.md",),
260
- "tasklings-preferences.mdc",
261
- False,
262
- _read_paths_glob(rules_directory / "tasklings-preferences.md"),
263
- "Tasklings: Prefer / Do / Always engineering preferences (scoped path)",
264
- "verbatim",
265
- True,
266
- ),
267
353
  RuleMapping(
268
354
  "right-sized-engineering",
269
355
  (rules_directory / "right-sized-engineering.md",),
@@ -282,15 +368,6 @@ def build_mappings(claude: Path) -> tuple[RuleMapping, ...]:
282
368
  "BDD: discovery-driven protocol (outside-in; Illustrate→Formulate→Automate)",
283
369
  "verbatim",
284
370
  ),
285
- RuleMapping(
286
- "test-quality",
287
- (rules_directory / "testing.md", docs_directory / "TEST_QUALITY.md"),
288
- "test-quality.mdc",
289
- False,
290
- test_globs,
291
- "Testing quality for test files",
292
- "merge_test_quality",
293
- ),
294
371
  RuleMapping(
295
372
  "research-mode",
296
373
  (rules_directory / "research-mode.md",),
@@ -319,3 +396,48 @@ def build_mappings(claude: Path) -> tuple[RuleMapping, ...]:
319
396
  "near_verbatim",
320
397
  ),
321
398
  )
399
+
400
+
401
+ def _path_scoped_mappings(rules_directory: Path, docs_directory: Path) -> tuple[RuleMapping, ...]:
402
+ return (
403
+ RuleMapping(
404
+ "tasklings-preferences",
405
+ (rules_directory / "tasklings-preferences.md",),
406
+ "tasklings-preferences.mdc",
407
+ False,
408
+ _require_paths_glob(rules_directory / "tasklings-preferences.md"),
409
+ "Tasklings: Prefer / Do / Always engineering preferences (scoped path)",
410
+ "verbatim",
411
+ True,
412
+ ),
413
+ RuleMapping(
414
+ "test-quality",
415
+ (rules_directory / "testing.md", docs_directory / "TEST_QUALITY.md"),
416
+ "test-quality.mdc",
417
+ False,
418
+ _require_paths_glob(rules_directory / "testing.md"),
419
+ "Testing quality for test files",
420
+ "merge_test_quality",
421
+ ),
422
+ )
423
+
424
+
425
+ def build_mappings(claude: Path) -> tuple[RuleMapping, ...]:
426
+ """Resolve every rule into a concrete Cursor mapping against a Claude layout.
427
+
428
+ Args:
429
+ claude: The Claude layout root holding the `rules` and `docs` directories.
430
+
431
+ Returns:
432
+ One RuleMapping per rule, each path-scoped rule carrying a glob derived
433
+ from its source rule's `paths:` frontmatter.
434
+ """
435
+ rules_directory = claude / "rules"
436
+ docs_directory = claude / "docs"
437
+ all_mappings = (
438
+ *_always_apply_mappings(rules_directory, docs_directory),
439
+ *_path_scoped_mappings(rules_directory, docs_directory),
440
+ )
441
+ mapping_by_key = {each_mapping.key: each_mapping for each_mapping in all_mappings}
442
+ assert set(mapping_by_key) == set(_merged_mapping_key_order)
443
+ return tuple(mapping_by_key[each_key] for each_key in _merged_mapping_key_order)
@@ -14,7 +14,19 @@ if str(_SCRIPTS_DIR) not in sys.path:
14
14
 
15
15
  import sync_to_cursor as mod
16
16
  from sync_to_cursor.engine import run as run_sync_to_cursor
17
- from sync_to_cursor.rules import _read_paths_glob
17
+ from sync_to_cursor.rules import (
18
+ _code_standards_section_order,
19
+ _parse_h2_sections,
20
+ _read_paths_glob,
21
+ _test_quality_section_order,
22
+ build_mappings,
23
+ strip_leading_yaml_frontmatter,
24
+ )
25
+
26
+ _CLAUDE_DEV_ENV_DIR = _SCRIPTS_DIR.parent
27
+ _REAL_TESTING_RULE = _CLAUDE_DEV_ENV_DIR / "rules" / "testing.md"
28
+ _REAL_CODE_RULES_DOC = _CLAUDE_DEV_ENV_DIR / "docs" / "CODE_RULES.md"
29
+ _REAL_TEST_QUALITY_DOC = _CLAUDE_DEV_ENV_DIR / "docs" / "TEST_QUALITY.md"
18
30
 
19
31
 
20
32
  def _minimal_rule_files(claude_rules: Path) -> None:
@@ -28,7 +40,10 @@ def _minimal_rule_files(claude_rules: Path) -> None:
28
40
  )
29
41
  (claude_rules / "right-sized-engineering.md").write_text("# RSE\n", encoding="utf-8")
30
42
  (claude_rules / "bdd.md").write_text("# BDD\n", encoding="utf-8")
31
- (claude_rules / "testing.md").write_text("# Testing\n", encoding="utf-8")
43
+ (claude_rules / "testing.md").write_text(
44
+ "---\npaths:\n - \"**/test_*.py\"\n---\n\n# Testing\n",
45
+ encoding="utf-8",
46
+ )
32
47
  (claude_rules / "research-mode.md").write_text("# RM\n", encoding="utf-8")
33
48
  (claude_rules / "conservative-action.md").write_text("# CA\n", encoding="utf-8")
34
49
  (claude_rules / "explore-thoroughly.md").write_text("# ET\n", encoding="utf-8")
@@ -36,8 +51,12 @@ def _minimal_rule_files(claude_rules: Path) -> None:
36
51
 
37
52
  def _minimal_code_rules_and_test_quality(claude_docs: Path) -> tuple[bytes, bytes]:
38
53
  claude_docs.mkdir(parents=True, exist_ok=True)
39
- cr = b"## CORE PRINCIPLES\n\nalpha\n"
40
- tq = b"## Core Testing Principles\n\nbeta\n"
54
+ cr = (
55
+ "\n\n".join(f"## {title}\n\nalpha" for title in _code_standards_section_order) + "\n"
56
+ ).encode("utf-8")
57
+ tq = (
58
+ "\n\n".join(f"## {title}\n\nbeta" for title in _test_quality_section_order) + "\n"
59
+ ).encode("utf-8")
41
60
  (claude_docs / "CODE_RULES.md").write_bytes(cr)
42
61
  (claude_docs / "TEST_QUALITY.md").write_bytes(tq)
43
62
  return cr, tq
@@ -168,6 +187,18 @@ def test_check_skips_optional_mapping_when_source_missing(
168
187
  assert run_sync_to_cursor(["--check"]) == 0, "--check must pass when only optional sources are missing"
169
188
 
170
189
 
190
+ def test_test_quality_glob_derived_from_testing_frontmatter() -> None:
191
+ expected_glob = _read_paths_glob(_REAL_TESTING_RULE)
192
+ assert expected_glob, "testing.md must declare a non-empty paths frontmatter"
193
+ pinned_glob = "**/test_*.py,**/*_test.py,**/*.test.*,**/*.spec.*,**/conftest.py,**/tests/**"
194
+ assert expected_glob == pinned_glob
195
+ test_quality_mapping = next(
196
+ mapping for mapping in build_mappings(_CLAUDE_DEV_ENV_DIR) if mapping.key == "test-quality"
197
+ )
198
+ assert test_quality_mapping.globs == expected_glob
199
+ assert test_quality_mapping.globs == pinned_glob
200
+
201
+
171
202
  def test_tasklings_glob_derived_from_frontmatter(tmp_path: Path) -> None:
172
203
  rules_directory = tmp_path / "rules"
173
204
  rules_directory.mkdir(parents=True)
@@ -185,18 +216,7 @@ def test_merge_reference_headers_point_at_cursor_docs(tmp_path: Path) -> None:
185
216
  rules_directory.mkdir(parents=True, exist_ok=True)
186
217
  docs_directory.mkdir(parents=True, exist_ok=True)
187
218
  (rules_directory / "code-standards.md").write_text("# CS\n", encoding="utf-8")
188
- cr_body = "\n\n".join(
189
- f"## {t}\n\nbody"
190
- for t in [
191
- "COMMENT PRESERVATION (ABSOLUTE RULE)",
192
- "CORE PRINCIPLES",
193
- "⚡ HOOK-ENFORCED RULES",
194
- "4. CONFIG LOCATIONS",
195
- "5. NO ABBREVIATIONS",
196
- "6. COMPLETE TYPE HINTS",
197
- "9. SELF-CONTAINED COMPONENTS",
198
- ]
199
- )
219
+ cr_body = "\n\n".join(f"## {t}\n\nbody" for t in _code_standards_section_order)
200
220
  (docs_directory / "CODE_RULES.md").write_text(cr_body, encoding="utf-8")
201
221
  merged = mod.merge_code_standards(
202
222
  (rules_directory / "code-standards.md", docs_directory / "CODE_RULES.md")
@@ -204,18 +224,94 @@ def test_merge_reference_headers_point_at_cursor_docs(tmp_path: Path) -> None:
204
224
  assert ".cursor/docs/CODE_RULES.md" in merged
205
225
 
206
226
  (rules_directory / "testing.md").write_text("# T\n", encoding="utf-8")
207
- tq_body = "\n\n".join(
208
- f"## {t}\n\nbody"
209
- for t in [
210
- "Delete Useless Tests",
211
- "Test Dependencies MUST FAIL",
212
- "Core Testing Principles",
213
- "React Testing Patterns",
214
- "Test File Organization",
215
- ]
216
- )
227
+ tq_body = "\n\n".join(f"## {t}\n\nbody" for t in _test_quality_section_order)
217
228
  (docs_directory / "TEST_QUALITY.md").write_text(tq_body, encoding="utf-8")
218
229
  merged_tq = mod.merge_test_quality(
219
230
  (rules_directory / "testing.md", docs_directory / "TEST_QUALITY.md")
220
231
  )
221
232
  assert ".cursor/docs/TEST_QUALITY.md" in merged_tq
233
+
234
+
235
+ def test_code_standards_section_order_titles_all_exist_in_real_doc() -> None:
236
+ real_headings = set(_parse_h2_sections(_REAL_CODE_RULES_DOC.read_text(encoding="utf-8")))
237
+ missing_titles = [title for title in _code_standards_section_order if title not in real_headings]
238
+ assert missing_titles == [], (
239
+ f"_code_standards_section_order titles absent from {_REAL_CODE_RULES_DOC.name}: "
240
+ f"{missing_titles}"
241
+ )
242
+
243
+
244
+ def test_test_quality_section_order_titles_all_exist_in_real_doc() -> None:
245
+ real_headings = set(_parse_h2_sections(_REAL_TEST_QUALITY_DOC.read_text(encoding="utf-8")))
246
+ missing_titles = [title for title in _test_quality_section_order if title not in real_headings]
247
+ assert missing_titles == [], (
248
+ f"_test_quality_section_order titles absent from {_REAL_TEST_QUALITY_DOC.name}: "
249
+ f"{missing_titles}"
250
+ )
251
+
252
+
253
+ def test_merge_code_standards_raises_when_expected_section_absent(tmp_path: Path) -> None:
254
+ rules_directory = tmp_path / "rules"
255
+ docs_directory = tmp_path / "docs"
256
+ rules_directory.mkdir(parents=True, exist_ok=True)
257
+ docs_directory.mkdir(parents=True, exist_ok=True)
258
+ (rules_directory / "code-standards.md").write_text("# CS\n", encoding="utf-8")
259
+ present_titles = [
260
+ title for title in _code_standards_section_order if title != "5. NO ABBREVIATIONS"
261
+ ]
262
+ cr_body = "\n\n".join(f"## {title}\n\nbody" for title in present_titles)
263
+ (docs_directory / "CODE_RULES.md").write_text(cr_body, encoding="utf-8")
264
+ with pytest.raises(AssertionError, match="5. NO ABBREVIATIONS"):
265
+ mod.merge_code_standards(
266
+ (rules_directory / "code-standards.md", docs_directory / "CODE_RULES.md")
267
+ )
268
+
269
+
270
+ def test_build_mappings_raises_when_testing_lacks_paths_frontmatter(tmp_path: Path) -> None:
271
+ claude = tmp_path / ".claude"
272
+ _minimal_rule_files(claude / "rules")
273
+ _minimal_code_rules_and_test_quality(claude / "docs")
274
+ (claude / "rules" / "testing.md").write_text("# Testing\n", encoding="utf-8")
275
+ with pytest.raises(AssertionError, match="testing.md"):
276
+ build_mappings(claude)
277
+
278
+
279
+ def test_merge_test_quality_raises_when_expected_section_absent(tmp_path: Path) -> None:
280
+ rules_directory = tmp_path / "rules"
281
+ docs_directory = tmp_path / "docs"
282
+ rules_directory.mkdir(parents=True, exist_ok=True)
283
+ docs_directory.mkdir(parents=True, exist_ok=True)
284
+ (rules_directory / "testing.md").write_text("# T\n", encoding="utf-8")
285
+ present_titles = [
286
+ title for title in _test_quality_section_order if title != "Core Testing Principles"
287
+ ]
288
+ tq_body = "\n\n".join(f"## {title}\n\nbody" for title in present_titles)
289
+ (docs_directory / "TEST_QUALITY.md").write_text(tq_body, encoding="utf-8")
290
+ with pytest.raises(AssertionError, match="Core Testing Principles"):
291
+ mod.merge_test_quality(
292
+ (rules_directory / "testing.md", docs_directory / "TEST_QUALITY.md")
293
+ )
294
+
295
+
296
+ def test_merge_test_quality_strips_leading_paths_frontmatter(tmp_path: Path) -> None:
297
+ docs_directory = tmp_path / "docs"
298
+ docs_directory.mkdir(parents=True, exist_ok=True)
299
+ test_quality_body = "\n\n".join(
300
+ f"## {title}\n\nbody" for title in _test_quality_section_order
301
+ )
302
+ (docs_directory / "TEST_QUALITY.md").write_text(test_quality_body, encoding="utf-8")
303
+ merged = mod.merge_test_quality(
304
+ (_REAL_TESTING_RULE, docs_directory / "TEST_QUALITY.md")
305
+ )
306
+ testing_text = _REAL_TESTING_RULE.read_text(encoding="utf-8")
307
+ first_content_line = next(
308
+ line for line in strip_leading_yaml_frontmatter(testing_text).splitlines()
309
+ if line.strip()
310
+ )
311
+ merged_lines = merged.splitlines()
312
+ assert merged_lines[0] != "---", "merged body must not open with a leaked frontmatter fence"
313
+ assert merged_lines[0] == first_content_line
314
+ body_before_reference = "\n".join(
315
+ merged_lines[: merged_lines.index("## Reference (full text: `.cursor/docs/TEST_QUALITY.md`)")]
316
+ )
317
+ assert "paths:" not in body_before_reference, "testing.md paths frontmatter leaked into rule body"
@@ -43,6 +43,8 @@ ALL_AUDIT_CATEGORY_ENTRIES = [
43
43
  ("L", "Behavior-equivalence for refactors"),
44
44
  ("M", "Producer/consumer cardinality vs collection-type contract"),
45
45
  ("N", "Test-name scenario verifier"),
46
+ ("O", "Docstring / fixture-prose vs implementation drift"),
47
+ ("P", "Name / regex / word-list vs behavior-contract precision"),
46
48
  ]
47
49
 
48
50
  AUDIT_RUBRIC_REFERENCE_TEXT = (
@@ -1,4 +1,4 @@
1
- """Tests pinning build_audit_prompt's emitted A-N category taxonomy."""
1
+ """Tests pinning build_audit_prompt's emitted A-P category taxonomy."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -18,7 +18,7 @@ from skills_pr_loop_constants.path_resolver_constants import (
18
18
  )
19
19
 
20
20
  _CATEGORY_RUBRICS_DIR = _SCRIPTS_DIR.parents[3] / "audit-rubrics" / "category_rubrics"
21
- _HEADING_PATTERN = re.compile(r"^# Category ([A-N]) — (.+)$")
21
+ _HEADING_PATTERN = re.compile(r"^# Category ([A-P]) — (.+)$")
22
22
 
23
23
 
24
24
  def _load_build_audit_prompt() -> ModuleType:
@@ -60,12 +60,12 @@ def _build_audit_root() -> Element:
60
60
  )
61
61
 
62
62
 
63
- def test_bug_categories_carry_ids_a_through_n_in_order() -> None:
63
+ def test_bug_categories_carry_ids_a_through_p_in_order() -> None:
64
64
  root = _build_audit_root()
65
65
  bug_categories = root.find("bug_categories")
66
66
  assert bug_categories is not None
67
67
  all_emitted_ids = [each_category.get("id") for each_category in bug_categories]
68
- all_expected_ids = list("ABCDEFGHIJKLMN")
68
+ all_expected_ids = list("ABCDEFGHIJKLMNOP")
69
69
  assert all_emitted_ids == all_expected_ids
70
70
 
71
71
 
@@ -90,3 +90,50 @@ def test_rubric_reference_element_names_category_rubrics_directory() -> None:
90
90
  assert rubric_reference is not None
91
91
  assert rubric_reference.text is not None
92
92
  assert "audit-rubrics/category_rubrics" in rubric_reference.text
93
+
94
+
95
+ def test_prompt_skeleton_sub_bucket_counts_match_rubric_rows() -> None:
96
+ """Each prompt skeleton's numeric sub-bucket count equals its rubric's row count.
97
+
98
+ For every (letter, label) the prompts dir holds a category-<letter>- file.
99
+ The skeleton above the first standalone --- line states "decomposed into N
100
+ sub-buckets"; that N must equal the rubric's count of | <letter>N | rows,
101
+ and a numeric walk-instruction range (For each sub-bucket X1-Xn) must end
102
+ at that same row count. Skeletons with a [N] placeholder are skipped.
103
+ """
104
+ prompts_directory = _CATEGORY_RUBRICS_DIR.parent / "prompts"
105
+ count_pattern = re.compile(r"decomposed into (\d+) sub-buckets")
106
+ for each_letter, _each_label in ALL_AUDIT_CATEGORY_ENTRIES:
107
+ all_prompt_matches = sorted(prompts_directory.glob(f"category-{each_letter.lower()}-*.md"))
108
+ assert all_prompt_matches, f"Missing prompt file for category {each_letter}"
109
+ all_skeleton_lines: list[str] = []
110
+ for each_line in all_prompt_matches[0].read_text(encoding="utf-8").splitlines():
111
+ if each_line == "---":
112
+ break
113
+ all_skeleton_lines.append(each_line)
114
+ skeleton_text = "\n".join(all_skeleton_lines)
115
+ each_count_match = count_pattern.search(skeleton_text)
116
+ if each_count_match is None:
117
+ assert "decomposed into [N] sub-buckets" in skeleton_text, (
118
+ f"Category {each_letter}: skeleton neither states a numeric "
119
+ "sub-bucket count nor carries the [N] placeholder"
120
+ )
121
+ continue
122
+ all_rubric_matches = sorted(_CATEGORY_RUBRICS_DIR.glob(f"category-{each_letter.lower()}-*.md"))
123
+ assert all_rubric_matches, f"Missing rubric file for category {each_letter}"
124
+ rubric_row_pattern = re.compile(r"^\| " + each_letter + r"\d+ \|", re.MULTILINE)
125
+ sub_bucket_row_count = len(rubric_row_pattern.findall(all_rubric_matches[0].read_text(encoding="utf-8")))
126
+ assert int(each_count_match.group(1)) == sub_bucket_row_count, (
127
+ f"Category {each_letter}: skeleton says {each_count_match.group(1)} sub-buckets "
128
+ f"but rubric has {sub_bucket_row_count} rows"
129
+ )
130
+ walk_range_pattern = re.compile(
131
+ rf"For each sub-bucket {each_letter}1[-–]{each_letter}(\d+)"
132
+ )
133
+ each_walk_match = walk_range_pattern.search(skeleton_text)
134
+ if each_walk_match is not None:
135
+ assert int(each_walk_match.group(1)) == sub_bucket_row_count, (
136
+ f"Category {each_letter}: walk instruction ends at "
137
+ f"{each_letter}{each_walk_match.group(1)} but rubric has "
138
+ f"{sub_bucket_row_count} rows"
139
+ )