claude-dev-env 8.11.1 → 8.13.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 (105) hide show
  1. package/.agents/skills/e-code-review/SKILL.md +24 -71
  2. package/.agents/skills/e-code-review/reference/fix.md +7 -78
  3. package/.agents/skills/e-code-review/reference/high.md +129 -0
  4. package/.agents/skills/e-code-review/reference/low.md +20 -51
  5. package/.agents/skills/e-code-review/reference/max.md +169 -0
  6. package/.agents/skills/e-code-review/reference/medium.md +7 -191
  7. package/.agents/skills/e-code-review/reference/xhigh.md +47 -91
  8. package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
  9. package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
  10. package/_shared/advisor/reference/astra-rung.md +7 -7
  11. package/_shared/advisor/reference/third-party-bind.md +2 -2
  12. package/_shared/advisor/scripts/codex_astra_advisor.py +33 -26
  13. package/_shared/advisor/scripts/codex_astra_preflight.py +57 -55
  14. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
  15. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
  16. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
  17. package/_shared/pr-loop/scripts/check_convergence.py +54 -49
  18. package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
  19. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
  20. package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
  21. package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
  22. package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
  23. package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
  24. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
  25. package/bin/git_hooks_installer.test.mjs +1 -1
  26. package/bin/install-constants.mjs +1 -6
  27. package/bin/install.agents-home.test.mjs +9 -9
  28. package/bin/install.cursor-rules.test.mjs +24 -51
  29. package/bin/install.mjs +16 -70
  30. package/commands/sr-loop.md +3 -1
  31. package/docs/codex-accounts.md +79 -0
  32. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
  33. package/hooks/blocking/test_pii_scanner.py +1 -1
  34. package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
  35. package/hooks/git-hooks/conftest.py +16 -0
  36. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
  37. package/hooks/git-hooks/test_post_commit.py +1 -1
  38. package/hooks/git-hooks/test_pre_commit.py +1 -1
  39. package/hooks/git-hooks/test_pre_push.py +2 -2
  40. package/hooks/git-hooks/test_verification_start.py +1 -1
  41. package/hooks/git-hooks/verification_notice.py +2 -2
  42. package/hooks/git-hooks/verification_notice_context.py +21 -2
  43. package/hooks/git-hooks/verification_start.py +2 -3
  44. package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -1
  45. package/package.json +1 -1
  46. package/rules/durable-post-artifacts.md +4 -3
  47. package/scripts/claude_account_profile.py +21 -9
  48. package/scripts/codex_account_choice.py +396 -0
  49. package/scripts/codex_account_meters.py +372 -0
  50. package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
  51. package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
  52. package/scripts/private_term_scan.py +6 -2
  53. package/scripts/repository_checks/config/constants.py +1 -0
  54. package/scripts/repository_checks/tracked_private_terms.py +7 -0
  55. package/scripts/test_claude_account_profile.py +12 -0
  56. package/scripts/test_codex_account_choice.py +276 -0
  57. package/scripts/test_codex_account_meters.py +205 -0
  58. package/scripts/tests/test_automatic_advisory.py +3 -3
  59. package/scripts/tests/test_automatic_advisory_polling.py +1 -1
  60. package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
  61. package/scripts/tests/test_closed_pr_label.py +1 -1
  62. package/scripts/tests/test_private_term_scan.py +28 -0
  63. package/scripts/tests/test_review_closure.py +2 -2
  64. package/scripts/tests/test_review_closure_model.py +2 -2
  65. package/scripts/tests/test_rules.py +2 -83
  66. package/scripts/tests/test_tracked_private_terms.py +15 -0
  67. package/scripts/tests/test_verification_notice.py +29 -13
  68. package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
  69. package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
  70. package/.agents/skills/e-code-review/preflight-proposal.contract.test.mjs +0 -20
  71. package/.agents/skills/e-code-review/reference/effort-evaluation.md +0 -35
  72. package/.agents/skills/e-code-review/reference/loop.md +0 -261
  73. package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +0 -67
  74. package/.agents/skills/e-code-review/reference/preflight-proposal.md +0 -75
  75. package/.agents/skills/e-code-review/reference/runner-selection.md +0 -40
  76. package/.agents/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +0 -41
  77. package/.agents/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +0 -40
  78. package/.agents/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +0 -1
  79. package/.agents/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +0 -49
  80. package/.agents/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +0 -55
  81. package/.agents/skills/e-code-review/scripts/effort_defaults_evidence.json +0 -186
  82. package/.agents/skills/e-code-review/scripts/effort_evaluation.py +0 -362
  83. package/.agents/skills/e-code-review/scripts/finding_pipeline.py +0 -140
  84. package/.agents/skills/e-code-review/scripts/fixtures/demanding.json +0 -26
  85. package/.agents/skills/e-code-review/scripts/fixtures/easy.json +0 -14
  86. package/.agents/skills/e-code-review/scripts/fixtures/medium.json +0 -20
  87. package/.agents/skills/e-code-review/scripts/grok_code_review.py +0 -230
  88. package/.agents/skills/e-code-review/scripts/test_effort_evaluation.py +0 -180
  89. package/.agents/skills/e-code-review/scripts/test_finding_pipeline.py +0 -197
  90. package/.agents/skills/e-code-review/scripts/test_grok_code_review.py +0 -289
  91. package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
  92. package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
  93. package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
  94. package/scripts/sync_to_cursor/AGENTS.md +0 -1
  95. package/scripts/sync_to_cursor/CLAUDE.md +0 -1
  96. package/scripts/sync_to_cursor/__init__.py +0 -13
  97. package/scripts/sync_to_cursor/canonical_docs.py +0 -66
  98. package/scripts/sync_to_cursor/config/__init__.py +0 -8
  99. package/scripts/sync_to_cursor/engine.py +0 -219
  100. package/scripts/sync_to_cursor/hashing.py +0 -7
  101. package/scripts/sync_to_cursor/paths.py +0 -18
  102. package/scripts/sync_to_cursor/rules.py +0 -494
  103. package/scripts/sync_to_cursor.py +0 -22
  104. package/scripts/tests/test_engine.py +0 -101
  105. package/scripts/tests/test_sync_to_cursor.py +0 -315
@@ -1,81 +1,10 @@
1
- """Tests for discovered Claude-to-Cursor rule mappings."""
1
+ """Tests that shipped instruction surfaces carry the comment and word policies."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import sys
6
5
  from pathlib import Path
7
6
 
8
- _SCRIPTS_DIR = Path(__file__).resolve().parent.parent
9
- if str(_SCRIPTS_DIR) not in sys.path:
10
- sys.path.insert(0, str(_SCRIPTS_DIR))
11
-
12
- from sync_to_cursor.rules import build_mappings, merge_code_standards
13
-
14
- _PACKAGE_ROOT = _SCRIPTS_DIR.parent
15
- _SKIPPED_RULE_FILE_NAMES = frozenset({"CLAUDE.md", "AGENTS.md"})
16
-
17
-
18
- def _write_minimal_curated_rules(rules_directory: Path) -> None:
19
- rules_directory.mkdir(parents=True, exist_ok=True)
20
- (rules_directory / "code-standards.md").write_text(
21
- "# Code standards stub\n", encoding="utf-8"
22
- )
23
- (rules_directory / "tasklings-preferences.md").write_text(
24
- '---\npaths:\n - "Y:/x/**"\n---\n\n# Tasklings\n',
25
- encoding="utf-8",
26
- )
27
- (rules_directory / "bdd.md").write_text("# BDD\n", encoding="utf-8")
28
- (rules_directory / "testing.md").write_text(
29
- '---\npaths:\n - "**/test_*.py"\n---\n\n# Testing\n',
30
- encoding="utf-8",
31
- )
32
- (rules_directory / "research-mode.md").write_text("# RM\n", encoding="utf-8")
33
- (rules_directory / "explore-thoroughly.md").write_text("# ET\n", encoding="utf-8")
34
-
35
-
36
- def test_build_mappings_emits_stem_mdc_for_remaining_claude_rules(
37
- tmp_path: Path,
38
- ) -> None:
39
- claude = tmp_path / ".claude"
40
- _write_minimal_curated_rules(claude / "rules")
41
- (claude / "docs").mkdir(parents=True, exist_ok=True)
42
- (claude / "rules" / "asd-ste100-language.md").write_text(
43
- "# ASD-STE100 Language Policy\n\nBe brief.\n",
44
- encoding="utf-8",
45
- )
46
- (claude / "rules" / "CLAUDE.md").write_text(
47
- "# Package inventory\n", encoding="utf-8"
48
- )
49
- (claude / "rules" / "AGENTS.md").write_text("# Agent inventory\n", encoding="utf-8")
50
- mappings = build_mappings(claude)
51
- output_by_key = {each_mapping.key: each_mapping for each_mapping in mappings}
52
- discovered = output_by_key["asd-ste100-language"]
53
- assert discovered.output_name == "asd-ste100-language.mdc"
54
- assert discovered.always_apply is True
55
- assert discovered.description == "ASD-STE100 Language Policy"
56
- assert "CLAUDE.md" not in {
57
- each_source.name
58
- for each_mapping in mappings
59
- for each_source in each_mapping.sources
60
- }
61
- assert "AGENTS.md" not in {
62
- each_source.name
63
- for each_mapping in mappings
64
- for each_source in each_mapping.sources
65
- }
66
-
67
-
68
- def test_every_shipped_claude_rule_maps_to_an_mdc() -> None:
69
- mappings = build_mappings(_PACKAGE_ROOT)
70
- output_name_by_rule_file = {}
71
- for each_mapping in mappings:
72
- for each_source in each_mapping.sources:
73
- if each_source.parent.name == "rules" and each_source.suffix == ".md":
74
- output_name_by_rule_file[each_source.name] = each_mapping.output_name
75
- for each_rule_file in sorted((_PACKAGE_ROOT / "rules").glob("*.md")):
76
- if each_rule_file.name in _SKIPPED_RULE_FILE_NAMES:
77
- continue
78
- assert each_rule_file.name in output_name_by_rule_file, each_rule_file.name
7
+ _PACKAGE_ROOT = Path(__file__).resolve().parents[2]
79
8
 
80
9
 
81
10
  def _comment_policy_phrases() -> tuple[str, ...]:
@@ -226,13 +155,3 @@ def test_comment_policy_removes_directive_justification_guidance() -> None:
226
155
  assert "changed `# noqa` directives are removed or resolved" in category_e_prompt
227
156
  assert "every `# noqa` on an import line must be justified" not in category_e_prompt
228
157
 
229
-
230
- def test_merge_code_standards_resolves_every_heading_it_names() -> None:
231
- merged_text = merge_code_standards(
232
- (
233
- _PACKAGE_ROOT / "rules" / "code-standards.md",
234
- _PACKAGE_ROOT / "docs" / "CODE_RULES.md",
235
- )
236
- )
237
- assert "## Reference (full text:" in merged_text
238
- assert "## COMMENT PRESERVATION" in merged_text
@@ -60,6 +60,21 @@ def test_should_flag_a_tracked_file_naming_a_private_organization(
60
60
  assert "Acme" not in stdout_text
61
61
 
62
62
 
63
+ def test_should_skip_a_changelog_that_names_a_private_organization(
64
+ tmp_path: Path,
65
+ ) -> None:
66
+ repository_root = tmp_path / "repo"
67
+ initialize_repository(repository_root)
68
+ write_text(
69
+ repository_root / "packages" / "tool" / "CHANGELOG.md",
70
+ "* Built by Acme Widgets.\n",
71
+ )
72
+ commit_tracked_files(repository_root)
73
+ exit_code, stdout_text, _stderr_text = run_policy(repository_root)
74
+ assert exit_code == SUCCESS_EXIT_CODE
75
+ assert stdout_text == ""
76
+
77
+
63
78
  def test_should_pass_a_tree_that_names_no_private_organization(
64
79
  tmp_path: Path,
65
80
  ) -> None:
@@ -10,16 +10,23 @@ from pathlib import Path
10
10
  HOOKS_DIRECTORY = Path(__file__).resolve().parents[2] / "hooks" / "git-hooks"
11
11
  NOTICE_SCRIPT = HOOKS_DIRECTORY / "verification_notice.py"
12
12
  ALL_REMOTE_URLS = (
13
- "https://github.com/JonEcho/python-automation.git",
14
- "git@github.com:JonEcho/python-automation.git",
15
- "ssh://git@github.com/JonEcho/python-automation.git",
13
+ "https://github.com/example-owner/example-repo.git",
14
+ "git@github.com:example-owner/example-repo.git",
15
+ "ssh://git@github.com/example-owner/example-repo.git",
16
16
  )
17
+ TARGET_REPOSITORY_SETTING = "CLAUDE_VERIFICATION_TARGET_REPOSITORY"
18
+ EXAMPLE_TARGET_REPOSITORY = "example-owner/example-repo"
17
19
 
18
20
 
19
21
  def run_notice(
20
22
  repository_path: Path,
21
23
  event_name: str = "commit",
24
+ target_repository: str | None = EXAMPLE_TARGET_REPOSITORY,
22
25
  ) -> subprocess.CompletedProcess[str]:
26
+ notice_environment = {**os.environ, "PYTHONIOENCODING": "utf-8"}
27
+ notice_environment.pop(TARGET_REPOSITORY_SETTING, None)
28
+ if target_repository is not None:
29
+ notice_environment[TARGET_REPOSITORY_SETTING] = target_repository
23
30
  return subprocess.run(
24
31
  [
25
32
  sys.executable,
@@ -33,7 +40,7 @@ def run_notice(
33
40
  capture_output=True,
34
41
  text=True,
35
42
  encoding="utf-8",
36
- env={**os.environ, "PYTHONIOENCODING": "utf-8"},
43
+ env=notice_environment,
37
44
  )
38
45
 
39
46
 
@@ -41,7 +48,7 @@ def create_target_repository(
41
48
  temporary_directory: Path,
42
49
  remote_url: str,
43
50
  ) -> tuple[Path, str]:
44
- repository_path = temporary_directory / "Jon Echo project with spaces Ω"
51
+ repository_path = temporary_directory / "Example project with spaces Ω"
45
52
  repository_path.mkdir(parents=True)
46
53
  run_git(repository_path, "init", "--quiet")
47
54
  run_git(repository_path, "config", "user.email", "test@example.invalid")
@@ -173,7 +180,7 @@ def test_notice_reports_pending_when_manifest_exists_without_report(
173
180
  ) -> None:
174
181
  repository_path, _ = create_target_repository(
175
182
  tmp_path,
176
- "https://github.com/JonEcho/python-automation.git",
183
+ "https://github.com/example-owner/example-repo.git",
177
184
  )
178
185
  write_report(repository_path, {})
179
186
  (repository_path / ".git" / "local-verification" / "report.json").unlink()
@@ -199,7 +206,7 @@ def test_notice_ignores_unrelated_remote(tmp_path: Path) -> None:
199
206
  def test_notice_reports_failed_checks_without_pass_label(tmp_path: Path) -> None:
200
207
  repository_path, current_head = create_target_repository(
201
208
  tmp_path,
202
- "https://github.com/JonEcho/python-automation.git",
209
+ "https://github.com/example-owner/example-repo.git",
203
210
  )
204
211
  write_report(repository_path, {"head": current_head, "status": "failed"})
205
212
  completed_notice = run_notice(repository_path)
@@ -213,7 +220,7 @@ def test_notice_reports_failed_checks_without_pass_label(tmp_path: Path) -> None
213
220
  def test_notice_reports_pass_only_for_complete_current_revision(tmp_path: Path) -> None:
214
221
  repository_path, _ = create_target_repository(
215
222
  tmp_path,
216
- "https://github.com/JonEcho/python-automation.git",
223
+ "https://github.com/example-owner/example-repo.git",
217
224
  )
218
225
  current_head = write_passing_report(repository_path)
219
226
  completed_notice = run_notice(repository_path)
@@ -229,7 +236,7 @@ def test_notice_reports_pass_only_for_complete_current_revision(tmp_path: Path)
229
236
  def test_notice_handles_malformed_report_and_missing_repository(tmp_path: Path) -> None:
230
237
  repository_path, _ = create_target_repository(
231
238
  tmp_path,
232
- "https://github.com/JonEcho/python-automation.git",
239
+ "https://github.com/example-owner/example-repo.git",
233
240
  )
234
241
  manifest_path = repository_path / "config" / "local-verification.json"
235
242
  manifest_path.parent.mkdir(exist_ok=True)
@@ -261,7 +268,7 @@ def test_notice_handles_malformed_report_and_missing_repository(tmp_path: Path)
261
268
  def test_notice_uses_worktree_specific_report_path(tmp_path: Path) -> None:
262
269
  repository_path, _ = create_target_repository(
263
270
  tmp_path / "main",
264
- "https://github.com/JonEcho/python-automation.git",
271
+ "https://github.com/example-owner/example-repo.git",
265
272
  )
266
273
  linked_worktree_path = tmp_path / "linked worktree Ω"
267
274
  run_git(repository_path, "worktree", "add", "--detach", str(linked_worktree_path))
@@ -281,7 +288,7 @@ def test_notice_uses_worktree_specific_report_path(tmp_path: Path) -> None:
281
288
  def test_notice_accepts_complete_report_in_worktree_metadata(tmp_path: Path) -> None:
282
289
  repository_path, _ = create_target_repository(
283
290
  tmp_path / "main",
284
- "https://github.com/JonEcho/python-automation.git",
291
+ "https://github.com/example-owner/example-repo.git",
285
292
  )
286
293
  current_head = write_passing_report(repository_path)
287
294
  linked_worktree_path = tmp_path / "linked worktree Ω"
@@ -312,7 +319,7 @@ def test_notice_accepts_complete_report_in_worktree_metadata(tmp_path: Path) ->
312
319
  def test_notice_invalidates_pass_after_worktree_edit(tmp_path: Path) -> None:
313
320
  repository_path, _ = create_target_repository(
314
321
  tmp_path,
315
- "https://github.com/JonEcho/python-automation.git",
322
+ "https://github.com/example-owner/example-repo.git",
316
323
  )
317
324
  current_head = write_passing_report(repository_path)
318
325
  passed_notice = run_notice(repository_path)
@@ -329,7 +336,7 @@ def test_notice_invalidates_pass_after_worktree_edit(tmp_path: Path) -> None:
329
336
  def test_notice_invalidates_pass_after_origin_base_moves(tmp_path: Path) -> None:
330
337
  repository_path, _ = create_target_repository(
331
338
  tmp_path,
332
- "https://github.com/JonEcho/python-automation.git",
339
+ "https://github.com/example-owner/example-repo.git",
333
340
  )
334
341
  current_head = write_passing_report(repository_path)
335
342
  passed_notice = run_notice(repository_path)
@@ -345,3 +352,12 @@ def test_notice_invalidates_pass_after_origin_base_moves(tmp_path: Path) -> None
345
352
  assert "State: passed" in passed_notice.stdout
346
353
  assert "Verified SHA: none" in moved_base_notice.stdout
347
354
  assert "State: unverified" in moved_base_notice.stdout
355
+
356
+
357
+ def test_notice_stays_silent_without_a_configured_target(tmp_path: Path) -> None:
358
+ repository_path, _ = create_target_repository(tmp_path, ALL_REMOTE_URLS[0])
359
+
360
+ completed_process = run_notice(repository_path, target_repository=None)
361
+
362
+ assert completed_process.returncode == 0
363
+ assert completed_process.stdout == ""
@@ -1,224 +0,0 @@
1
- ---
2
- name: cert-classification-rule
3
- description: Add or change a Samsung cert-failure classification rule (fixable, unfixable, mixed). Use when the user says add a fixable item, add an unfixable item, mark this rejection unfixable, add a mixed item, classify this cert rejection, or add a cert rule. Covers the unfixable pattern config, the paired production-path test, and the re-sort recipe for already-filed folders.
4
- ---
5
-
6
- # Cert-Failure Classification Rule
7
-
8
- Add or change how a Samsung Galaxy Store certification rejection is sorted into one of four
9
- buckets: `Fold`, `unfixable`, `mixed`, or `fixable`. A rejection lands in a bucket from its
10
- must-fix issue titles plus a Fold device check, and the bucket decides which subfolder holds
11
- the theme folder.
12
-
13
- ## Contents
14
-
15
- - [When to use](#when-to-use)
16
- - [Procedure checklist](#procedure-checklist)
17
- - [The classification model](#the-classification-model)
18
- - [How to add an unfixable title pattern](#how-to-add-an-unfixable-title-pattern)
19
- - [How to add the paired production-path test](#how-to-add-the-paired-production-path-test)
20
- - [Re-sort already-filed folders for a day](#re-sort-already-filed-folders-for-a-day)
21
- - [Gotchas](#gotchas)
22
- - [File index](#file-index)
23
- - [Folder map](#folder-map)
24
-
25
- ## When to use
26
-
27
- Use this skill when the user asks to:
28
-
29
- - add a fixable item, add an unfixable item, or add a mixed item
30
- - mark a rejection unfixable, or classify a cert rejection
31
- - add a cert rule
32
-
33
- Almost every such request is one new unfixable title pattern plus its paired test. The
34
- `fixable` and `mixed` buckets are not configured by hand (see the model below), so a request
35
- to "add a fixable item" usually means "stop treating this title as unfixable" or "confirm
36
- this title stays fixable with a guard test".
37
-
38
- ### When a title pattern cannot help
39
-
40
- If the rejection's only unfixable cue lives in the report description (the `L_TESTRESULT`
41
- text, for example `*Not Identifiable`) and no must-fix title carries it, stop and tell the
42
- user this exact point:
43
-
44
- > This rejection's only unfixable signal is in the report description, not a must-fix title.
45
- > The classifier reads titles only, so no pattern here will catch it — teaching it to read
46
- > the description is a code change to the classifier, not a config rule.
47
-
48
- ## Procedure checklist
49
-
50
- Copy this and check each item off as you go:
51
-
52
- - [ ] Confirm the unfixable cue is in a must-fix **title**, not only the description (see Gotchas).
53
- - [ ] Append the new `{pattern, label, review_note}` after the Gallery entry in `unfixable_patterns.json`.
54
- - [ ] Add a paired production-path test: the live title classifies as `unfixable`, plus a negative guard for an out-of-scope title.
55
- - [ ] Run the tests from the `shared_utils` directory and confirm green.
56
- - [ ] Re-sort the affected day folders with the re-sort recipe (dry run first, then `--apply`).
57
-
58
- ## The classification model
59
-
60
- `classify_failure(issue_titles, unfixable_patterns)` in
61
- `shared_utils/samsung_utils/cert_failure_classifier.py:71` decides the bucket from the
62
- must-fix issue titles:
63
-
64
- - **unfixable** — every title matches an unfixable pattern.
65
- - **mixed** — some titles match, some do not. This bucket is **derived**, never configured.
66
- The classifier returns it once at least one title matches an unfixable pattern and at least
67
- one does not.
68
- - **fixable** — no title matches any unfixable pattern. This is the **default**: a rejection
69
- needs no pattern to be fixable.
70
- - **None** — there are no must-fix titles at all.
71
-
72
- Only the `unfixable_patterns` list is authored. Adding an unfixable pattern is the only way to
73
- move a title out of the default `fixable` bucket.
74
-
75
- **Visibility auto-fix (Stage 1) also gates on these patterns.**
76
- `classify_visibility_failure_title` in `visibility_failure_classifier.py` calls
77
- `get_matched_unfixable_patterns` / `load_unfixable_patterns` so a title that maps to a
78
- curated colour slot still sets `is_blocked_from_auto_fix=True` (and
79
- `can_auto_fix(record)` is False) when it matches Gallery, Clock system-UI, lock-screen
80
- video wallpaper crop, Quick-panel preview mismatch, or any other unfixable pattern.
81
- Changing a pattern here therefore changes both folder sorting and visibility auto-fix
82
- eligibility.
83
-
84
- **Lock-screen video wallpaper crop stays UNFIXABLE.** Pad-to-canvas (`lock_wallpaper_cover`,
85
- #1449) and tall-aspect image-gen outpaint (`lock_aspect_regen`, #1454) were tried and
86
- abandoned; Themes Studio automation for this class was cancelled. Do not reopen a crop,
87
- outpaint, pad, or Primary/color autofix for Blue Edge-style "cut on both side(s)" titles.
88
-
89
- The categorizer (`FailureCategorizer.categorize_and_move` in
90
- `shared_utils/samsung_utils/cert_failure_processor/failure_categorizer.py:65`) runs the Fold
91
- device check and the classifier, then resolves a single bucket in
92
- `_resolve_category_priority` (`failure_categorizer.py:105`):
93
-
94
- - An `unfixable` or `mixed` classification overrides the detector — these are the
95
- `severity_priority_buckets` in
96
- `shared_utils/samsung_utils/config/cert_failure.py:76` (`("unfixable", "mixed")`).
97
- - A `Fold` detector category is kept only when the classification is `fixable`.
98
- - A `fixable` classification applies only when no detector matched.
99
-
100
- `FoldDetector` (`failure_categorizer.py:37`) matches the device regex `SM-F[79]\d{2}[A-Z]?` or
101
- the keyword `fold` (config in `cert_failure.py:52`).
102
-
103
- ## How to add an unfixable title pattern
104
-
105
- Patterns live in `shared_utils/samsung_utils/config/unfixable_patterns.json` as a JSON list
106
- under the `unfixable_patterns` key. Each entry is `{pattern, label, review_note}`.
107
-
108
- **The Gallery entry stays first (index 0).** The tests in
109
- `test_cert_failure_classifier.py::TestDefaultConfigPath` assert `result[0]` is the Gallery
110
- pattern. Append every new pattern after it; never insert ahead of it.
111
-
112
- Live pattern shape, anchored on the rejected surface (Gallery is the shipped example):
113
-
114
- ```json
115
- {
116
- "pattern": "(?i)<surface>.*poor visibility|poor visibility.*<surface>",
117
- "label": "<Surface> visibility",
118
- "review_note": "<honest note about the issue>"
119
- }
120
- ```
121
-
122
- `(?i)` is case-insensitive. The two-sided alternation matches the surface either before or
123
- after the "poor visibility" phrase, because Samsung phrases the same title both ways. Replace
124
- `<surface>` with the lowercase app or screen name (`settings`, `clock`, `calculator`). Match
125
- against the **title** text the report shows, not the description.
126
-
127
- ### Writing the review_note honestly
128
-
129
- `review_note` is the message the per-pattern explanation would carry in a review reply.
130
- `DEFAULT_THEME_REVIEW_NOTES` still calls `compose_review_notes` with an empty matched-pattern
131
- list. Devices-only, Quick-panel, and sound-preview labels in `cert_portal_notes_update_config.all_labels`
132
- are the exception: `resubmit_via_notes_only_update` APPENDs those `review_note` sentences on
133
- the existing ThemeUpdateProcessor path (search → Re-register → Certification Notes). Write a
134
- truthful note, because it becomes the pasted sentence for those classes.
135
-
136
- Do **not** fabricate a "Samsung confirmed" claim. State only what is true. The shipped Gallery
137
- note is true because Samsung Support did confirm that issue; a new pattern for a different
138
- issue must not borrow that wording unless the confirmation happened.
139
-
140
- ## How to add the paired production-path test
141
-
142
- Every new unfixable pattern gets a paired test in
143
- `shared_utils/samsung_utils/tests/test_cert_failure_classifier.py` that drives the production
144
- path:
145
-
146
- 1. Load the patterns with `load_unfixable_patterns()` (no path argument, so it reads the
147
- shipped config).
148
- 2. Build the report HTML with the live must-fix title text, run it through
149
- `extract_issue_titles`, then `classify_failure`, and assert the result is `unfixable`.
150
- 3. Add a negative guard: an out-of-scope "poor visibility" title (a surface with no pattern)
151
- classifies as `fixable`, so a future broad regex cannot silently swallow unrelated
152
- rejections.
153
-
154
- Use the exact title strings Samsung's report produces. The existing
155
- `TestClassifyFailure::test_gallery_regex_matches_all_known_variants` shows the shape: a list of
156
- title variants, each asserted as `unfixable` through `classify_failure`.
157
-
158
- Run the tests from the `shared_utils` directory (its pytest config is anchored there):
159
-
160
- ```bash
161
- cd shared_utils && C:\Python313\python.exe -m pytest samsung_utils/tests/test_cert_failure_classifier.py -q
162
- ```
163
-
164
- ## Re-sort already-filed folders for a day
165
-
166
- After adding a pattern, theme folders filed for an earlier day still sit in their old bucket.
167
- Run `scripts/resort_cert_folders.py` to recompute each theme's bucket with the new patterns and
168
- move any folder whose current bucket differs from the computed one. The script reuses the
169
- production `FailureCategorizer` decision helpers (`_parse_rejection_text`, `_detect_category`,
170
- `_classify_from_html`, `_resolve_category_priority`), so it stays in step with the live
171
- categorizer. It runs dry by default and moves only on `--apply`.
172
-
173
- Set `PYTHONPATH` to the repo root so the import resolves the edited `shared_utils` tree rather
174
- than a pip-editable install:
175
-
176
- ```bash
177
- set PYTHONPATH=Y:\Information Technology\Scripts\Automation\Python
178
- C:\Python313\python.exe .claude\skills\cert-classification-rule\scripts\resort_cert_folders.py "<cert base path>" June 18 PRIMARY
179
- C:\Python313\python.exe .claude\skills\cert-classification-rule\scripts\resort_cert_folders.py "<cert base path>" June 18 PRIMARY --apply
180
- ```
181
-
182
- Cert-failure folders live at `<base>\<Month>\<day>\<account_folder>\<bucket>\<theme>`, where
183
- `<account_folder>` is `Themes` for PRIMARY and `Theme Editor Themes` for SECONDARY
184
- (`AccountFolderConfig` in `shared_utils/web_automation/config/account.py:91`).
185
-
186
- `scripts/resort_cert_folders.py` passes the repo `code_rules_enforcer` Write hook: output goes
187
- through a `logger` with %-style format strings (no bare `print()`), the base path is an
188
- argument, every named value is a typed `dataclass` field rather than a module-level
189
- `UPPER_SNAKE` constant, and loop variables carry the `each_` prefix.
190
-
191
- ## Gotchas
192
-
193
- - **Only titles are matched, never the description.** The classifier reads the must-fix issue
194
- **titles** via the `L_TITLE` regex in `extract_issue_titles`
195
- (`cert_failure_classifier.py:40`). The rejection description text — the `L_TESTRESULT` span,
196
- for example a `*Not Identifiable` note — is **not** read by the classifier. A signal that
197
- lives only in the description will not flag a title unless that title also matches a pattern.
198
- When a rejection's only "unfixable" cue is in the description, no title pattern can catch it.
199
- - **Do not reopen crop autofix.** Lock-screen video wallpaper "cut on both side(s)" is
200
- UNFIXABLE. Closed #1449 (pad-to-canvas) and #1454 (tall outpaint) must stay closed.
201
- - **The Gallery entry stays at index 0** of `unfixable_patterns.json`. Tests assert
202
- `result[0]` is Gallery; append new patterns after it.
203
- - **"-" placeholders are excluded.** Recommended-table rows show a `-` title; `extract_issue_titles`
204
- drops them, so they never count toward a classification.
205
- - **Run tests from the `shared_utils` directory.** Its pytest config (`testpaths`,
206
- `asyncio_mode`) is anchored there. A run from the repo root will not collect the tests
207
- correctly.
208
- - **Use `C:\Python313\python.exe`.** The repo-root `.venv` is a Linux/NAS virtual environment
209
- with no `Scripts\python.exe` on Windows.
210
- - **Set `PYTHONPATH` to the repo root for the re-sort.** Without it, the import can resolve a
211
- pip-editable install of `shared_utils` whose config lacks the freshly added pattern, so the
212
- new pattern does not take effect.
213
-
214
- ## File index
215
-
216
- | File | Purpose |
217
- |------|---------|
218
- | `SKILL.md` | This hub — the classification model, the add-a-rule procedure, the test recipe, and gotchas. |
219
- | `scripts/resort_cert_folders.py` | Re-sorts already-filed cert-failure folders for a day after a pattern change. Execute it (see the re-sort section); set `PYTHONPATH` to the repo root first. Dry-run by default, moves on `--apply`. |
220
-
221
- ## Folder map
222
-
223
- - `SKILL.md` — hub.
224
- - `scripts/` — the re-sort utility, executed rather than read into context.
@@ -1,158 +0,0 @@
1
- """Re-sort already-filed cert-failure theme folders after the unfixable patterns change.
2
-
3
- Recomputes each theme's bucket from its HTML report using the production
4
- FailureCategorizer decision helpers, then moves folders whose current bucket no
5
- longer matches. Dry-run by default; pass --apply to move folders.
6
- """
7
-
8
- import argparse
9
- import logging
10
- import shutil
11
- from dataclasses import dataclass
12
- from pathlib import Path
13
-
14
- from shared_utils.samsung_utils.cert_failure_processor.failure_categorizer import (
15
- FailureCategorizer,
16
- )
17
- from shared_utils.web_automation.config.account import (
18
- AccountType,
19
- account_folder_config,
20
- )
21
-
22
- logger = logging.getLogger("resort_cert_folders")
23
-
24
-
25
- @dataclass(frozen=True)
26
- class ResortConfig:
27
- """Naming and discovery constants for the re-sort pass."""
28
-
29
- report_glob: str = "*_Report.html"
30
- log_format: str = "%(message)s"
31
-
32
-
33
- @dataclass(frozen=True)
34
- class FolderMove:
35
- """A single planned move from a theme's current bucket to its computed bucket."""
36
-
37
- theme_folder: Path
38
- current_bucket: str
39
- computed_bucket: str
40
-
41
-
42
- def compute_bucket(categorizer: FailureCategorizer, theme_folder: Path, resort_config: ResortConfig) -> str | None:
43
- """Compute the bucket a theme folder belongs in from its HTML report.
44
-
45
- Args:
46
- categorizer: The production categorizer holding the loaded unfixable patterns.
47
- theme_folder: The theme folder containing the cert-failure HTML report.
48
- resort_config: Discovery and logging constants for this pass.
49
-
50
- Returns:
51
- The computed bucket name, or None when no report is present or no bucket applies.
52
- """
53
- all_reports = list(theme_folder.glob(resort_config.report_glob))
54
- if not all_reports:
55
- logger.warning("No report for '%s' - skipping", theme_folder.name)
56
- return None
57
- html_content = all_reports[0].read_text(encoding="utf-8", errors="ignore")
58
- rejection_text = categorizer._parse_rejection_text(html_content)
59
- detector_category = categorizer._detect_category(html_content, rejection_text)
60
- classification = categorizer._classify_from_html(html_content)
61
- return categorizer._resolve_category_priority(detector_category, classification)
62
-
63
-
64
- def plan_moves(account_root: Path, categorizer: FailureCategorizer, resort_config: ResortConfig) -> list[FolderMove]:
65
- """Plan every move needed to bring a day's account folder into line with the patterns.
66
-
67
- Args:
68
- account_root: The <base>/<Month>/<day>/<account_folder> directory to scan.
69
- categorizer: The production categorizer holding the loaded unfixable patterns.
70
- resort_config: Discovery and logging constants for this pass.
71
-
72
- Returns:
73
- One FolderMove per theme whose current bucket differs from its computed bucket.
74
- """
75
- planned_moves: list[FolderMove] = []
76
- for each_bucket_dir in sorted(path for path in account_root.iterdir() if path.is_dir()):
77
- current_bucket = each_bucket_dir.name
78
- for each_theme in sorted(path for path in each_bucket_dir.iterdir() if path.is_dir()):
79
- computed_bucket = compute_bucket(categorizer, each_theme, resort_config)
80
- if computed_bucket is not None and computed_bucket != current_bucket:
81
- planned_moves.append(
82
- FolderMove(
83
- theme_folder=each_theme,
84
- current_bucket=current_bucket,
85
- computed_bucket=computed_bucket,
86
- )
87
- )
88
- return planned_moves
89
-
90
-
91
- def apply_move(account_root: Path, folder_move: FolderMove) -> None:
92
- """Move one theme folder into its computed bucket directory.
93
-
94
- Args:
95
- account_root: The <base>/<Month>/<day>/<account_folder> directory.
96
- folder_move: The planned move to carry out.
97
- """
98
- target_dir = account_root / folder_move.computed_bucket
99
- target_dir.mkdir(parents=True, exist_ok=True)
100
- target_path = target_dir / folder_move.theme_folder.name
101
- if target_path.exists():
102
- logger.warning("Target exists for '%s' - skipping", folder_move.theme_folder.name)
103
- return
104
- shutil.move(str(folder_move.theme_folder), str(target_path))
105
-
106
-
107
- def run_resort(base_path: str, month: str, day: str, account_label: str, should_apply: bool) -> None:
108
- """Plan and optionally apply a re-sort of one day's cert-failure folders.
109
-
110
- Args:
111
- base_path: The cert-failure base directory.
112
- month: The month folder name (for example "June").
113
- day: The day folder name (for example "18").
114
- account_label: "PRIMARY" or "SECONDARY".
115
- should_apply: When True, move folders; when False, only log the plan.
116
- """
117
- resort_config = ResortConfig()
118
- logging.basicConfig(level=logging.INFO, format=resort_config.log_format)
119
- account_type = AccountType[account_label]
120
- account_folder = account_folder_config.get_folder_name(account_type)
121
- account_root = Path(base_path) / month / day / account_folder
122
- categorizer = FailureCategorizer(base_path)
123
- planned_moves = plan_moves(account_root, categorizer, resort_config)
124
- if not planned_moves:
125
- logger.info("Nothing to re-sort in %s", account_root)
126
- return
127
- for each_move in planned_moves:
128
- logger.info(
129
- "%s: %s -> %s",
130
- each_move.theme_folder.name,
131
- each_move.current_bucket,
132
- each_move.computed_bucket,
133
- )
134
- if should_apply:
135
- apply_move(account_root, each_move)
136
- logger.info("%d folder(s) %s", len(planned_moves), "moved" if should_apply else "planned (dry run)")
137
-
138
-
139
- def main() -> None:
140
- """Parse arguments and run the re-sort pass."""
141
- parser = argparse.ArgumentParser(description="Re-sort cert-failure folders after a pattern change.")
142
- parser.add_argument("base_path", help="Cert-failure base directory.")
143
- parser.add_argument("month", help='Month folder name, for example "June".')
144
- parser.add_argument("day", help='Day folder name, for example "18".')
145
- parser.add_argument("account", choices=["PRIMARY", "SECONDARY"], help="Account type.")
146
- parser.add_argument("--apply", action="store_true", help="Move folders instead of dry-running.")
147
- parsed_arguments = parser.parse_args()
148
- run_resort(
149
- parsed_arguments.base_path,
150
- parsed_arguments.month,
151
- parsed_arguments.day,
152
- parsed_arguments.account,
153
- parsed_arguments.apply,
154
- )
155
-
156
-
157
- if __name__ == "__main__":
158
- main()
@@ -1,20 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import { readFileSync } from 'node:fs'
3
- import { dirname, resolve } from 'node:path'
4
- import test from 'node:test'
5
- import { fileURLToPath } from 'node:url'
6
-
7
- const here = dirname(fileURLToPath(import.meta.url))
8
- const skill = readFileSync(resolve(here, 'SKILL.md'), 'utf8')
9
- const contract = readFileSync(resolve(here, 'reference/preflight-proposal.md'), 'utf8')
10
-
11
- test('e-code-review owns its preflight proposal contract', () => {
12
- assert.match(skill, /reference\/preflight-proposal\.md/)
13
- assert.doesNotMatch(skill, /_shared\/pr-loop/)
14
- assert.match(contract, /caller-supplied isolated worktree/)
15
- assert.match(contract, /exact `base_sha\.\.\.head_sha` range/)
16
- assert.match(contract, /immutable proposal identity/)
17
- assert.match(contract, /selected or dispositioned proposal collection/)
18
- assert.match(contract, /review_level: low \| medium \| xhigh/)
19
- assert.match(contract, /outcome: fixed \| no_change_needed \| skipped/)
20
- })