its-magic 0.1.3-0 → 0.1.3-2

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 (61) hide show
  1. package/installer.ps1 +8 -0
  2. package/installer.py +8 -0
  3. package/installer.sh +1 -0
  4. package/package.json +1 -1
  5. package/scripts/check_intake_template_parity.py +62 -0
  6. package/template/.cursor/agents/curator.mdc +1 -0
  7. package/template/.cursor/agents/po.mdc +1 -0
  8. package/template/.cursor/agents/release.mdc +1 -0
  9. package/template/.cursor/commands/auto.md +90 -0
  10. package/template/.cursor/commands/execute.md +26 -0
  11. package/template/.cursor/commands/refresh-context.md +32 -0
  12. package/template/.cursor/commands/sovereign-critic.md +104 -0
  13. package/template/.cursor/model-catalog.local.example.cursor-only.json +9 -0
  14. package/template/.cursor/model-catalog.local.example.json +9 -0
  15. package/template/.cursor/model-catalog.local.example.level-1-easy.json +9 -0
  16. package/template/.cursor/model-catalog.local.example.level-2-complex.json +9 -0
  17. package/template/.cursor/model-catalog.local.example.level-3-mega.json +9 -0
  18. package/template/.cursor/model-catalog.local.example.level-4-super.json +9 -0
  19. package/template/.cursor/model-catalog.local.example.role-based-balanced.json +18 -0
  20. package/template/.cursor/model-catalog.local.example.role-based-highend.json +18 -0
  21. package/template/.cursor/rules/sovereign-role-manifest.mdc.example +27 -0
  22. package/template/.cursor/scratchpad.local.example.md +55 -0
  23. package/template/.cursor/scratchpad.md +203 -0
  24. package/template/.cursor/sovereign-role-manifest.yaml.example +53 -0
  25. package/template/decisions/DEC-0104.md +279 -0
  26. package/template/decisions/DEC-0105.md +231 -0
  27. package/template/decisions/DEC-0107.md +246 -0
  28. package/template/docs/engineering/architecture.md +78 -0
  29. package/template/docs/engineering/auto-orchestration-reference.md +7 -0
  30. package/template/docs/engineering/context/installer-owned-paths.manifest +8 -0
  31. package/template/docs/engineering/reason_codes.md +411 -0
  32. package/template/docs/engineering/runbook.md +1119 -0
  33. package/template/docs/engineering/sovereign-memory/.gitkeep +0 -0
  34. package/template/docs/engineering/sovereign-memory/retrospectives/.gitkeep +0 -0
  35. package/template/handoffs/sovereign_decisions/.gitkeep +0 -0
  36. package/template/handoffs/sovereign_deferrals/.gitkeep +0 -0
  37. package/template/handoffs/sovereign_role_reviews.jsonl +0 -0
  38. package/template/scripts/__pycache__/decision_ledger_lib.cpython-312.pyc +0 -0
  39. package/template/scripts/check_intake_template_parity.py +181 -0
  40. package/template/scripts/decision_ledger_lib.py +732 -0
  41. package/template/scripts/ledger_validate.py +153 -0
  42. package/template/scripts/model_tier_lib.py +680 -0
  43. package/template/scripts/model_tier_validate.py +368 -0
  44. package/template/scripts/parallel_dev_arbiter.py +923 -0
  45. package/template/scripts/release_trigger_adapters.py +843 -0
  46. package/template/scripts/self_healing_deploy_lib.py +463 -0
  47. package/template/scripts/self_healing_deploy_validate.py +78 -0
  48. package/template/scripts/sovereign_convergence_lib.py +994 -0
  49. package/template/scripts/sovereign_convergence_validate.py +206 -0
  50. package/template/scripts/sovereign_critic_lib.py +629 -0
  51. package/template/scripts/sovereign_critic_validate.py +131 -0
  52. package/template/scripts/sovereign_loop_lib.py +828 -0
  53. package/template/scripts/sovereign_loop_validate.py +122 -0
  54. package/template/scripts/sovereign_memory_lib.py +869 -0
  55. package/template/scripts/sovereign_memory_validate.py +153 -0
  56. package/template/scripts/sovereign_role_manifest_lib.py +547 -0
  57. package/template/scripts/sovereign_role_manifest_validate.py +105 -0
  58. package/template/tests/us0108_contract_test.py +207 -0
  59. package/template/tests/us0109_contract_test.py +209 -0
  60. package/template/tests/us0109_us0110_compose_test.py +34 -0
  61. package/template/tests/us0111_contract_test.py +345 -0
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python3
2
+ """Sovereign Role-Behavior Manifest validator CLI (US-0106 / DEC-0106).
3
+
4
+ CLI contract:
5
+ --file <path> validate a single YAML manifest
6
+ --repo <root> validate repo root active + template pair
7
+ --self-test run lib self-test
8
+ --enforce exit non-zero on validation failure
9
+
10
+ Success: [SOVEREIGN_ROLE_MANIFEST_VALIDATION_OK]
11
+ Fail: reason codes SOVEREIGN_ROLE_* (SCHEMA_INVALID / UNKNOWN_ROLE / UNKNOWN_PHASE /
12
+ SECRET_DETECTED / OBJECTIVE_OVERFLOW)
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import sys
18
+ from pathlib import Path
19
+ from typing import List, Tuple
20
+
21
+ _SCRIPT_DIR = Path(__file__).resolve().parent
22
+ if str(_SCRIPT_DIR) not in sys.path:
23
+ sys.path.insert(0, str(_SCRIPT_DIR))
24
+
25
+ from sovereign_role_manifest_lib import ( # noqa: E402
26
+ MANIFEST_REL,
27
+ ReasonCode,
28
+ is_role_manifest_enabled,
29
+ load_manifest,
30
+ self_test,
31
+ validate_manifest,
32
+ )
33
+
34
+ TEMPLATE_MANIFEST_REL = "template/.cursor/sovereign-role-manifest.yaml.example"
35
+
36
+
37
+ def _load_yaml_text(path: Path) -> Tuple[str, str | None]:
38
+ try:
39
+ return path.read_text(encoding="utf-8"), None
40
+ except Exception as exc:
41
+ return "", f"{ReasonCode.SCHEMA_INVALID.value}: cannot read {path}: {exc}"
42
+
43
+
44
+ def _validate_file(path: Path) -> Tuple[bool, List[str]]:
45
+ text, err = _load_yaml_text(path)
46
+ if err:
47
+ return False, [err]
48
+ from sovereign_role_manifest_lib import _parse_yaml_minimal
49
+ parsed = _parse_yaml_minimal(text)
50
+ ok, reason = validate_manifest(parsed)
51
+ if not ok:
52
+ return False, [f"{ReasonCode.SCHEMA_INVALID.value}: {reason}"]
53
+ return True, []
54
+
55
+
56
+ def main() -> int:
57
+ p = argparse.ArgumentParser(description=__doc__)
58
+ p.add_argument("--file", type=Path, help="Validate a single manifest YAML")
59
+ p.add_argument("--repo", type=Path, help="Repo root to validate active + template pair")
60
+ p.add_argument("--self-test", action="store_true")
61
+ p.add_argument("--enforce", action="store_true", help="Exit non-zero on validation failure")
62
+ args = p.parse_args()
63
+
64
+ if args.self_test:
65
+ ok = self_test()
66
+ print("[SOVEREIGN_ROLE_MANIFEST_SELF_TEST_OK]" if ok else "[SOVEREIGN_ROLE_MANIFEST_SELF_TEST_FAIL]")
67
+ return 0 if ok else 2
68
+
69
+ if args.file:
70
+ ok, errs = _validate_file(args.file)
71
+ if ok:
72
+ print(f"[SOVEREIGN_ROLE_MANIFEST_VALIDATION_OK] file={args.file}")
73
+ return 0
74
+ for e in errs:
75
+ print(f"[SOVEREIGN_ROLE_MANIFEST_VALIDATION_ERROR] {e}")
76
+ return 1 if args.enforce else 0
77
+
78
+ if args.repo:
79
+ root = args.repo
80
+ active = root / MANIFEST_REL
81
+ template = root / TEMPLATE_MANIFEST_REL
82
+ errors: List[str] = []
83
+ if not active.is_file():
84
+ errors.append(f"{ReasonCode.SCHEMA_INVALID.value}: missing active manifest {active}")
85
+ else:
86
+ ok, errs = _validate_file(active)
87
+ errors.extend(errs)
88
+ if not template.is_file():
89
+ errors.append(f"{ReasonCode.SCHEMA_INVALID.value}: missing template example {template}")
90
+ else:
91
+ ok, errs = _validate_file(template)
92
+ errors.extend(errs)
93
+ if errors:
94
+ for e in errors:
95
+ print(f"[SOVEREIGN_ROLE_MANIFEST_VALIDATION_ERROR] {e}")
96
+ return 1 if args.enforce else 0
97
+ print(f"[SOVEREIGN_ROLE_MANIFEST_VALIDATION_OK] repo={root}")
98
+ return 0
99
+
100
+ p.error("one of --file, --repo, --self-test is required")
101
+ return 2
102
+
103
+
104
+ if __name__ == "__main__":
105
+ sys.exit(main())
@@ -0,0 +1,207 @@
1
+ """US-0108: Eight contract tests for Parallel Instance Arbitrage.
2
+
3
+ DEC-0108 §10: scratchpad keys, worktree isolation, selection predicate,
4
+ merge policy, resource guard, execute steps 25-28, backward compat, parity scope.
5
+
6
+ Default-off: SOVEREIGN_PARALLEL_DEV=0 → zero overhead.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import sys
13
+ import tempfile
14
+ import unittest
15
+ from pathlib import Path
16
+
17
+
18
+ def _repo_root() -> Path:
19
+ return Path(__file__).resolve().parents[1]
20
+
21
+
22
+ def _load_arbiter():
23
+ root = _repo_root()
24
+ scripts_dir = str(root / "scripts")
25
+ if scripts_dir not in sys.path:
26
+ sys.path.insert(0, scripts_dir)
27
+ import parallel_dev_arbiter as mod # noqa: E402
28
+ return mod
29
+
30
+
31
+ def _enabled_scratchpad(scratchpad_path: Path) -> None:
32
+ """Write scratchpad with US-0108 enabled."""
33
+ scratchpad_path.parent.mkdir(parents=True, exist_ok=True)
34
+ lines = [
35
+ "# US-0108 parallel dev enabled\n",
36
+ "SOVEREIGN_PARALLEL_DEV=1\n",
37
+ "AUTO_SOVEREIGN_PARALLEL_N=3\n",
38
+ "AUTO_SOVEREIGN_PARALLEL_MAX_TOTAL=6\n",
39
+ "AUTO_SOVEREIGN_MERGE_RESOLVE=first_pass_wins\n",
40
+ "AUTO_SOVEREIGN_WORKTREE_KEEP=0\n",
41
+ "AUTO_SOVEREIGN_PARALLEL_QA=0\n",
42
+ "AUTO_SOVEREIGN_PARALLEL_QA_ARBITER=critic_first_pass\n",
43
+ "AUTO_SOVEREIGN_PARALLEL_ANTI_SLOP_THRESHOLD=6\n",
44
+ "AUTO_SOVEREIGN_PARALLEL_REWORK_MAX=2\n",
45
+ "AUTO_SOVEREIGN_PARALLEL_MERGE_TIMEOUT_SEC=60\n",
46
+ ]
47
+ scratchpad_path.write_text("".join(lines), encoding="utf-8")
48
+
49
+
50
+ class TestUS0108ScratchpadKeys(unittest.TestCase):
51
+ """test_us0108_scratchpad_keys_literals (AC-1)."""
52
+
53
+ def test_scratchpad_key_literals(self) -> None:
54
+ mod = _load_arbiter()
55
+ required_keys = {
56
+ "SOVEREIGN_PARALLEL_DEV",
57
+ "AUTO_SOVEREIGN_PARALLEL_N",
58
+ "AUTO_SOVEREIGN_PARALLEL_MAX_TOTAL",
59
+ "AUTO_SOVEREIGN_MERGE_RESOLVE",
60
+ "AUTO_SOVEREIGN_WORKTREE_KEEP",
61
+ "AUTO_SOVEREIGN_PARALLEL_QA",
62
+ "AUTO_SOVEREIGN_PARALLEL_QA_ARBITER",
63
+ "AUTO_SOVEREIGN_PARALLEL_ANTI_SLOP_THRESHOLD",
64
+ "AUTO_SOVEREIGN_PARALLEL_REWORK_MAX",
65
+ "AUTO_SOVEREIGN_PARALLEL_MERGE_TIMEOUT_SEC",
66
+ }
67
+ for key in required_keys:
68
+ self.assertIn(key, mod.SCRATCHPAD_KEY_DEFAULTS,
69
+ f"{key} missing from SCRATCHPAD_KEY_DEFAULTS")
70
+ self.assertEqual(mod.SCRATCHPAD_KEY_DEFAULTS["SOVEREIGN_PARALLEL_DEV"], "0")
71
+ self.assertEqual(mod.SCRATCHPAD_KEY_DEFAULTS["AUTO_SOVEREIGN_PARALLEL_N"], "3")
72
+ self.assertEqual(mod.SCRATCHPAD_KEY_DEFAULTS["AUTO_SOVEREIGN_PARALLEL_MAX_TOTAL"], "6")
73
+
74
+
75
+ class TestUS0108WorktreeIsolation(unittest.TestCase):
76
+ """test_us0108_worktree_isolation (AC-2)."""
77
+
78
+ def test_worktree_creation_pattern(self) -> None:
79
+ mod = _load_arbiter()
80
+ with tempfile.TemporaryDirectory() as td:
81
+ repo_root = Path(td)
82
+ (repo_root / ".git").mkdir()
83
+ wt = mod.create_worktree("US-0108", 0, "main", repo_root)
84
+ self.assertEqual(wt.instance_id, "US-0108-inst0")
85
+ self.assertEqual(wt.branch, "us0108-US-0108-0")
86
+ expected_path = repo_root / ".git" / "worktrees" / "us0108-US-0108-0"
87
+ self.assertEqual(wt.path, str(expected_path))
88
+
89
+
90
+ class TestUS0108SelectionDeterminism(unittest.TestCase):
91
+ """test_us0108_selection_determinism (AC-3)."""
92
+
93
+ def test_selection_logic(self) -> None:
94
+ mod = _load_arbiter()
95
+ results = [
96
+ {"qa_verdict": "pass", "anti_slop_score": 5, "proof_issued_at": "2026-06-29T22:00:00Z", "instance_id": "A"},
97
+ {"qa_verdict": "pass", "anti_slop_score": 8, "proof_issued_at": "2026-06-29T22:01:00Z", "instance_id": "B"},
98
+ {"qa_verdict": "fail", "anti_slop_score": 10, "proof_issued_at": "2026-06-29T22:00:30Z", "instance_id": "C"},
99
+ ]
100
+ winner = mod.select_winner(results)
101
+ self.assertIsNotNone(winner)
102
+ self.assertEqual(winner["instance_id"], "B") # highest anti_slop among passing
103
+
104
+ def test_tie_break_earliest(self) -> None:
105
+ mod = _load_arbiter()
106
+ results = [
107
+ {"qa_verdict": "pass", "anti_slop_score": 7, "proof_issued_at": "2026-06-29T22:05:00Z", "instance_id": "X"},
108
+ {"qa_verdict": "pass", "anti_slop_score": 7, "proof_issued_at": "2026-06-29T22:01:00Z", "instance_id": "Y"},
109
+ ]
110
+ winner = mod.select_winner(results)
111
+ self.assertIsNotNone(winner)
112
+ self.assertEqual(winner["instance_id"], "Y") # earliest proof_issued_at
113
+
114
+
115
+ class TestUS0108MergeAndPickSchema(unittest.TestCase):
116
+ """test_us0108_merge_and_pick_schema (AC-4)."""
117
+
118
+ def test_pick_record_schema(self) -> None:
119
+ mod = _load_arbiter()
120
+ with tempfile.TemporaryDirectory() as td:
121
+ pick_path = Path(td) / "pick.json"
122
+ rec = mod.build_pick_record(
123
+ story_id="US-0108",
124
+ winner_id="inst0",
125
+ winner_path="/tmp/wt0",
126
+ qa_verdict="pass",
127
+ anti_slop_score=8,
128
+ merge_policy="first_pass_wins",
129
+ loser_ids=["inst1", "inst2"],
130
+ orchestrator_run_id="test-001",
131
+ )
132
+ ok, msg = mod.write_pick_record(rec, pick_path)
133
+ self.assertTrue(ok, msg)
134
+ self.assertTrue(pick_path.exists())
135
+ loaded = json.loads(pick_path.read_text(encoding="utf-8"))
136
+ vok, vmsg = mod.validate_pick_record(loaded)
137
+ self.assertTrue(vok, vmsg)
138
+ self.assertEqual(loaded["schema_version"], 1)
139
+ self.assertEqual(loaded["story_id"], "US-0108")
140
+ self.assertIn("loser_instance_ids", loaded)
141
+
142
+
143
+ class TestUS0108ResourceCap(unittest.TestCase):
144
+ """test_us0108_resource_cap (AC-5)."""
145
+
146
+ def test_lockfile_acquire_release(self) -> None:
147
+ mod = _load_arbiter()
148
+ with tempfile.TemporaryDirectory() as td:
149
+ repo_root = Path(td)
150
+ (repo_root / ".git").mkdir()
151
+ ok1, msg1 = mod.acquire_parallel_slot("slot1", repo_root, max_total=2)
152
+ self.assertTrue(ok1, msg1)
153
+ ok2, msg2 = mod.acquire_parallel_slot("slot2", repo_root, max_total=2)
154
+ self.assertTrue(ok2, msg2)
155
+ ok3, msg3 = mod.acquire_parallel_slot("slot3", repo_root, max_total=2)
156
+ self.assertFalse(ok3)
157
+ self.assertEqual(msg3, mod.ReasonCode.PARALLEL_DEV_RESOURCE_CAP_EXHAUSTED)
158
+ mod.release_parallel_slot("slot1", repo_root)
159
+ ok4, msg4 = mod.acquire_parallel_slot("slot4", repo_root, max_total=2)
160
+ self.assertTrue(ok4, msg4)
161
+
162
+
163
+ class TestUS0108ExecuteSteps(unittest.TestCase):
164
+ """test_us0108_execute_steps_25_28 (AC-6)."""
165
+
166
+ def test_execute_disabled_by_default(self) -> None:
167
+ mod = _load_arbiter()
168
+ with tempfile.TemporaryDirectory() as td:
169
+ repo_root = Path(td)
170
+ (repo_root / ".git").mkdir()
171
+ (repo_root / ".cursor").mkdir()
172
+ scratchpad = repo_root / ".cursor" / "scratchpad.md"
173
+ scratchpad.write_text("SOVEREIGN_PARALLEL_DEV=0\n", encoding="utf-8")
174
+ result = mod.execute_parallel_dev("US-0108", "main", 3, scratchpad, repo_root)
175
+ self.assertIsNone(result.winner_worktree)
176
+ self.assertEqual(result.merge_result.conflicts, [mod.ReasonCode.PARALLEL_DEV_DISABLED])
177
+
178
+
179
+ class TestUS0108BackwardCompat(unittest.TestCase):
180
+ """test_us0108_backward_compat_single_dev_unchanged (AC-7)."""
181
+
182
+ def test_disabled_zero_overhead(self) -> None:
183
+ mod = _load_arbiter()
184
+ with tempfile.TemporaryDirectory() as td:
185
+ repo_root = Path(td)
186
+ (repo_root / ".git").mkdir()
187
+ (repo_root / ".cursor").mkdir()
188
+ scratchpad = repo_root / ".cursor" / "scratchpad.md"
189
+ scratchpad.write_text("# no US-0108 keys\n", encoding="utf-8")
190
+ self.assertFalse(mod.is_parallel_enabled(scratchpad))
191
+ result = mod.execute_parallel_dev("US-0108", "main", 3, scratchpad, repo_root)
192
+ self.assertIsNone(result.winner_worktree)
193
+ self.assertEqual(result.merge_result.conflicts, [mod.ReasonCode.PARALLEL_DEV_DISABLED])
194
+
195
+
196
+ class TestUS0108ParityScope(unittest.TestCase):
197
+ """test_us0108_parity_scope (AC-8)."""
198
+
199
+ def test_parity_scope_registered(self) -> None:
200
+ from check_intake_template_parity import SCOPES
201
+ self.assertIn("sovereign-parallel-dev", SCOPES)
202
+ pairs = SCOPES["sovereign-parallel-dev"]
203
+ self.assertGreater(len(pairs), 0)
204
+
205
+
206
+ if __name__ == "__main__":
207
+ unittest.main()
@@ -0,0 +1,209 @@
1
+ """Contract tests for US-0109 Self-Healing Deploy Loop (DEC-0109).
2
+
3
+ 8 core markers + 2 compose guards + 1 backward compat.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import pathlib
9
+ import subprocess
10
+ import sys
11
+
12
+ import pytest
13
+
14
+ REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
15
+ SCRIPTS_DIR = REPO_ROOT / "scripts"
16
+ sys.path.insert(0, str(SCRIPTS_DIR))
17
+
18
+ from self_healing_deploy_lib import ( # noqa: E402
19
+ AUTO_SOVEREIGN_DEPLOY_HEALTH_ENDPOINT_KEY,
20
+ AUTO_SOVEREIGN_DEPLOY_PROBE_KIND_KEY,
21
+ AUTO_SOVEREIGN_DEPLOY_RETRY_MAX_KEY,
22
+ AUTO_SOVEREIGN_DEPLOY_SMOKE_TIMEOUT_SEC_KEY,
23
+ AUTO_SOVEREIGN_SELF_HEALING_DEPLOY_KEY,
24
+ DEFAULT_ACCEPTANCE_SMOKE_PATH,
25
+ DEFAULT_PROBE_KIND,
26
+ DEFAULT_RETRY_MAX,
27
+ DEFAULT_SELF_HEALING_ENABLED,
28
+ DEFAULT_SMOKE_TIMEOUT_SEC,
29
+ SOVEREIGN_DEPLOY_ACCEPTANCE_SMOKE_PATH_KEY,
30
+ ProbeKind,
31
+ ReasonCode,
32
+ get_acceptance_smoke_path,
33
+ get_probe_kind,
34
+ get_retry_max,
35
+ get_smoke_timeout_sec,
36
+ is_self_healing_deploy_enabled,
37
+ resolve_health_endpoint_url,
38
+ run_acceptance_smoke,
39
+ run_deploy_healing_loop,
40
+ run_health_probe,
41
+ run_smoke_probe_chain,
42
+ self_test,
43
+ )
44
+
45
+
46
+ # --- T-006: 8 core markers ---------------------------------------------------
47
+
48
+
49
+ def test_us0109_scratchpad_keys_and_defaults() -> None:
50
+ """AC-1: 6 scratchpad keys resolve to DEC-0109 defaults."""
51
+ scratchpad = {}
52
+ assert is_self_healing_deploy_enabled(scratchpad) is False
53
+ assert get_retry_max(scratchpad) == DEFAULT_RETRY_MAX
54
+ assert get_smoke_timeout_sec(scratchpad) == DEFAULT_SMOKE_TIMEOUT_SEC
55
+ assert get_probe_kind(scratchpad) == ProbeKind.BOTH
56
+ assert get_acceptance_smoke_path(scratchpad) == DEFAULT_ACCEPTANCE_SMOKE_PATH
57
+ assert resolve_health_endpoint_url(scratchpad) is None
58
+ assert DEFAULT_SELF_HEALING_ENABLED == "0"
59
+ expected_keys = {
60
+ AUTO_SOVEREIGN_SELF_HEALING_DEPLOY_KEY,
61
+ AUTO_SOVEREIGN_DEPLOY_RETRY_MAX_KEY,
62
+ AUTO_SOVEREIGN_DEPLOY_SMOKE_TIMEOUT_SEC_KEY,
63
+ AUTO_SOVEREIGN_DEPLOY_PROBE_KIND_KEY,
64
+ SOVEREIGN_DEPLOY_ACCEPTANCE_SMOKE_PATH_KEY,
65
+ AUTO_SOVEREIGN_DEPLOY_HEALTH_ENDPOINT_KEY,
66
+ }
67
+ assert len(expected_keys) == 6
68
+
69
+
70
+ def test_us0109_probe_health_stage() -> None:
71
+ """AC-2: health probe stage returns fail when target missing."""
72
+ scratchpad = {
73
+ AUTO_SOVEREIGN_SELF_HEALING_DEPLOY_KEY: "1",
74
+ AUTO_SOVEREIGN_DEPLOY_PROBE_KIND_KEY: "health_endpoint",
75
+ AUTO_SOVEREIGN_DEPLOY_HEALTH_ENDPOINT_KEY: "",
76
+ }
77
+ result = run_health_probe(scratchpad)
78
+ assert result.overall == "fail"
79
+ assert result.reason_code == ReasonCode.DEPLOY_HEALING_PROBE_TARGET_MISSING.value
80
+ assert result.health_status == "fail"
81
+ assert result.acceptance_status is None
82
+
83
+
84
+ def test_us0109_probe_acceptance_stage() -> None:
85
+ """AC-2: acceptance smoke skip when path absent."""
86
+ scratchpad = {
87
+ AUTO_SOVEREIGN_SELF_HEALING_DEPLOY_KEY: "1",
88
+ AUTO_SOVEREIGN_DEPLOY_PROBE_KIND_KEY: "acceptance_smoke",
89
+ SOVEREIGN_DEPLOY_ACCEPTANCE_SMOKE_PATH_KEY: "nonexistent_path_xyz/",
90
+ }
91
+ result = run_acceptance_smoke(scratchpad)
92
+ assert result.acceptance_status == "skip"
93
+ assert result.overall == "pass"
94
+ assert result.reason_code == "DEPLOY_ACCEPTANCE_SMOKE_SKIP_NO_PATH"
95
+
96
+
97
+ def test_us0109_retry_loop_bounded() -> None:
98
+ """AC-3: bounded retry loop caps at AUTO_SOVEREIGN_DEPLOY_RETRY_MAX."""
99
+ scratchpad = {
100
+ AUTO_SOVEREIGN_SELF_HEALING_DEPLOY_KEY: "1",
101
+ AUTO_SOVEREIGN_DEPLOY_RETRY_MAX_KEY: "3",
102
+ }
103
+ attempts = 0
104
+
105
+ def always_fail(reason: str) -> bool:
106
+ nonlocal attempts
107
+ attempts += 1
108
+ return False
109
+
110
+ result = run_deploy_healing_loop(REPO_ROOT, scratchpad, always_fail, story_id="US-0109")
111
+ assert result.enabled is True
112
+ assert result.reason_code == ReasonCode.DEPLOY_HEALING_RETRY_CAP_EXHAUSTED.value
113
+ assert result.retry_count <= 3
114
+ assert attempts <= 3
115
+
116
+
117
+ def test_us0109_deferred_after_cap_exhaustion() -> None:
118
+ """AC-4: DEPLOY_DEFERRED path after retry cap exhaustion."""
119
+ from self_healing_deploy_lib import emit_deploy_deferral
120
+
121
+ scratchpad = {
122
+ AUTO_SOVEREIGN_SELF_HEALING_DEPLOY_KEY: "1",
123
+ AUTO_SOVEREIGN_DEPLOY_RETRY_MAX_KEY: "2",
124
+ }
125
+ deferral_id, reason = emit_deploy_deferral(
126
+ REPO_ROOT,
127
+ scratchpad,
128
+ story_id="US-0109",
129
+ orchestrator_run_id="auto-20260628-04",
130
+ smoke_summary="smoke probe failed; retry cap exhausted",
131
+ )
132
+ allowed = (
133
+ ReasonCode.DEPLOY_HEALING_DISABLED.value,
134
+ "SOVEREIGN_LOOP_DISABLED",
135
+ "",
136
+ )
137
+ assert deferral_id is None or reason in allowed
138
+
139
+
140
+ def test_us0109_backward_compat_off_path_byte_identical() -> None:
141
+ """AC-5: AUTO_SOVEREIGN_SELF_HEALING_DEPLOY=0 byte-identical US-0054 path."""
142
+ scratchpad = {AUTO_SOVEREIGN_SELF_HEALING_DEPLOY_KEY: "0"}
143
+ probe = run_smoke_probe_chain(scratchpad)
144
+ assert probe.overall == "pass"
145
+ assert probe.reason_code == ReasonCode.DEPLOY_HEALING_DISABLED.value
146
+ assert probe.health_status == "skip"
147
+ assert probe.acceptance_status == "skip"
148
+
149
+ loop_result = run_deploy_healing_loop(
150
+ REPO_ROOT, scratchpad, lambda reason: False, story_id="US-0109"
151
+ )
152
+ assert loop_result.enabled is False
153
+ assert loop_result.reason_code == ReasonCode.DEPLOY_HEALING_DISABLED.value
154
+
155
+
156
+ def test_us0109_validator_cli_self_test() -> None:
157
+ """AC-6: validator CLI emits [SELF_HEALING_DEPLOY_VALIDATION_OK]."""
158
+ token = self_test()
159
+ assert token == "[SELF_HEALING_DEPLOY_VALIDATION_OK]"
160
+
161
+
162
+ def test_us0109_reason_codes_section_present() -> None:
163
+ """AC-8: 8 DEPLOY_HEALING_* reason codes in reason_codes.md."""
164
+ reason_codes_path = REPO_ROOT / "docs" / "engineering" / "reason_codes.md"
165
+ assert reason_codes_path.is_file()
166
+ content = reason_codes_path.read_text(encoding="utf-8")
167
+ expected_codes = [
168
+ "DEPLOY_HEALING_DISABLED",
169
+ "DEPLOY_HEALING_SMOKE_HEALTH_FAIL",
170
+ "DEPLOY_HEALING_SMOKE_ACCEPTANCE_FAIL",
171
+ "DEPLOY_HEALING_RETRY_ATTEMPT",
172
+ "DEPLOY_HEALING_RETRY_CAP_EXHAUSTED",
173
+ "DEPLOY_HEALING_DEFERRED",
174
+ "DEPLOY_HEALING_PROBE_TARGET_MISSING",
175
+ "DEPLOY_HEALING_TIMEOUT",
176
+ ]
177
+ for code in expected_codes:
178
+ assert code in content, f"reason code {code} missing"
179
+ assert "## US-0109" in content
180
+
181
+
182
+ # --- T-007 / T-009: compose guards -------------------------------------------
183
+
184
+
185
+ def test_us0109_us0054_compose_no_publish_semantics_change() -> None:
186
+ """AC-7 compose guard: US-0054 publish targets / confirmation gate UNCHANGED."""
187
+ lib_path = SCRIPTS_DIR / "self_healing_deploy_lib.py"
188
+ content = lib_path.read_text(encoding="utf-8")
189
+ forbidden_tokens = ["RELEASE_PUBLISH_OK", "release_publish", "publish_targets"]
190
+ for token in forbidden_tokens:
191
+ assert token not in content, f"US-0054 publish token {token} found in US-0109 lib"
192
+
193
+
194
+ def test_us0109_us0100_compose_no_changelog_change() -> None:
195
+ """AC-7 compose guard: US-0100 changelog / [Unreleased] UNCHANGED."""
196
+ lib_path = SCRIPTS_DIR / "self_healing_deploy_lib.py"
197
+ content = lib_path.read_text(encoding="utf-8")
198
+ forbidden_tokens = ["changelog", "[Unreleased]", "changelog_lib", "version_changelog"]
199
+ for token in forbidden_tokens:
200
+ assert token not in content, f"US-0100 changelog token {token} found in US-0109 lib"
201
+
202
+
203
+ def test_us0109_us0110_compose_no_convergence_change() -> None:
204
+ """AC-7 compose guard: US-0110 convergence predicate UNCHANGED."""
205
+ lib_path = SCRIPTS_DIR / "self_healing_deploy_lib.py"
206
+ content = lib_path.read_text(encoding="utf-8")
207
+ forbidden_tokens = ["convergence", "evaluate_convergence", "sovereign_convergence_lib"]
208
+ for token in forbidden_tokens:
209
+ assert token not in content, f"US-0110 convergence token {token} found in US-0109 lib"
@@ -0,0 +1,34 @@
1
+ """US-0109 compose guard: US-0054 publish semaphore + US-0100 changelog + US-0110 convergence unchanged."""
2
+ from __future__ import annotations
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ def _repo_root() -> Path:
7
+ return Path(__file__).resolve().parents[1]
8
+
9
+ def test_us0109_us0054_compose_no_publish_semantics_change() -> None:
10
+ arch_path = _repo_root() / "docs" / "engineering" / "architecture.md"
11
+ content = arch_path.read_text(encoding="utf-8")
12
+ assert "# US-0109" in content
13
+ us0109_section = content.split("# US-0109")[1] if "# US-0109" in content else ""
14
+ assert "US-0054" in us0109_section
15
+ assert "publish" in us0109_section.lower() or "re-enter" in us0109_section.lower()
16
+
17
+ def test_us0109_us0100_compose_no_changelog_change() -> None:
18
+ root = _repo_root()
19
+ scripts_dir = str(root / "scripts")
20
+ if scripts_dir not in sys.path:
21
+ sys.path.insert(0, scripts_dir)
22
+ import self_healing_deploy_lib as mod
23
+ assert not hasattr(mod, "write_changelog")
24
+ assert not hasattr(mod, "promote_unreleased")
25
+
26
+ def test_us0109_us0110_compose_no_convergence_change() -> None:
27
+ root = _repo_root()
28
+ scripts_dir = str(root / "scripts")
29
+ if scripts_dir not in sys.path:
30
+ sys.path.insert(0, scripts_dir)
31
+ import self_healing_deploy_lib as mod
32
+ assert not hasattr(mod, "evaluate_convergence")
33
+ assert not hasattr(mod, "is_converged")
34
+ assert not hasattr(mod, "convergence_predicate")