prizmkit 1.1.111 → 1.1.113
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/bundled/VERSION.json +3 -3
- package/bundled/adapters/codex/skill-adapter.js +4 -0
- package/bundled/agents/prizm-dev-team-reviewer.md +12 -1
- package/bundled/dev-pipeline/SCHEMA_ANALYSIS.md +10 -10
- package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +1 -0
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +20 -73
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +290 -421
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +1 -1
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +259 -481
- package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +34 -0
- package/bundled/dev-pipeline/scripts/init-pipeline.py +12 -0
- package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +24 -1
- package/bundled/dev-pipeline/scripts/prompt_framework.py +312 -0
- package/bundled/dev-pipeline/scripts/update-bug-status.py +87 -20
- package/bundled/dev-pipeline/scripts/update-feature-status.py +82 -16
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +68 -28
- package/bundled/dev-pipeline/scripts/utils.py +171 -0
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +7 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +17 -19
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +21 -26
- package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +9 -1
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +11 -8
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +122 -182
- package/bundled/dev-pipeline/templates/sections/bugfix-artifacts.md +16 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-critical-paths.md +12 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-mission.md +9 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +31 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-critic.md +22 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +23 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-implement.md +20 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-init.md +9 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-manual-verification.md +15 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +29 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +11 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-session-context.md +7 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +31 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-task-contract.md +40 -0
- package/bundled/dev-pipeline/templates/sections/failure-capture.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-auto.md +8 -2
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-opencli.md +7 -1
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +7 -1
- package/bundled/dev-pipeline/templates/sections/refactor-artifacts.md +17 -0
- package/bundled/dev-pipeline/templates/sections/refactor-critical-paths.md +13 -0
- package/bundled/dev-pipeline/templates/sections/refactor-mission.md +9 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-commit-report.md +37 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-critic.md +22 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-implement.md +16 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-init.md +9 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-plan.md +22 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +19 -0
- package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +11 -0
- package/bundled/dev-pipeline/templates/sections/refactor-session-context.md +7 -0
- package/bundled/dev-pipeline/templates/sections/refactor-session-status.md +28 -0
- package/bundled/dev-pipeline/templates/sections/refactor-task-contract.md +52 -0
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +104 -26
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +14 -0
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +239 -0
- package/bundled/dev-pipeline/tests/test_unified_cli.py +6 -2
- package/bundled/dev-pipeline/tests/test_utils.py +66 -1
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/app-planner/SKILL.md +8 -7
- package/bundled/skills/app-planner/references/project-brief-guide.md +2 -2
- package/bundled/skills/app-planner/references/rules/backend/derivation-rules.md +1 -1
- package/bundled/skills/app-planner/references/rules/frontend/derivation-rules.md +1 -1
- package/bundled/skills/app-planner/references/rules-configuration.md +53 -26
- package/bundled/skills/bug-fix-workflow/SKILL.md +191 -336
- package/bundled/skills/bug-planner/SKILL.md +6 -5
- package/bundled/skills/bug-planner/references/critic-and-verification.md +3 -3
- package/bundled/skills/bug-planner/references/schema-validation.md +2 -1
- package/bundled/skills/bug-planner/scripts/validate-bug-list.py +70 -1
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +21 -21
- package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +2 -2
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +25 -26
- package/bundled/skills/feature-planner/SKILL.md +9 -11
- package/bundled/skills/feature-planner/assets/planning-guide.md +1 -1
- package/bundled/skills/feature-planner/references/decomposition-patterns.md +1 -1
- package/bundled/skills/feature-planner/references/error-recovery.md +2 -1
- package/bundled/skills/feature-planner/references/incremental-feature-planning.md +2 -2
- package/bundled/skills/feature-planner/references/new-project-planning.md +35 -39
- package/bundled/skills/feature-planner/scripts/validate-and-generate.py +1 -1
- package/bundled/skills/feature-workflow/SKILL.md +170 -298
- package/bundled/skills/prizmkit/SKILL.md +103 -57
- package/bundled/skills/prizmkit-code-review/SKILL.md +97 -116
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +1 -0
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +14 -3
- package/bundled/skills/prizmkit-committer/SKILL.md +59 -47
- package/bundled/skills/prizmkit-deploy/SKILL.md +162 -381
- package/bundled/skills/prizmkit-deploy/references/database-setup.md +12 -3
- package/bundled/skills/prizmkit-deploy/references/ssh-adapter-flow.md +220 -0
- package/bundled/skills/prizmkit-implement/SKILL.md +51 -40
- package/bundled/skills/prizmkit-init/SKILL.md +4 -3
- package/bundled/skills/prizmkit-plan/SKILL.md +85 -70
- package/bundled/skills/prizmkit-prizm-docs/SKILL.md +94 -73
- package/bundled/skills/prizmkit-prizm-docs/assets/prizm-docs-format.md +20 -18
- package/bundled/skills/prizmkit-prizm-docs/references/op-init.md +11 -13
- package/bundled/skills/prizmkit-prizm-docs/references/op-update.md +18 -14
- package/bundled/skills/prizmkit-retrospective/SKILL.md +56 -48
- package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +79 -27
- package/bundled/skills/prizmkit-test/SKILL.md +138 -141
- package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +1 -1
- package/bundled/skills/prizmkit-test/references/examples.md +11 -9
- package/bundled/skills/prizmkit-test/references/test-generation-steps.md +1 -1
- package/bundled/skills/prizmkit-test/references/test-report-template.md +2 -2
- package/bundled/skills/recovery-workflow/SKILL.md +195 -256
- package/bundled/skills/recovery-workflow/evals/evals.json +21 -13
- package/bundled/skills/recovery-workflow/references/detection.md +48 -39
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +258 -322
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +32 -13
- package/bundled/skills/refactor-planner/SKILL.md +2 -2
- package/bundled/skills/refactor-planner/references/behavior-preservation.md +24 -19
- package/bundled/skills/refactor-planner/references/fast-path.md +2 -4
- package/bundled/skills/refactor-planner/references/planning-phases.md +2 -2
- package/bundled/skills/refactor-workflow/SKILL.md +174 -307
- package/package.json +1 -1
- package/src/scaffold.js +5 -0
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +0 -6
- package/bundled/skills/feature-workflow/references/brainstorm-guide.md +0 -137
- package/bundled/skills/refactor-workflow/references/brainstorm-guide.md +0 -116
|
@@ -884,7 +884,8 @@ class TestDirectOrchestratorImplementationPrompts:
|
|
|
884
884
|
assert FORBIDDEN_DEV_FIX_PLACEHOLDER not in replacements
|
|
885
885
|
assert FORBIDDEN_DEV_RESUME_PLACEHOLDER not in replacements
|
|
886
886
|
assert "{{AGENT_PROMPT_CRITIC_PLAN_CHALLENGE}}" in replacements
|
|
887
|
-
assert "{{AGENT_PROMPT_REVIEWER_REVIEW}}" in replacements
|
|
887
|
+
assert "{{AGENT_PROMPT_REVIEWER_REVIEW}}" not in replacements
|
|
888
|
+
assert (templates_dir / "agent-prompts" / "reviewer-review.md").exists() is False
|
|
888
889
|
assert (templates_dir / "agent-prompts" / "dev-implement.md").exists() is False
|
|
889
890
|
assert (templates_dir / "agent-prompts" / FORBIDDEN_DEV_FIX_PROMPT).exists() is False
|
|
890
891
|
assert (templates_dir / "agent-prompts" / FORBIDDEN_DEV_RESUME_PROMPT).exists() is False
|
|
@@ -923,9 +924,19 @@ class TestDirectOrchestratorImplementationPrompts:
|
|
|
923
924
|
assert "Implement — Orchestrator Direct" in rendered
|
|
924
925
|
assert "Spawn Dev subagent" not in rendered
|
|
925
926
|
assert "Spawn Dev agent" not in rendered
|
|
927
|
+
assert "Spawn Reviewer agent" not in rendered
|
|
928
|
+
assert "Reviewer Agent Def" not in rendered
|
|
929
|
+
assert "subagent_type=\"prizm-dev-team-reviewer\"" not in rendered
|
|
926
930
|
assert "Implement — Dev" not in rendered
|
|
927
931
|
assert "{{AGENT_PROMPT_DEV_IMPLEMENT}}" not in rendered
|
|
932
|
+
assert "{{AGENT_PROMPT_REVIEWER_REVIEW}}" not in rendered
|
|
933
|
+
assert "reviewer-review.md" not in rendered
|
|
928
934
|
assert "dev-implement.md" not in rendered
|
|
935
|
+
assert "Bug Fix Documentation Policy" not in rendered
|
|
936
|
+
assert "For bug-fix sessions" not in rendered
|
|
937
|
+
assert "fix(<scope>)" not in rendered
|
|
938
|
+
assert "port check 3000" not in rendered
|
|
939
|
+
assert "localhost:3000" not in rendered
|
|
929
940
|
assert FORBIDDEN_REVIEW_DEV_FIX not in rendered
|
|
930
941
|
assert FORBIDDEN_REVIEWER_DEV_SPAWN not in rendered
|
|
931
942
|
assert FORBIDDEN_EXPLORE_SUBAGENT not in rendered
|
|
@@ -1089,7 +1100,10 @@ def _ensure_agent_files(project, platforms=("claude", "codex")):
|
|
|
1089
1100
|
(agents_dir / f"prizm-dev-team-{name}{suffix}").write_text("agent\n", encoding="utf-8")
|
|
1090
1101
|
|
|
1091
1102
|
|
|
1092
|
-
def _render_bugfix_prompt(
|
|
1103
|
+
def _render_bugfix_prompt(
|
|
1104
|
+
tmp_path, platform="claude", explicit_platform=None,
|
|
1105
|
+
bug_overrides=None, retry_count="0", resume_phase="null",
|
|
1106
|
+
):
|
|
1093
1107
|
from generate_bugfix_prompt import main as bugfix_main
|
|
1094
1108
|
|
|
1095
1109
|
project = tmp_path / "bugfix-project"
|
|
@@ -1101,8 +1115,11 @@ def _render_bugfix_prompt(tmp_path, platform="claude", explicit_platform=None):
|
|
|
1101
1115
|
_ensure_agent_files(project)
|
|
1102
1116
|
bug_list = project / ".prizmkit" / "plans" / "bug-fix-list.json"
|
|
1103
1117
|
bug_list.parent.mkdir(parents=True)
|
|
1118
|
+
bug = {"id": "B-001", "title": "Broken", "description": "Fix it", "acceptance_criteria": ["Works"]}
|
|
1119
|
+
if bug_overrides:
|
|
1120
|
+
bug.update(bug_overrides)
|
|
1104
1121
|
bug_list.write_text(json.dumps({
|
|
1105
|
-
"bugs": [
|
|
1122
|
+
"bugs": [bug],
|
|
1106
1123
|
"global_context": {"language": "Python"},
|
|
1107
1124
|
}), encoding="utf-8")
|
|
1108
1125
|
output = project / "bugfix-prompt.md"
|
|
@@ -1119,8 +1136,8 @@ def _render_bugfix_prompt(tmp_path, platform="claude", explicit_platform=None):
|
|
|
1119
1136
|
"--bug-id", "B-001",
|
|
1120
1137
|
"--session-id", "session-1",
|
|
1121
1138
|
"--run-id", "run-1",
|
|
1122
|
-
"--retry-count",
|
|
1123
|
-
"--resume-phase",
|
|
1139
|
+
"--retry-count", retry_count,
|
|
1140
|
+
"--resume-phase", resume_phase,
|
|
1124
1141
|
"--state-dir", str(project / ".prizmkit" / "state" / "bugfix"),
|
|
1125
1142
|
"--project-root", str(project),
|
|
1126
1143
|
"--output", str(output),
|
|
@@ -1139,7 +1156,7 @@ def _render_bugfix_prompt(tmp_path, platform="claude", explicit_platform=None):
|
|
|
1139
1156
|
return output.read_text(encoding="utf-8")
|
|
1140
1157
|
|
|
1141
1158
|
|
|
1142
|
-
def _render_refactor_prompt(tmp_path, platform="claude", explicit_platform=None):
|
|
1159
|
+
def _render_refactor_prompt(tmp_path, platform="claude", explicit_platform=None, refactor_overrides=None):
|
|
1143
1160
|
from generate_refactor_prompt import main as refactor_main
|
|
1144
1161
|
|
|
1145
1162
|
project = tmp_path / "refactor-project"
|
|
@@ -1151,14 +1168,17 @@ def _render_refactor_prompt(tmp_path, platform="claude", explicit_platform=None)
|
|
|
1151
1168
|
_ensure_agent_files(project)
|
|
1152
1169
|
refactor_list = project / ".prizmkit" / "plans" / "refactor-list.json"
|
|
1153
1170
|
refactor_list.parent.mkdir(parents=True)
|
|
1171
|
+
refactor = {
|
|
1172
|
+
"id": "R-001",
|
|
1173
|
+
"title": "Restructure",
|
|
1174
|
+
"description": "Preserve behavior",
|
|
1175
|
+
"acceptance_criteria": ["Behavior preserved"],
|
|
1176
|
+
"scope": {"files": ["src/a.py"], "modules": ["src"]},
|
|
1177
|
+
}
|
|
1178
|
+
if refactor_overrides:
|
|
1179
|
+
refactor.update(refactor_overrides)
|
|
1154
1180
|
refactor_list.write_text(json.dumps({
|
|
1155
|
-
"refactors": [
|
|
1156
|
-
"id": "R-001",
|
|
1157
|
-
"title": "Restructure",
|
|
1158
|
-
"description": "Preserve behavior",
|
|
1159
|
-
"acceptance_criteria": ["Behavior preserved"],
|
|
1160
|
-
"scope": {"files": ["src/a.py"], "modules": ["src"]},
|
|
1161
|
-
}],
|
|
1181
|
+
"refactors": [refactor],
|
|
1162
1182
|
"global_context": {"language": "Python"},
|
|
1163
1183
|
}), encoding="utf-8")
|
|
1164
1184
|
output = project / "refactor-prompt.md"
|
|
@@ -1178,18 +1198,14 @@ def _render_refactor_prompt(tmp_path, platform="claude", explicit_platform=None)
|
|
|
1178
1198
|
"--retry-count", "0",
|
|
1179
1199
|
"--resume-phase", "null",
|
|
1180
1200
|
"--state-dir", str(project / ".prizmkit" / "state" / "refactor"),
|
|
1201
|
+
"--project-root", str(project),
|
|
1181
1202
|
"--output", str(output),
|
|
1182
1203
|
]
|
|
1183
|
-
old_cwd = os.getcwd()
|
|
1184
|
-
os.chdir(project)
|
|
1185
1204
|
try:
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
raise
|
|
1191
|
-
finally:
|
|
1192
|
-
os.chdir(old_cwd)
|
|
1205
|
+
refactor_main()
|
|
1206
|
+
except SystemExit as exc:
|
|
1207
|
+
if exc.code not in (0, None):
|
|
1208
|
+
raise
|
|
1193
1209
|
finally:
|
|
1194
1210
|
os.sys.argv = old_argv
|
|
1195
1211
|
if old_platform is None:
|
|
@@ -1199,6 +1215,67 @@ def _render_refactor_prompt(tmp_path, platform="claude", explicit_platform=None)
|
|
|
1199
1215
|
return output.read_text(encoding="utf-8")
|
|
1200
1216
|
|
|
1201
1217
|
|
|
1218
|
+
class TestBrowserBlockingPromptPolicy:
|
|
1219
|
+
def test_feature_bugfix_refactor_blocking_browser_policy_stops_before_commit(self, tmp_path):
|
|
1220
|
+
browser_interaction = {
|
|
1221
|
+
"tool": "auto",
|
|
1222
|
+
"blocking": True,
|
|
1223
|
+
"verify_steps": ["Open the page"],
|
|
1224
|
+
}
|
|
1225
|
+
prompts = [
|
|
1226
|
+
_render_feature_prompt(tmp_path / "feature-browser-blocking", browser_interaction=browser_interaction),
|
|
1227
|
+
_render_bugfix_prompt(tmp_path / "bugfix-browser-blocking", bug_overrides={"browser_interaction": browser_interaction}),
|
|
1228
|
+
_render_refactor_prompt(tmp_path / "refactor-browser-blocking", refactor_overrides={"browser_interaction": browser_interaction}),
|
|
1229
|
+
]
|
|
1230
|
+
|
|
1231
|
+
for prompt in prompts:
|
|
1232
|
+
assert "Browser blocking policy: `true`" in prompt
|
|
1233
|
+
assert "stop before commit" in prompt
|
|
1234
|
+
assert "Failures do NOT block the commit" not in prompt
|
|
1235
|
+
|
|
1236
|
+
def test_feature_bugfix_refactor_nonblocking_browser_policy_continues(self, tmp_path):
|
|
1237
|
+
browser_interaction = {
|
|
1238
|
+
"tool": "auto",
|
|
1239
|
+
"blocking": False,
|
|
1240
|
+
"verify_steps": ["Open the page"],
|
|
1241
|
+
}
|
|
1242
|
+
prompts = [
|
|
1243
|
+
_render_feature_prompt(tmp_path / "feature-browser-nonblocking", browser_interaction=browser_interaction),
|
|
1244
|
+
_render_bugfix_prompt(tmp_path / "bugfix-browser-nonblocking", bug_overrides={"browser_interaction": browser_interaction}),
|
|
1245
|
+
_render_refactor_prompt(tmp_path / "refactor-browser-nonblocking", refactor_overrides={"browser_interaction": browser_interaction}),
|
|
1246
|
+
]
|
|
1247
|
+
|
|
1248
|
+
for prompt in prompts:
|
|
1249
|
+
assert "Browser blocking policy: `false`" in prompt
|
|
1250
|
+
assert "clean up the dev server/browser, and continue" in prompt
|
|
1251
|
+
assert "Failures do NOT block the commit" not in prompt
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
class TestManualHybridBugfixPromptPolicy:
|
|
1255
|
+
def test_fresh_manual_bugfix_prompt_stops_before_commit_phase(self, tmp_path):
|
|
1256
|
+
prompt = _render_bugfix_prompt(
|
|
1257
|
+
tmp_path / "fresh-manual-bugfix",
|
|
1258
|
+
bug_overrides={"verification_type": "manual"},
|
|
1259
|
+
)
|
|
1260
|
+
|
|
1261
|
+
assert "Manual / Hybrid Verification Gate" in prompt
|
|
1262
|
+
assert "Stop before `/prizmkit-retrospective` and `/prizmkit-committer`" in prompt
|
|
1263
|
+
assert "### Phase 4: Commit & Learn" not in prompt
|
|
1264
|
+
assert "Run `/prizmkit-committer` with commit prefix" not in prompt
|
|
1265
|
+
|
|
1266
|
+
def test_recovery_manual_bugfix_prompt_includes_commit_phase_with_substitution_policy(self, tmp_path):
|
|
1267
|
+
prompt = _render_bugfix_prompt(
|
|
1268
|
+
tmp_path / "recovery-manual-bugfix",
|
|
1269
|
+
bug_overrides={"verification_type": "manual"},
|
|
1270
|
+
retry_count="1",
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
assert "Manual / Hybrid Verification Gate" not in prompt
|
|
1274
|
+
assert "Recovery-class session: automated verification may substitute for manual/hybrid UAT" in prompt
|
|
1275
|
+
assert "### Phase 4: Commit & Learn" in prompt
|
|
1276
|
+
assert "Run `/prizmkit-committer` with commit prefix" in prompt
|
|
1277
|
+
|
|
1278
|
+
|
|
1202
1279
|
class TestHeadlessPromptCleanupF033:
|
|
1203
1280
|
def test_rendered_feature_bugfix_refactor_prompts_are_clean(self, tmp_path):
|
|
1204
1281
|
prompts = []
|
|
@@ -1311,7 +1388,6 @@ class TestHeadlessPromptCleanupF033:
|
|
|
1311
1388
|
refactor_prompt = _render_refactor_prompt(tmp_path / "refactor-mixed", platform="claude")
|
|
1312
1389
|
|
|
1313
1390
|
assert replacements["{{REVIEWER_SUBAGENT_PATH}}"].endswith(".claude/agents/prizm-dev-team-reviewer.md")
|
|
1314
|
-
assert ".claude/agents/prizm-dev-team-reviewer.md" in refactor_prompt
|
|
1315
1391
|
assert ".codex/agents/prizm-dev-team-reviewer.toml" not in refactor_prompt
|
|
1316
1392
|
|
|
1317
1393
|
def test_bugfix_refactor_explicit_codex_override(self, tmp_path, monkeypatch):
|
|
@@ -1333,7 +1409,6 @@ class TestHeadlessPromptCleanupF033:
|
|
|
1333
1409
|
refactor_prompt = _render_refactor_prompt(tmp_path / "refactor-codex", platform="claude", explicit_platform="codex")
|
|
1334
1410
|
|
|
1335
1411
|
assert replacements["{{REVIEWER_SUBAGENT_PATH}}"].endswith(".codex/agents/prizm-dev-team-reviewer.toml")
|
|
1336
|
-
assert ".codex/agents/prizm-dev-team-reviewer.toml" in refactor_prompt
|
|
1337
1412
|
assert ".claude/agents/prizm-dev-team-reviewer.md" not in refactor_prompt
|
|
1338
1413
|
|
|
1339
1414
|
def test_explicit_codex_env_still_renders_codex_critic_path(self, tmp_path, monkeypatch):
|
|
@@ -1399,7 +1474,9 @@ class TestFeatureBootstrapShellExtraction:
|
|
|
1399
1474
|
assert "executable version opencli --arg=--version" in prompt
|
|
1400
1475
|
assert "process start --pid-file .prizmkit/state/browser-dev-server.pid" in prompt
|
|
1401
1476
|
assert "process cleanup-pid <pid-from-.prizmkit/state/browser-dev-server.pid>" in prompt
|
|
1402
|
-
assert "port check <DEV_PORT>" in prompt
|
|
1477
|
+
assert "port check <DEV_PORT>" in prompt
|
|
1478
|
+
assert "port check 3000" not in prompt
|
|
1479
|
+
assert "localhost:3000" not in prompt
|
|
1403
1480
|
|
|
1404
1481
|
def test_rendered_legacy_tier_feature_prompts_use_runtime_helper_not_extracted_bash(self, tmp_path):
|
|
1405
1482
|
cases = [
|
|
@@ -1419,7 +1496,8 @@ class TestFeatureBootstrapShellExtraction:
|
|
|
1419
1496
|
self.assert_shell_extracted_operations_removed(prompt)
|
|
1420
1497
|
assert "process start --pid-file .prizmkit/state/browser-dev-server.pid" in prompt
|
|
1421
1498
|
assert "process cleanup-pid <pid-from-.prizmkit/state/browser-dev-server.pid>" in prompt
|
|
1422
|
-
assert "port check 3000" in prompt
|
|
1499
|
+
assert "port check 3000" not in prompt
|
|
1500
|
+
assert "localhost:3000" not in prompt
|
|
1423
1501
|
|
|
1424
1502
|
def test_legacy_tier_specific_extracted_operations_map_to_helper_commands(self, tmp_path):
|
|
1425
1503
|
tier1 = _render_feature_prompt(
|
|
@@ -307,6 +307,20 @@ def test_template_source_is_clean_for_headless_prompt_contract():
|
|
|
307
307
|
assert forbidden not in content
|
|
308
308
|
|
|
309
309
|
|
|
310
|
+
def test_bugfix_prompt_keeps_fine_grained_phases_out_of_external_status_contract():
|
|
311
|
+
content = Path("dev-pipeline/templates/bugfix-bootstrap-prompt.md").read_text(encoding="utf-8")
|
|
312
|
+
old_statuses = ["triaging", "reproducing", "fixing", "verifying"]
|
|
313
|
+
|
|
314
|
+
assert "external bug-list status field" in content
|
|
315
|
+
assert "pending`, `in_progress`, `completed`, `failed`, `skipped`, or `needs_info`" in content
|
|
316
|
+
for status in old_statuses:
|
|
317
|
+
assert f"status=\"{status}\"" not in content
|
|
318
|
+
assert f"status = \"{status}\"" not in content
|
|
319
|
+
assert f"status='{status}'" not in content
|
|
320
|
+
assert f"status = '{status}'" not in content
|
|
321
|
+
assert f"Set bug status to `{status}`" not in content
|
|
322
|
+
|
|
323
|
+
|
|
310
324
|
# ---------------------------------------------------------------------------
|
|
311
325
|
# Anti-Bash regression guardrails (F-039)
|
|
312
326
|
# Note: grep -n, sed -n, grep -q, wc -l excluded — referenced in bugfix context
|
|
@@ -61,6 +61,35 @@ def _write_feature_list(paths):
|
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
def _write_family_plan(paths, kind, items):
|
|
65
|
+
plan_config = {
|
|
66
|
+
"feature": (paths.feature_plan, "features", "dev-pipeline-feature-list-v1"),
|
|
67
|
+
"bugfix": (paths.bugfix_plan, "bugs", "dev-pipeline-bug-fix-list-v1"),
|
|
68
|
+
"refactor": (paths.refactor_plan, "refactors", "dev-pipeline-refactor-list-v1"),
|
|
69
|
+
}
|
|
70
|
+
plan_path, list_key, schema = plan_config[kind]
|
|
71
|
+
plan_path.write_text(
|
|
72
|
+
json.dumps({"$schema": schema, "project_name": "Fake", list_key: items}),
|
|
73
|
+
encoding="utf-8",
|
|
74
|
+
)
|
|
75
|
+
return plan_path
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _get_next_result(kind, paths):
|
|
79
|
+
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
80
|
+
from prizmkit_runtime.runner_status import get_next
|
|
81
|
+
|
|
82
|
+
family = family_for(kind, paths)
|
|
83
|
+
invocation = parse_invocation(family, "run", ())
|
|
84
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": family.plan_path})
|
|
85
|
+
return get_next(family, invocation, paths.project_root)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _item_id_from_next_result(kind, result):
|
|
89
|
+
key = {"feature": "feature_id", "bugfix": "bug_id", "refactor": "refactor_id"}[kind]
|
|
90
|
+
return result.data[key]
|
|
91
|
+
|
|
92
|
+
|
|
64
93
|
def test_retired_shell_runtime_files_are_absent_from_source():
|
|
65
94
|
retired = [
|
|
66
95
|
"run-feature.sh",
|
|
@@ -360,6 +389,216 @@ def test_status_bridge_preserves_feature_filter_selection(tmp_path):
|
|
|
360
389
|
assert update.data["context_overflow_count"] == 1
|
|
361
390
|
|
|
362
391
|
|
|
392
|
+
def test_status_bridge_uses_stable_dependency_order_across_pipeline_families(tmp_path):
|
|
393
|
+
cases = {
|
|
394
|
+
"feature": [
|
|
395
|
+
{"id": "F-001", "title": "Blocked", "status": "pending", "priority": "critical", "dependencies": ["F-002"]},
|
|
396
|
+
{"id": "F-002", "title": "First", "status": "pending", "priority": "low", "dependencies": []},
|
|
397
|
+
{"id": "F-003", "title": "Independent", "status": "pending", "priority": "high", "dependencies": []},
|
|
398
|
+
],
|
|
399
|
+
"bugfix": [
|
|
400
|
+
{"id": "B-001", "title": "Blocked", "status": "pending", "severity": "critical", "priority": "high", "error_source": {"type": "user_report"}, "verification_type": "manual", "acceptance_criteria": ["fixed"], "dependencies": ["B-002"]},
|
|
401
|
+
{"id": "B-002", "title": "First", "status": "pending", "severity": "low", "priority": "low", "error_source": {"type": "user_report"}, "verification_type": "manual", "acceptance_criteria": ["fixed"], "dependencies": []},
|
|
402
|
+
{"id": "B-003", "title": "Independent", "status": "pending", "severity": "high", "priority": "high", "error_source": {"type": "user_report"}, "verification_type": "manual", "acceptance_criteria": ["fixed"], "dependencies": []},
|
|
403
|
+
],
|
|
404
|
+
"refactor": [
|
|
405
|
+
{"id": "R-001", "title": "Blocked", "description": "blocked", "scope": {}, "type": "restructure", "priority": "critical", "complexity": "high", "behavior_preservation": {"strategy": "test-gate"}, "acceptance_criteria": ["done"], "dependencies": ["R-002"], "status": "pending"},
|
|
406
|
+
{"id": "R-002", "title": "First", "description": "first", "scope": {}, "type": "extract", "priority": "low", "complexity": "low", "behavior_preservation": {"strategy": "test-gate"}, "acceptance_criteria": ["done"], "dependencies": [], "status": "pending"},
|
|
407
|
+
{"id": "R-003", "title": "Independent", "description": "independent", "scope": {}, "type": "simplify", "priority": "high", "complexity": "medium", "behavior_preservation": {"strategy": "test-gate"}, "acceptance_criteria": ["done"], "dependencies": [], "status": "pending"},
|
|
408
|
+
],
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for kind, items in cases.items():
|
|
412
|
+
paths = _make_paths(tmp_path / f"stable-{kind}")
|
|
413
|
+
_write_family_plan(paths, kind, items)
|
|
414
|
+
|
|
415
|
+
result = _get_next_result(kind, paths)
|
|
416
|
+
|
|
417
|
+
assert result.ok
|
|
418
|
+
assert _item_id_from_next_result(kind, result) == {"feature": "F-002", "bugfix": "B-002", "refactor": "R-002"}[kind]
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def test_status_bridge_prefers_in_progress_items_with_satisfied_dependencies(tmp_path):
|
|
422
|
+
cases = {
|
|
423
|
+
"feature": [
|
|
424
|
+
{"id": "F-001", "title": "Done", "status": "completed", "priority": "low", "dependencies": []},
|
|
425
|
+
{"id": "F-002", "title": "Resume", "status": "in_progress", "priority": "low", "dependencies": ["F-001"]},
|
|
426
|
+
{"id": "F-003", "title": "Pending", "status": "pending", "priority": "critical", "dependencies": []},
|
|
427
|
+
],
|
|
428
|
+
"bugfix": [
|
|
429
|
+
{"id": "B-001", "title": "Done", "status": "completed", "severity": "low", "priority": "low", "error_source": {"type": "user_report"}, "verification_type": "manual", "acceptance_criteria": ["fixed"], "dependencies": []},
|
|
430
|
+
{"id": "B-002", "title": "Resume", "status": "in_progress", "severity": "low", "priority": "low", "error_source": {"type": "user_report"}, "verification_type": "manual", "acceptance_criteria": ["fixed"], "dependencies": ["B-001"]},
|
|
431
|
+
{"id": "B-003", "title": "Pending", "status": "pending", "severity": "critical", "priority": "high", "error_source": {"type": "user_report"}, "verification_type": "manual", "acceptance_criteria": ["fixed"], "dependencies": []},
|
|
432
|
+
],
|
|
433
|
+
"refactor": [
|
|
434
|
+
{"id": "R-001", "title": "Done", "description": "done", "scope": {}, "type": "extract", "priority": "low", "complexity": "low", "behavior_preservation": {"strategy": "test-gate"}, "acceptance_criteria": ["done"], "dependencies": [], "status": "completed"},
|
|
435
|
+
{"id": "R-002", "title": "Resume", "description": "resume", "scope": {}, "type": "extract", "priority": "low", "complexity": "low", "behavior_preservation": {"strategy": "test-gate"}, "acceptance_criteria": ["done"], "dependencies": ["R-001"], "status": "in_progress"},
|
|
436
|
+
{"id": "R-003", "title": "Pending", "description": "pending", "scope": {}, "type": "migrate", "priority": "critical", "complexity": "high", "behavior_preservation": {"strategy": "test-gate"}, "acceptance_criteria": ["done"], "dependencies": [], "status": "pending"},
|
|
437
|
+
],
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
for kind, items in cases.items():
|
|
441
|
+
paths = _make_paths(tmp_path / f"resume-{kind}")
|
|
442
|
+
_write_family_plan(paths, kind, items)
|
|
443
|
+
|
|
444
|
+
result = _get_next_result(kind, paths)
|
|
445
|
+
|
|
446
|
+
assert result.ok
|
|
447
|
+
assert _item_id_from_next_result(kind, result) == {"feature": "F-002", "bugfix": "B-002", "refactor": "R-002"}[kind]
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def test_refactor_dependencies_require_completed_not_other_terminal_statuses(tmp_path):
|
|
451
|
+
paths = _make_paths(tmp_path)
|
|
452
|
+
_write_family_plan(
|
|
453
|
+
paths,
|
|
454
|
+
"refactor",
|
|
455
|
+
[
|
|
456
|
+
{"id": "R-001", "title": "Failed Dependency", "description": "failed", "scope": {}, "type": "extract", "priority": "high", "complexity": "low", "behavior_preservation": {"strategy": "test-gate"}, "acceptance_criteria": ["done"], "dependencies": [], "status": "failed"},
|
|
457
|
+
{"id": "R-002", "title": "Blocked", "description": "blocked", "scope": {}, "type": "restructure", "priority": "high", "complexity": "medium", "behavior_preservation": {"strategy": "test-gate"}, "acceptance_criteria": ["done"], "dependencies": ["R-001"], "status": "pending"},
|
|
458
|
+
],
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
result = _get_next_result("refactor", paths)
|
|
462
|
+
|
|
463
|
+
assert result.ok
|
|
464
|
+
assert result.text == "PIPELINE_BLOCKED"
|
|
465
|
+
assert result.data is None
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def test_init_scripts_reject_missing_self_and_cycle_dependencies(tmp_path):
|
|
469
|
+
script_config = {
|
|
470
|
+
"feature": ("init-pipeline.py", "--feature-list", "F-", "missing dependency"),
|
|
471
|
+
"bugfix": ("init-bugfix-pipeline.py", "--bug-list", "B-", "missing dependency"),
|
|
472
|
+
"refactor": ("init-refactor-pipeline.py", "--refactor-list", "R-", "missing dependency"),
|
|
473
|
+
}
|
|
474
|
+
scenarios = {
|
|
475
|
+
"missing": lambda kind, prefix: _family_items(kind, [
|
|
476
|
+
(f"{prefix}001", [f"{prefix}999"], "pending"),
|
|
477
|
+
]),
|
|
478
|
+
"self": lambda kind, prefix: _family_items(kind, [
|
|
479
|
+
(f"{prefix}001", [f"{prefix}001"], "pending"),
|
|
480
|
+
]),
|
|
481
|
+
"cycle": lambda kind, prefix: _family_items(kind, [
|
|
482
|
+
(f"{prefix}001", [f"{prefix}002"], "pending"),
|
|
483
|
+
(f"{prefix}002", [f"{prefix}001"], "pending"),
|
|
484
|
+
]),
|
|
485
|
+
}
|
|
486
|
+
expected_fragments = {
|
|
487
|
+
"missing": "missing dependency",
|
|
488
|
+
"self": "cannot depend on itself",
|
|
489
|
+
"cycle": "Dependency cycle detected",
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
for kind, (script_name, list_arg, prefix, _missing_fragment) in script_config.items():
|
|
493
|
+
for scenario, build_items in scenarios.items():
|
|
494
|
+
paths = _make_paths(tmp_path / f"init-{kind}-{scenario}")
|
|
495
|
+
plan_path = _write_family_plan(paths, kind, build_items(kind, prefix))
|
|
496
|
+
result = subprocess.run(
|
|
497
|
+
[
|
|
498
|
+
PYTHON_310_PLUS,
|
|
499
|
+
str(PIPELINE_ROOT / "scripts" / script_name),
|
|
500
|
+
list_arg,
|
|
501
|
+
str(plan_path),
|
|
502
|
+
"--state-dir",
|
|
503
|
+
str(paths.project_root / ".prizmkit" / "state" / f"init-{kind}-{scenario}"),
|
|
504
|
+
],
|
|
505
|
+
cwd=REPO_ROOT,
|
|
506
|
+
stdout=subprocess.PIPE,
|
|
507
|
+
stderr=subprocess.PIPE,
|
|
508
|
+
text=True,
|
|
509
|
+
check=False,
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
assert result.returncode == 1, (kind, scenario, result.stdout, result.stderr)
|
|
513
|
+
payload = json.loads(result.stdout)
|
|
514
|
+
assert any(expected_fragments[scenario] in error for error in payload["errors"])
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def test_bug_planner_validator_rejects_dependency_cycles():
|
|
518
|
+
from importlib.util import module_from_spec, spec_from_file_location
|
|
519
|
+
|
|
520
|
+
script_path = REPO_ROOT / "core" / "skills" / "orchestration-skill" / "pipelines" / "bug-planner" / "scripts" / "validate-bug-list.py"
|
|
521
|
+
spec = spec_from_file_location("bug_planner_validate_bug_list", script_path)
|
|
522
|
+
module = module_from_spec(spec)
|
|
523
|
+
spec.loader.exec_module(module)
|
|
524
|
+
|
|
525
|
+
result = module.validate({
|
|
526
|
+
"$schema": "dev-pipeline-bug-fix-list-v1",
|
|
527
|
+
"project_name": "Fake",
|
|
528
|
+
"bugs": [
|
|
529
|
+
{
|
|
530
|
+
"id": "B-001",
|
|
531
|
+
"title": "First",
|
|
532
|
+
"description": "First bug",
|
|
533
|
+
"severity": "medium",
|
|
534
|
+
"error_source": {"type": "user_report"},
|
|
535
|
+
"verification_type": "manual",
|
|
536
|
+
"acceptance_criteria": ["fixed"],
|
|
537
|
+
"dependencies": ["B-002"],
|
|
538
|
+
"status": "pending",
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
"id": "B-002",
|
|
542
|
+
"title": "Second",
|
|
543
|
+
"description": "Second bug",
|
|
544
|
+
"severity": "medium",
|
|
545
|
+
"error_source": {"type": "user_report"},
|
|
546
|
+
"verification_type": "manual",
|
|
547
|
+
"acceptance_criteria": ["fixed"],
|
|
548
|
+
"dependencies": ["B-001"],
|
|
549
|
+
"status": "pending",
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
assert result["valid"] is False
|
|
555
|
+
assert any("Dependency cycle detected" in error for error in result["errors"])
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _family_items(kind, entries):
|
|
560
|
+
items = []
|
|
561
|
+
for item_id, dependencies, status in entries:
|
|
562
|
+
if kind == "feature":
|
|
563
|
+
items.append({
|
|
564
|
+
"id": item_id,
|
|
565
|
+
"title": item_id,
|
|
566
|
+
"description": "Implement this feature with enough detail for initialization validation.",
|
|
567
|
+
"priority": "medium",
|
|
568
|
+
"dependencies": dependencies,
|
|
569
|
+
"acceptance_criteria": ["done"],
|
|
570
|
+
"status": status,
|
|
571
|
+
})
|
|
572
|
+
elif kind == "bugfix":
|
|
573
|
+
items.append({
|
|
574
|
+
"id": item_id,
|
|
575
|
+
"title": item_id,
|
|
576
|
+
"description": "Bug description",
|
|
577
|
+
"severity": "medium",
|
|
578
|
+
"priority": "medium",
|
|
579
|
+
"error_source": {"type": "user_report"},
|
|
580
|
+
"verification_type": "manual",
|
|
581
|
+
"acceptance_criteria": ["fixed"],
|
|
582
|
+
"dependencies": dependencies,
|
|
583
|
+
"status": status,
|
|
584
|
+
})
|
|
585
|
+
else:
|
|
586
|
+
items.append({
|
|
587
|
+
"id": item_id,
|
|
588
|
+
"title": item_id,
|
|
589
|
+
"description": "Refactor description",
|
|
590
|
+
"scope": {},
|
|
591
|
+
"type": "extract",
|
|
592
|
+
"priority": "medium",
|
|
593
|
+
"complexity": "low",
|
|
594
|
+
"behavior_preservation": {"strategy": "test-gate"},
|
|
595
|
+
"acceptance_criteria": ["done"],
|
|
596
|
+
"dependencies": dependencies,
|
|
597
|
+
"status": status,
|
|
598
|
+
})
|
|
599
|
+
return items
|
|
600
|
+
|
|
601
|
+
|
|
363
602
|
def test_status_bridge_passes_max_retries_from_invocation_or_environment(monkeypatch, tmp_path):
|
|
364
603
|
import subprocess
|
|
365
604
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
@@ -458,7 +458,7 @@ def test_interoperability_preserves_existing_utility_mapping():
|
|
|
458
458
|
explicitly_library_only = set(UTILITY_LIBRARY_ONLY)
|
|
459
459
|
|
|
460
460
|
assert actual_scripts == mapped_scripts | explicitly_library_only
|
|
461
|
-
assert explicitly_library_only == {"continuation.py", "utils.py"}
|
|
461
|
+
assert explicitly_library_only == {"continuation.py", "prompt_framework.py", "utils.py"}
|
|
462
462
|
assert "parse-stream-progress.py" in mapped_scripts
|
|
463
463
|
assert "prizmkit-test-gate.py" not in mapped_scripts
|
|
464
464
|
|
|
@@ -803,7 +803,11 @@ def test_headless_subagent_prompt_guidance_keeps_no_worktree_boundary_visible():
|
|
|
803
803
|
|
|
804
804
|
for path in guidance_files:
|
|
805
805
|
text = path.read_text(encoding="utf-8")
|
|
806
|
-
assert
|
|
806
|
+
assert (
|
|
807
|
+
"default/no worktree" in text
|
|
808
|
+
or "current-workspace / active-checkout / no-worktree" in text
|
|
809
|
+
or "current workspace / active checkout / no worktree" in text
|
|
810
|
+
), path
|
|
807
811
|
assert "do not request or create" in text.lower() or "do not request worktree isolation" in text.lower(), path
|
|
808
812
|
assert ".claude/worktrees/..." in text, path
|
|
809
813
|
assert ".prizmkit/state/worktrees/..." in text, path
|
|
@@ -4,7 +4,13 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
import tempfile
|
|
6
6
|
|
|
7
|
-
from utils import
|
|
7
|
+
from utils import (
|
|
8
|
+
load_json_file,
|
|
9
|
+
write_json_file,
|
|
10
|
+
pad_right,
|
|
11
|
+
_build_progress_bar,
|
|
12
|
+
stable_topological_order,
|
|
13
|
+
)
|
|
8
14
|
|
|
9
15
|
|
|
10
16
|
# ---------------------------------------------------------------------------
|
|
@@ -80,3 +86,62 @@ class TestBuildProgressBar:
|
|
|
80
86
|
def test_fifty_percent(self):
|
|
81
87
|
bar = _build_progress_bar(50, width=10)
|
|
82
88
|
assert "50%" in bar
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# stable_topological_order
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
class TestStableTopologicalOrder:
|
|
96
|
+
def test_dependencies_precede_dependents_with_stable_tie_breaking(self):
|
|
97
|
+
items = [
|
|
98
|
+
{"id": "F-001", "dependencies": []},
|
|
99
|
+
{"id": "F-002", "dependencies": ["F-001"]},
|
|
100
|
+
{"id": "F-003", "dependencies": []},
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
ordered, errors = stable_topological_order(items, item_label="Feature")
|
|
104
|
+
|
|
105
|
+
assert errors == []
|
|
106
|
+
assert [item["id"] for item in ordered] == ["F-001", "F-002", "F-003"]
|
|
107
|
+
|
|
108
|
+
def test_unrelated_items_preserve_original_list_order(self):
|
|
109
|
+
items = [
|
|
110
|
+
{"id": "F-010", "dependencies": []},
|
|
111
|
+
{"id": "F-002", "dependencies": []},
|
|
112
|
+
{"id": "F-007", "dependencies": []},
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
ordered, errors = stable_topological_order(items, item_label="Feature")
|
|
116
|
+
|
|
117
|
+
assert errors == []
|
|
118
|
+
assert [item["id"] for item in ordered] == ["F-010", "F-002", "F-007"]
|
|
119
|
+
|
|
120
|
+
def test_missing_dependency_is_blocking_error(self):
|
|
121
|
+
items = [
|
|
122
|
+
{"id": "B-001", "dependencies": ["B-999"]},
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
_ordered, errors = stable_topological_order(items, item_label="Bug")
|
|
126
|
+
|
|
127
|
+
assert errors == ["Bug 'B-001' depends on missing dependency 'B-999'"]
|
|
128
|
+
|
|
129
|
+
def test_self_dependency_is_blocking_error(self):
|
|
130
|
+
items = [
|
|
131
|
+
{"id": "R-001", "dependencies": ["R-001"]},
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
_ordered, errors = stable_topological_order(items, item_label="Refactor")
|
|
135
|
+
|
|
136
|
+
assert errors == ["Refactor 'R-001' cannot depend on itself"]
|
|
137
|
+
|
|
138
|
+
def test_cycle_reports_chain(self):
|
|
139
|
+
items = [
|
|
140
|
+
{"id": "F-001", "dependencies": ["F-003"]},
|
|
141
|
+
{"id": "F-002", "dependencies": ["F-001"]},
|
|
142
|
+
{"id": "F-003", "dependencies": ["F-002"]},
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
_ordered, errors = stable_topological_order(items, item_label="Feature")
|
|
146
|
+
|
|
147
|
+
assert errors == ["Dependency cycle detected: F-001 -> F-003 -> F-002 -> F-001"]
|