claude-dev-env 8.12.0 → 8.14.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.
- package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
- package/.agents/skills/team-advisor/SKILL.md +1 -1
- package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
- package/_shared/advisor/advisor-protocol.md +19 -19
- package/_shared/advisor/reference/advisor-block.md +1 -1
- package/_shared/advisor/reference/astra-rung.md +11 -11
- package/_shared/advisor/reference/identity.md +4 -4
- package/_shared/advisor/reference/lifecycle.md +2 -2
- package/_shared/advisor/reference/spawn-walk-log.md +4 -4
- package/_shared/advisor/reference/third-party-bind.md +7 -7
- package/_shared/advisor/reference/warm-up.md +3 -3
- package/_shared/advisor/scripts/codex_astra_advisor.py +33 -26
- package/_shared/advisor/scripts/codex_astra_preflight.py +57 -55
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +2 -2
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -3
- package/_shared/advisor/scripts/model_tier_run_validator.py +8 -9
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +91 -91
- package/_shared/pr-loop/scripts/check_convergence.py +54 -49
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
- package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
- package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
- package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
- package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
- package/bin/git_hooks_installer.test.mjs +1 -1
- package/bin/install-constants.mjs +1 -6
- package/bin/install.cursor-rules.test.mjs +24 -51
- package/bin/install.mjs +16 -70
- package/docs/codex-accounts.md +79 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
- package/hooks/blocking/test_pii_scanner.py +1 -1
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
- package/hooks/git-hooks/conftest.py +16 -0
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
- package/hooks/git-hooks/test_post_commit.py +1 -1
- package/hooks/git-hooks/test_pre_commit.py +1 -1
- package/hooks/git-hooks/test_pre_push.py +2 -2
- package/hooks/git-hooks/test_verification_start.py +1 -1
- package/hooks/git-hooks/verification_notice.py +2 -2
- package/hooks/git-hooks/verification_notice_context.py +21 -2
- package/hooks/git-hooks/verification_start.py +2 -3
- package/package.json +1 -1
- package/rules/durable-post-artifacts.md +4 -3
- package/scripts/claude_account_profile.py +21 -9
- package/scripts/codex_account_choice.py +396 -0
- package/scripts/codex_account_meters.py +372 -0
- package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
- package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
- package/scripts/private_term_scan.py +6 -2
- package/scripts/repository_checks/config/constants.py +1 -0
- package/scripts/repository_checks/tracked_private_terms.py +7 -0
- package/scripts/test_claude_account_profile.py +12 -0
- package/scripts/test_codex_account_choice.py +276 -0
- package/scripts/test_codex_account_meters.py +205 -0
- package/scripts/tests/test_automatic_advisory.py +3 -3
- package/scripts/tests/test_automatic_advisory_polling.py +1 -1
- package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
- package/scripts/tests/test_closed_pr_label.py +1 -1
- package/scripts/tests/test_private_term_scan.py +28 -0
- package/scripts/tests/test_review_closure.py +2 -2
- package/scripts/tests/test_review_closure_model.py +2 -2
- package/scripts/tests/test_rules.py +2 -83
- package/scripts/tests/test_tracked_private_terms.py +15 -0
- package/scripts/tests/test_verification_notice.py +29 -13
- package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
- package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
- package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
- package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
- package/scripts/sync_to_cursor/AGENTS.md +0 -1
- package/scripts/sync_to_cursor/CLAUDE.md +0 -1
- package/scripts/sync_to_cursor/__init__.py +0 -13
- package/scripts/sync_to_cursor/canonical_docs.py +0 -66
- package/scripts/sync_to_cursor/config/__init__.py +0 -8
- package/scripts/sync_to_cursor/engine.py +0 -219
- package/scripts/sync_to_cursor/hashing.py +0 -7
- package/scripts/sync_to_cursor/paths.py +0 -18
- package/scripts/sync_to_cursor/rules.py +0 -494
- package/scripts/sync_to_cursor.py +0 -22
- package/scripts/tests/test_engine.py +0 -101
- package/scripts/tests/test_sync_to_cursor.py +0 -315
|
@@ -1,81 +1,10 @@
|
|
|
1
|
-
"""Tests
|
|
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
|
-
|
|
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/
|
|
14
|
-
"git@github.com:
|
|
15
|
-
"ssh://git@github.com/
|
|
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=
|
|
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 / "
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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()
|