prizmkit 1.1.85 → 1.1.87
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/dev-pipeline/scripts/generate-bootstrap-prompt.py +462 -4
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +14 -0
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +18 -3
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +18 -3
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-commit.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +260 -0
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +5 -1
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +5 -1
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +429 -0
- package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +462 -4
- package/bundled/dev-pipeline-windows/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +14 -0
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +18 -3
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +18 -3
- package/bundled/dev-pipeline-windows/templates/sections/phase-commit-full.md +2 -2
- package/bundled/dev-pipeline-windows/templates/sections/phase-commit.md +2 -2
- package/bundled/dev-pipeline-windows/templates/sections/phase-prizmkit-test.md +186 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +5 -1
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +5 -1
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +7 -2
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +6 -2
- package/bundled/skills/prizmkit-test/SKILL.md +107 -13
- package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +220 -0
- package/bundled/skills/prizmkit-test/references/examples.md +82 -9
- package/bundled/skills/prizmkit-test/references/service-boundary-test-catalog.md +225 -0
- package/bundled/skills/prizmkit-test/references/test-generation-steps.md +74 -14
- package/bundled/skills/prizmkit-test/references/test-report-template.md +74 -5
- package/bundled/skills/prizmkit-test/scripts/validate_boundary_report.py +347 -0
- package/bundled/skills-windows/prizmkit-code-review/SKILL.md +7 -2
- package/bundled/skills-windows/prizmkit-code-review/references/reviewer-agent-prompt.md +6 -2
- package/bundled/skills-windows/prizmkit-test/SKILL.md +107 -13
- package/bundled/skills-windows/prizmkit-test/references/boundary-coverage-protocol.md +220 -0
- package/bundled/skills-windows/prizmkit-test/references/examples.md +82 -9
- package/bundled/skills-windows/prizmkit-test/references/service-boundary-test-catalog.md +225 -0
- package/bundled/skills-windows/prizmkit-test/references/test-generation-steps.md +74 -14
- package/bundled/skills-windows/prizmkit-test/references/test-report-template.md +74 -5
- package/bundled/skills-windows/prizmkit-test/scripts/validate_boundary_report.py +347 -0
- package/package.json +1 -1
package/bundled/VERSION.json
CHANGED
|
@@ -21,6 +21,7 @@ Usage:
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
import argparse
|
|
24
|
+
import glob
|
|
24
25
|
import json
|
|
25
26
|
import os
|
|
26
27
|
import re
|
|
@@ -623,6 +624,11 @@ SECTION_TO_SKILL = {
|
|
|
623
624
|
[".prizmkit/specs/{slug}/plan.md"]),
|
|
624
625
|
"phase-critic-plan": ("critic-plan-review", "Critic: Plan Review", []),
|
|
625
626
|
"phase-implement": ("prizmkit-implement", "Implement + Test", []),
|
|
627
|
+
"phase-prizmkit-test": (
|
|
628
|
+
"prizmkit-test", "Scoped Feature Test Gate",
|
|
629
|
+
[".prizmkit/specs/{slug}/test-report-path.txt",
|
|
630
|
+
".prizmkit/test/*/test-report.md"],
|
|
631
|
+
),
|
|
626
632
|
"phase-review": ("prizmkit-code-review", "Code Review", []),
|
|
627
633
|
"phase-browser": ("browser-verification", "Browser Verification", []),
|
|
628
634
|
"phase-commit": None, # special: split into retrospective + committer
|
|
@@ -640,6 +646,446 @@ def _resolve_artifacts(artifact_templates, slug):
|
|
|
640
646
|
return [a.replace("{slug}", slug) for a in artifact_templates]
|
|
641
647
|
|
|
642
648
|
|
|
649
|
+
def _artifact_exists(project_root, artifact):
|
|
650
|
+
"""Return whether an artifact path or glob exists under project_root."""
|
|
651
|
+
full_path = os.path.join(project_root, artifact)
|
|
652
|
+
if glob.has_magic(full_path):
|
|
653
|
+
return bool(glob.glob(full_path))
|
|
654
|
+
return os.path.exists(full_path)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _read_text(path):
|
|
658
|
+
"""Read a UTF-8 text file, returning an empty string on IO errors."""
|
|
659
|
+
try:
|
|
660
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
661
|
+
return f.read()
|
|
662
|
+
except IOError:
|
|
663
|
+
return ""
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _normalize_artifact_dir(path):
|
|
667
|
+
"""Normalize report artifact_dir values for comparison."""
|
|
668
|
+
return path.strip().replace("\\", "/").rstrip("/")
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _report_path_matches_artifacts(project_root, report_path, artifacts):
|
|
672
|
+
"""Return whether report_path is an in-project test report artifact."""
|
|
673
|
+
test_root = os.path.realpath(
|
|
674
|
+
os.path.join(project_root, ".prizmkit", "test")
|
|
675
|
+
)
|
|
676
|
+
resolved_report = os.path.realpath(report_path)
|
|
677
|
+
|
|
678
|
+
try:
|
|
679
|
+
if os.path.commonpath([test_root, resolved_report]) != test_root:
|
|
680
|
+
return False
|
|
681
|
+
except ValueError:
|
|
682
|
+
return False
|
|
683
|
+
|
|
684
|
+
report_artifacts = [a for a in artifacts if a.endswith("test-report.md")]
|
|
685
|
+
for artifact in report_artifacts:
|
|
686
|
+
artifact_path = os.path.join(project_root, artifact)
|
|
687
|
+
if glob.has_magic(artifact_path):
|
|
688
|
+
candidates = glob.glob(artifact_path)
|
|
689
|
+
else:
|
|
690
|
+
candidates = [artifact_path]
|
|
691
|
+
for candidate in candidates:
|
|
692
|
+
if os.path.realpath(candidate) == resolved_report:
|
|
693
|
+
return True
|
|
694
|
+
return False
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _extract_scope_field(report_text, field_name):
|
|
698
|
+
"""Extract a '- Field: value' entry from the report Scope section."""
|
|
699
|
+
in_scope = False
|
|
700
|
+
prefix = "- {}:".format(field_name)
|
|
701
|
+
for raw_line in report_text.splitlines():
|
|
702
|
+
line = raw_line.strip()
|
|
703
|
+
if line == "## Scope":
|
|
704
|
+
in_scope = True
|
|
705
|
+
continue
|
|
706
|
+
if in_scope and line.startswith("## "):
|
|
707
|
+
break
|
|
708
|
+
if in_scope and line.startswith(prefix):
|
|
709
|
+
return line[len(prefix):].strip()
|
|
710
|
+
return ""
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _extract_report_verdict(report_text):
|
|
714
|
+
"""Extract the first non-empty line after the report Verdict heading."""
|
|
715
|
+
in_verdict = False
|
|
716
|
+
for raw_line in report_text.splitlines():
|
|
717
|
+
line = raw_line.strip()
|
|
718
|
+
if line == "## Verdict":
|
|
719
|
+
in_verdict = True
|
|
720
|
+
continue
|
|
721
|
+
if in_verdict:
|
|
722
|
+
if line.startswith("## "):
|
|
723
|
+
break
|
|
724
|
+
if line:
|
|
725
|
+
return line
|
|
726
|
+
return ""
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _extract_report_section(report_text, heading):
|
|
730
|
+
"""Extract text under a level-2 markdown heading."""
|
|
731
|
+
in_section = False
|
|
732
|
+
lines = []
|
|
733
|
+
for raw_line in report_text.splitlines():
|
|
734
|
+
line = raw_line.strip()
|
|
735
|
+
if line == heading:
|
|
736
|
+
in_section = True
|
|
737
|
+
continue
|
|
738
|
+
if in_section and line.startswith("## "):
|
|
739
|
+
break
|
|
740
|
+
if in_section:
|
|
741
|
+
lines.append(raw_line)
|
|
742
|
+
return "\n".join(lines)
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _discover_openapi_file(project_root):
|
|
746
|
+
"""Return the first common OpenAPI/Swagger file under project_root."""
|
|
747
|
+
candidates = [
|
|
748
|
+
"openapi.yaml", "openapi.yml", "swagger.yaml", "swagger.yml",
|
|
749
|
+
"docs/openapi.yaml", "docs/openapi.yml",
|
|
750
|
+
"docs/swagger.yaml", "docs/swagger.yml",
|
|
751
|
+
"api/openapi.yaml", "api/openapi.yml",
|
|
752
|
+
"api/swagger.yaml", "api/swagger.yml",
|
|
753
|
+
]
|
|
754
|
+
for candidate in candidates:
|
|
755
|
+
full_path = os.path.join(project_root, candidate)
|
|
756
|
+
if os.path.isfile(full_path):
|
|
757
|
+
return full_path
|
|
758
|
+
return ""
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def _normalize_api_path(path):
|
|
762
|
+
"""Normalize API path tokens for exact comparison."""
|
|
763
|
+
normalized = path.strip().strip("'\"`")
|
|
764
|
+
if len(normalized) > 1:
|
|
765
|
+
normalized = normalized.rstrip("/")
|
|
766
|
+
return normalized
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def _extract_openapi_operations(openapi_text):
|
|
770
|
+
"""Extract OpenAPI method+path operations without requiring PyYAML."""
|
|
771
|
+
methods = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE"}
|
|
772
|
+
operations = []
|
|
773
|
+
in_paths = False
|
|
774
|
+
paths_indent = 0
|
|
775
|
+
current_path = ""
|
|
776
|
+
current_path_indent = 0
|
|
777
|
+
path_key_re = re.compile(
|
|
778
|
+
r"^\s*(?:(['\"])(/.*?)\1|(/[^:\s]+))\s*:\s*(?:#.*)?$"
|
|
779
|
+
)
|
|
780
|
+
method_re = re.compile(r"^\s*([A-Za-z]+)\s*:\s*(?:#.*)?$")
|
|
781
|
+
|
|
782
|
+
for raw_line in openapi_text.splitlines():
|
|
783
|
+
stripped = raw_line.strip()
|
|
784
|
+
if not stripped or stripped.startswith("#"):
|
|
785
|
+
continue
|
|
786
|
+
|
|
787
|
+
indent = len(raw_line) - len(raw_line.lstrip())
|
|
788
|
+
if not in_paths:
|
|
789
|
+
if re.match(r"^\s*paths\s*:\s*(?:#.*)?$", raw_line):
|
|
790
|
+
in_paths = True
|
|
791
|
+
paths_indent = indent
|
|
792
|
+
continue
|
|
793
|
+
|
|
794
|
+
if indent <= paths_indent:
|
|
795
|
+
break
|
|
796
|
+
|
|
797
|
+
path_match = path_key_re.match(raw_line)
|
|
798
|
+
if path_match:
|
|
799
|
+
current_path = path_match.group(2) or path_match.group(3) or ""
|
|
800
|
+
current_path_indent = indent
|
|
801
|
+
continue
|
|
802
|
+
|
|
803
|
+
method_match = method_re.match(raw_line)
|
|
804
|
+
if current_path and method_match and indent > current_path_indent:
|
|
805
|
+
method = method_match.group(1).upper()
|
|
806
|
+
if method in methods:
|
|
807
|
+
operations.append((method, _normalize_api_path(current_path)))
|
|
808
|
+
|
|
809
|
+
return operations
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def _extract_matrix_operations(rows):
|
|
813
|
+
"""Extract exact method+path tokens from Boundary Matrix rows."""
|
|
814
|
+
operations = set()
|
|
815
|
+
path_only = set()
|
|
816
|
+
token_re = re.compile(
|
|
817
|
+
r"(?:(\b(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE)\b)\s+)?(/[^\s,`|)]+)",
|
|
818
|
+
flags=re.IGNORECASE,
|
|
819
|
+
)
|
|
820
|
+
for row in rows:
|
|
821
|
+
interface = row.get("Interface", "")
|
|
822
|
+
for match in token_re.finditer(interface):
|
|
823
|
+
method = match.group(1).upper() if match.group(1) else ""
|
|
824
|
+
path = _normalize_api_path(match.group(2))
|
|
825
|
+
if not path:
|
|
826
|
+
continue
|
|
827
|
+
if method:
|
|
828
|
+
operations.add((method, path))
|
|
829
|
+
else:
|
|
830
|
+
path_only.add(path)
|
|
831
|
+
return operations, path_only
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _boundary_report_sections_valid(project_root, report_text):
|
|
835
|
+
"""Return whether required boundary sections do not report failure."""
|
|
836
|
+
required_columns = [
|
|
837
|
+
"Module",
|
|
838
|
+
"Interface",
|
|
839
|
+
"Service Type",
|
|
840
|
+
"Happy Path",
|
|
841
|
+
"Request Validation",
|
|
842
|
+
"Auth / Permission / Ownership",
|
|
843
|
+
"Domain Invariants",
|
|
844
|
+
"Collection / Pagination",
|
|
845
|
+
"Date / Time",
|
|
846
|
+
"State Transitions",
|
|
847
|
+
"Dependency Failure",
|
|
848
|
+
"Response Contract",
|
|
849
|
+
"N/A Reasons",
|
|
850
|
+
"Final Status",
|
|
851
|
+
]
|
|
852
|
+
category_columns = [
|
|
853
|
+
"Happy Path",
|
|
854
|
+
"Request Validation",
|
|
855
|
+
"Auth / Permission / Ownership",
|
|
856
|
+
"Domain Invariants",
|
|
857
|
+
"Collection / Pagination",
|
|
858
|
+
"Date / Time",
|
|
859
|
+
"State Transitions",
|
|
860
|
+
"Dependency Failure",
|
|
861
|
+
"Response Contract",
|
|
862
|
+
]
|
|
863
|
+
allowed_final_statuses = {
|
|
864
|
+
"boundary-covered",
|
|
865
|
+
"generated-needs-review",
|
|
866
|
+
"skipped-with-reason",
|
|
867
|
+
}
|
|
868
|
+
incomplete_markers = {
|
|
869
|
+
"boundary-missing",
|
|
870
|
+
"happy-path-only",
|
|
871
|
+
"unresolved",
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
matrix_section = _extract_report_section(report_text, "## Boundary Matrix")
|
|
875
|
+
if not matrix_section:
|
|
876
|
+
return False
|
|
877
|
+
matrix_lines = [line.strip() for line in matrix_section.splitlines()
|
|
878
|
+
if line.strip().startswith("|")]
|
|
879
|
+
headers = []
|
|
880
|
+
rows = []
|
|
881
|
+
for line in matrix_lines:
|
|
882
|
+
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
|
|
883
|
+
if not headers:
|
|
884
|
+
if "Interface" in cells and "Final Status" in cells:
|
|
885
|
+
headers = cells
|
|
886
|
+
continue
|
|
887
|
+
if all(re.fullmatch(r":?-{3,}:?", cell) for cell in cells):
|
|
888
|
+
continue
|
|
889
|
+
if len(cells) < len(headers):
|
|
890
|
+
cells.extend([""] * (len(headers) - len(cells)))
|
|
891
|
+
row = {header: cells[i] if i < len(cells) else ""
|
|
892
|
+
for i, header in enumerate(headers)}
|
|
893
|
+
if row.get("Interface", "").strip(".` ") in {"", "..."}:
|
|
894
|
+
continue
|
|
895
|
+
rows.append(row)
|
|
896
|
+
|
|
897
|
+
if not headers or not rows:
|
|
898
|
+
return False
|
|
899
|
+
if any(column not in headers for column in required_columns):
|
|
900
|
+
return False
|
|
901
|
+
|
|
902
|
+
for row in rows:
|
|
903
|
+
final_status = row.get("Final Status", "").strip().lower()
|
|
904
|
+
if final_status not in allowed_final_statuses:
|
|
905
|
+
return False
|
|
906
|
+
for column in category_columns:
|
|
907
|
+
if not row.get(column, "").strip():
|
|
908
|
+
return False
|
|
909
|
+
for value in row.values():
|
|
910
|
+
normalized = value.strip().lower()
|
|
911
|
+
if any(marker in normalized for marker in incomplete_markers):
|
|
912
|
+
return False
|
|
913
|
+
|
|
914
|
+
openapi_file = _discover_openapi_file(project_root)
|
|
915
|
+
if openapi_file:
|
|
916
|
+
operations = _extract_openapi_operations(_read_text(openapi_file))
|
|
917
|
+
matrix_operations, _matrix_path_only = _extract_matrix_operations(rows)
|
|
918
|
+
for method, path in operations:
|
|
919
|
+
if (method, path) not in matrix_operations:
|
|
920
|
+
return False
|
|
921
|
+
|
|
922
|
+
gate_section = _extract_report_section(report_text,
|
|
923
|
+
"## Boundary Completion Gate")
|
|
924
|
+
if not gate_section:
|
|
925
|
+
return False
|
|
926
|
+
completion_gate_passed = False
|
|
927
|
+
boundary_missing_seen = False
|
|
928
|
+
happy_path_only_seen = False
|
|
929
|
+
for raw_line in gate_section.splitlines():
|
|
930
|
+
line = raw_line.strip()
|
|
931
|
+
match = re.match(r"-?\s*(Boundary-missing|Happy-path-only):\s*(\d+)\s*$",
|
|
932
|
+
line, re.IGNORECASE)
|
|
933
|
+
if match:
|
|
934
|
+
if match.group(1).lower() == "boundary-missing":
|
|
935
|
+
boundary_missing_seen = True
|
|
936
|
+
if match.group(1).lower() == "happy-path-only":
|
|
937
|
+
happy_path_only_seen = True
|
|
938
|
+
if int(match.group(2)) > 0:
|
|
939
|
+
return False
|
|
940
|
+
if re.match(r"-?\s*Completion gate passed:\s*yes\s*$",
|
|
941
|
+
line, re.IGNORECASE):
|
|
942
|
+
completion_gate_passed = True
|
|
943
|
+
if re.match(r"-?\s*Completion gate passed:\s*no\b",
|
|
944
|
+
line, re.IGNORECASE):
|
|
945
|
+
return False
|
|
946
|
+
if not (completion_gate_passed and boundary_missing_seen and
|
|
947
|
+
happy_path_only_seen):
|
|
948
|
+
return False
|
|
949
|
+
|
|
950
|
+
validation_section = _extract_report_section(report_text,
|
|
951
|
+
"## Boundary Validation")
|
|
952
|
+
if not validation_section:
|
|
953
|
+
return False
|
|
954
|
+
validator_result = ""
|
|
955
|
+
for raw_line in validation_section.splitlines():
|
|
956
|
+
line = raw_line.strip()
|
|
957
|
+
match = re.match(r"-?\s*Validator result:\s*(\S+)",
|
|
958
|
+
line, re.IGNORECASE)
|
|
959
|
+
if match:
|
|
960
|
+
validator_result = match.group(1).strip().lower()
|
|
961
|
+
break
|
|
962
|
+
|
|
963
|
+
return validator_result == "passed"
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def _extract_scope_list(report_text, list_name):
|
|
967
|
+
"""Extract a nested file list from the report Scope section."""
|
|
968
|
+
in_scope = False
|
|
969
|
+
in_list = False
|
|
970
|
+
list_marker = "- {}:".format(list_name)
|
|
971
|
+
values = []
|
|
972
|
+
for raw_line in report_text.splitlines():
|
|
973
|
+
line = raw_line.strip()
|
|
974
|
+
if line == "## Scope":
|
|
975
|
+
in_scope = True
|
|
976
|
+
continue
|
|
977
|
+
if in_scope and line.startswith("## "):
|
|
978
|
+
break
|
|
979
|
+
if not in_scope:
|
|
980
|
+
continue
|
|
981
|
+
if line == list_marker:
|
|
982
|
+
in_list = True
|
|
983
|
+
continue
|
|
984
|
+
if in_list:
|
|
985
|
+
if raw_line.startswith(" -") or raw_line.startswith("\t-"):
|
|
986
|
+
value = line[2:].strip().strip("`")
|
|
987
|
+
if value and value not in {"{...}", "..."}:
|
|
988
|
+
values.append(value)
|
|
989
|
+
continue
|
|
990
|
+
if line.startswith("- "):
|
|
991
|
+
break
|
|
992
|
+
return values
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def _scoped_report_fresh(project_root, pointer_artifact, report_path, report_text):
|
|
996
|
+
"""Return whether the report is newer than the gate marker and in-scope files."""
|
|
997
|
+
marker_path = os.path.join(
|
|
998
|
+
project_root,
|
|
999
|
+
os.path.dirname(pointer_artifact),
|
|
1000
|
+
".prizmkit-test-started",
|
|
1001
|
+
)
|
|
1002
|
+
if not os.path.exists(marker_path):
|
|
1003
|
+
return False
|
|
1004
|
+
|
|
1005
|
+
try:
|
|
1006
|
+
report_mtime = os.path.getmtime(report_path)
|
|
1007
|
+
marker_mtime = os.path.getmtime(marker_path)
|
|
1008
|
+
except OSError:
|
|
1009
|
+
return False
|
|
1010
|
+
|
|
1011
|
+
if report_mtime < marker_mtime:
|
|
1012
|
+
return False
|
|
1013
|
+
|
|
1014
|
+
project_root_real = os.path.realpath(project_root)
|
|
1015
|
+
source_files = _extract_scope_list(report_text, "In-Scope Source Files")
|
|
1016
|
+
test_files = _extract_scope_list(report_text, "In-Scope Test Files")
|
|
1017
|
+
if not source_files or not test_files:
|
|
1018
|
+
return False
|
|
1019
|
+
|
|
1020
|
+
ignored = {"none", "n/a", "not applicable", "(none)", "{...}", "..."}
|
|
1021
|
+
for listed_file in source_files + test_files:
|
|
1022
|
+
normalized = listed_file.strip().strip("`")
|
|
1023
|
+
if not normalized or normalized.lower() in ignored:
|
|
1024
|
+
return False
|
|
1025
|
+
candidate = (normalized if os.path.isabs(normalized)
|
|
1026
|
+
else os.path.join(project_root, normalized))
|
|
1027
|
+
candidate_real = os.path.realpath(candidate)
|
|
1028
|
+
try:
|
|
1029
|
+
if os.path.commonpath([project_root_real, candidate_real]) != project_root_real:
|
|
1030
|
+
return False
|
|
1031
|
+
except ValueError:
|
|
1032
|
+
return False
|
|
1033
|
+
if not os.path.isfile(candidate_real):
|
|
1034
|
+
return False
|
|
1035
|
+
if os.path.getmtime(candidate_real) > report_mtime:
|
|
1036
|
+
return False
|
|
1037
|
+
|
|
1038
|
+
return True
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def _validate_scoped_test_report(project_root, artifacts):
|
|
1042
|
+
"""Validate the feature prizmkit-test pointer and scoped PASS report."""
|
|
1043
|
+
pointer_artifacts = [a for a in artifacts
|
|
1044
|
+
if a.endswith("/test-report-path.txt")]
|
|
1045
|
+
if not pointer_artifacts:
|
|
1046
|
+
return False
|
|
1047
|
+
|
|
1048
|
+
pointer_artifact = pointer_artifacts[0]
|
|
1049
|
+
pointer_path = os.path.join(project_root, pointer_artifact)
|
|
1050
|
+
pointed_report = _read_text(pointer_path).strip().splitlines()
|
|
1051
|
+
if not pointed_report:
|
|
1052
|
+
return False
|
|
1053
|
+
|
|
1054
|
+
report_path = pointed_report[0].strip()
|
|
1055
|
+
if not os.path.isabs(report_path):
|
|
1056
|
+
report_path = os.path.join(project_root, report_path)
|
|
1057
|
+
if not os.path.exists(report_path):
|
|
1058
|
+
return False
|
|
1059
|
+
if not _report_path_matches_artifacts(project_root, report_path, artifacts):
|
|
1060
|
+
return False
|
|
1061
|
+
|
|
1062
|
+
report_text = _read_text(report_path)
|
|
1063
|
+
mode = _extract_scope_field(report_text, "Mode")
|
|
1064
|
+
artifact_dir = _extract_scope_field(report_text, "Artifact Dir")
|
|
1065
|
+
verdict = _extract_report_verdict(report_text)
|
|
1066
|
+
|
|
1067
|
+
expected_artifact_dir = _normalize_artifact_dir(
|
|
1068
|
+
os.path.dirname(pointer_artifact)
|
|
1069
|
+
)
|
|
1070
|
+
actual_artifact_dir = _normalize_artifact_dir(artifact_dir)
|
|
1071
|
+
|
|
1072
|
+
return (mode == "this-change" and
|
|
1073
|
+
actual_artifact_dir == expected_artifact_dir and
|
|
1074
|
+
verdict == "PASS" and
|
|
1075
|
+
_boundary_report_sections_valid(project_root, report_text) and
|
|
1076
|
+
_scoped_report_fresh(project_root, pointer_artifact,
|
|
1077
|
+
report_path, report_text))
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def _completed_step_artifacts_valid(project_root, skill_key, artifacts):
|
|
1081
|
+
"""Return whether a completed checkpoint step can be preserved."""
|
|
1082
|
+
if not all(_artifact_exists(project_root, a) for a in artifacts):
|
|
1083
|
+
return False
|
|
1084
|
+
if skill_key == "prizmkit-test":
|
|
1085
|
+
return _validate_scoped_test_report(project_root, artifacts)
|
|
1086
|
+
return True
|
|
1087
|
+
|
|
1088
|
+
|
|
643
1089
|
def generate_checkpoint_definition(sections, pipeline_mode, workflow_type,
|
|
644
1090
|
item_id, item_slug, session_id,
|
|
645
1091
|
init_done=False):
|
|
@@ -727,17 +1173,24 @@ def merge_checkpoint_state(existing, fresh, project_root):
|
|
|
727
1173
|
"""
|
|
728
1174
|
existing_status = {}
|
|
729
1175
|
existing_artifacts = {}
|
|
1176
|
+
fresh_artifacts = {step["skill"]: step.get("required_artifacts", [])
|
|
1177
|
+
for step in fresh.get("steps", [])}
|
|
730
1178
|
for step in existing.get("steps", []):
|
|
731
1179
|
existing_status[step["skill"]] = step["status"]
|
|
732
1180
|
existing_artifacts[step["skill"]] = step.get("required_artifacts", [])
|
|
733
1181
|
|
|
734
|
-
# Determine which completed steps have valid artifacts
|
|
1182
|
+
# Determine which completed steps have valid artifacts. Prefer the freshly
|
|
1183
|
+
# generated artifact contract so newly added gates invalidate older
|
|
1184
|
+
# checkpoint entries that only satisfied a weaker contract.
|
|
735
1185
|
valid_completed = set()
|
|
736
1186
|
for skill_key, status in existing_status.items():
|
|
737
1187
|
if status == "completed":
|
|
738
|
-
artifacts =
|
|
739
|
-
|
|
740
|
-
|
|
1188
|
+
artifacts = fresh_artifacts.get(
|
|
1189
|
+
skill_key, existing_artifacts.get(skill_key, [])
|
|
1190
|
+
)
|
|
1191
|
+
if _completed_step_artifacts_valid(project_root,
|
|
1192
|
+
skill_key,
|
|
1193
|
+
artifacts):
|
|
741
1194
|
valid_completed.add(skill_key)
|
|
742
1195
|
else:
|
|
743
1196
|
LOGGER.warning(
|
|
@@ -1045,6 +1498,11 @@ def assemble_sections(pipeline_mode, sections_dir, init_done, is_resume,
|
|
|
1045
1498
|
load_section(sections_dir,
|
|
1046
1499
|
"test-failure-recovery-agent.md")))
|
|
1047
1500
|
|
|
1501
|
+
# --- Scoped Feature Test Gate ---
|
|
1502
|
+
sections.append(("phase-prizmkit-test",
|
|
1503
|
+
load_section(sections_dir,
|
|
1504
|
+
"phase-prizmkit-test.md")))
|
|
1505
|
+
|
|
1048
1506
|
# Verification Gates are included in Task Contract. Keep AC in one place so
|
|
1049
1507
|
# background context and implementation prompts cannot redefine scope.
|
|
1050
1508
|
# --- Review (only for agent tiers) ---
|
|
@@ -30,7 +30,7 @@ Infer what needs to be done from the feature context above and follow the standa
|
|
|
30
30
|
|
|
31
31
|
3. **Implement**: Run `/prizmkit-implement` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to execute the plan using TDD (write tests first, then implement).
|
|
32
32
|
|
|
33
|
-
4. **Test**: Run
|
|
33
|
+
4. **Test**: Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to generate and verify tests only for this feature's changed scope. Do not use bugfix/refactor artifact directories for this gate.
|
|
34
34
|
|
|
35
35
|
5. **Review**: Run `/prizmkit-code-review` with `artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` to review and auto-fix changes against the spec (internal review-fix loop, max 3 rounds).
|
|
36
36
|
|
|
@@ -163,6 +163,20 @@ When tests fail, use convergence recovery — keep fixing while progress is bein
|
|
|
163
163
|
**Key rule**: If failures decrease (even by 1), plateau counter resets. Do NOT block commit for unresolved failures — document and defer to next session.
|
|
164
164
|
|
|
165
165
|
|
|
166
|
+
### Scoped Feature Test Gate — PrizmKit Test
|
|
167
|
+
|
|
168
|
+
Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after implementation and before review/commit.
|
|
169
|
+
|
|
170
|
+
Before invoking the skill, create `.prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started` so stale reports are ignored.
|
|
171
|
+
|
|
172
|
+
Gate requirements:
|
|
173
|
+
- Generate or update tests only for this feature's changed files, changed public interfaces, and acceptance criteria.
|
|
174
|
+
- Do NOT use `.prizmkit/bugfix/` or `.prizmkit/refactor/` artifact directories.
|
|
175
|
+
- Record unrelated historical gaps under `Out of Scope Gaps`; do not generate tests for them and do not fail this feature because of them.
|
|
176
|
+
- Accept only a current-run `.prizmkit/test/*/test-report.md` whose `Scope` mode is `this-change`, whose `Artifact Dir` matches `.prizmkit/specs/{{FEATURE_SLUG}}/`, whose `Verdict` is `PASS`, whose `Boundary Completion Gate` reports `Completion gate passed: yes` with zero `Boundary-missing` and zero `Happy-path-only`, and whose `Boundary Validation` result is `passed`.
|
|
177
|
+
- Only after a valid `PASS`, write the report path to `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt` and append a 3-5 bullet `## PrizmKit Test Gate` summary to `context-snapshot.md`.
|
|
178
|
+
- If the report verdict is `NEEDS_FIXES`, fix in-scope implementation/test issues and rerun `/prizmkit-test`. If the report is `BLOCKED`, missing, stale, or for the wrong scope/artifact dir, write `failure-log.md` and stop for recovery.
|
|
179
|
+
|
|
166
180
|
{{IF_BROWSER_INTERACTION}}
|
|
167
181
|
### Phase 3.5: Browser Verification — MANDATORY
|
|
168
182
|
|
|
@@ -240,6 +240,20 @@ grep -q "## Implementation Log" .prizmkit/specs/{{FEATURE_SLUG}}/context-snapsho
|
|
|
240
240
|
```
|
|
241
241
|
If GATE:MISSING — send message to Dev (re-spawn if needed): "Write the '## Implementation Log' section to context-snapshot.md before I can proceed to review. Include: files changed/created, key decisions, deviations from plan, notable discoveries."
|
|
242
242
|
|
|
243
|
+
### Scoped Feature Test Gate — PrizmKit Test
|
|
244
|
+
|
|
245
|
+
Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after implementation and before review/commit.
|
|
246
|
+
|
|
247
|
+
Before invoking the skill, create `.prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started` so stale reports are ignored.
|
|
248
|
+
|
|
249
|
+
Gate requirements:
|
|
250
|
+
- Generate or update tests only for this feature's changed files, changed public interfaces, and acceptance criteria.
|
|
251
|
+
- Do NOT use `.prizmkit/bugfix/` or `.prizmkit/refactor/` artifact directories.
|
|
252
|
+
- Record unrelated historical gaps under `Out of Scope Gaps`; do not generate tests for them and do not fail this feature because of them.
|
|
253
|
+
- Accept only a current-run `.prizmkit/test/*/test-report.md` whose `Scope` mode is `this-change`, whose `Artifact Dir` matches `.prizmkit/specs/{{FEATURE_SLUG}}/`, whose `Verdict` is `PASS`, whose `Boundary Completion Gate` reports `Completion gate passed: yes` with zero `Boundary-missing` and zero `Happy-path-only`, and whose `Boundary Validation` result is `passed`.
|
|
254
|
+
- Only after a valid `PASS`, write the report path to `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt` and append a 3-5 bullet `## PrizmKit Test Gate` summary to `context-snapshot.md`.
|
|
255
|
+
- If the report verdict is `NEEDS_FIXES`, fix in-scope implementation/test issues and rerun `/prizmkit-test`. If the report is `BLOCKED`, missing, stale, or for the wrong scope/artifact dir, write `failure-log.md` and stop for recovery.
|
|
256
|
+
|
|
243
257
|
### Phase 5: Review + Test — Reviewer Subagent
|
|
244
258
|
|
|
245
259
|
Spawn Reviewer subagent (Agent tool, subagent_type="prizm-dev-team-reviewer", run_in_background=false).
|
|
@@ -250,9 +264,10 @@ Prompt:
|
|
|
250
264
|
> "Read {{REVIEWER_SUBAGENT_PATH}}. For feature {{FEATURE_ID}} (slug: {{FEATURE_SLUG}}):
|
|
251
265
|
> 1. Read `.prizmkit/specs/{{FEATURE_SLUG}}/spec.md` for goals and acceptance criteria
|
|
252
266
|
> 2. Read `.prizmkit/specs/{{FEATURE_SLUG}}/plan.md` for architecture decisions and completed tasks
|
|
253
|
-
> 3.
|
|
254
|
-
> 4. Run
|
|
255
|
-
> 5.
|
|
267
|
+
> 3. Read `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt`, then read the referenced `/prizmkit-test` report. Review generated/updated tests, `In-Scope Failures`, `Baseline Failures`, `Out of Scope Gaps`, boundary coverage if present, and the report verdict.
|
|
268
|
+
> 4. Run /prizmkit-code-review with artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/. The skill runs an internal review-fix loop (Reviewer → filter → Dev fix, max 3 rounds) and writes review-report.md.
|
|
269
|
+
> 5. Run the full test suite using `{{TEST_CMD}}`. When running: `({{TEST_CMD}}) 2>&1 | tee /tmp/review-test-out.txt | tail -20`, then grep the file for details — do NOT re-run the suite multiple times.
|
|
270
|
+
> 6. review-report.md will be written to .prizmkit/specs/{{FEATURE_SLUG}}/ by prizmkit-code-review.
|
|
256
271
|
> Report: verdict (PASS/NEEDS_FIXES), number of rounds, findings fixed/rejected."
|
|
257
272
|
|
|
258
273
|
Wait for Reviewer to return.
|
|
@@ -301,6 +301,20 @@ Wait for Dev to return. **If Dev times out before all tasks are `[x]`**:
|
|
|
301
301
|
|
|
302
302
|
All tasks `[x]`, tests pass.
|
|
303
303
|
|
|
304
|
+
### Scoped Feature Test Gate — PrizmKit Test
|
|
305
|
+
|
|
306
|
+
Run `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/` after implementation and before review/commit.
|
|
307
|
+
|
|
308
|
+
Before invoking the skill, create `.prizmkit/specs/{{FEATURE_SLUG}}/.prizmkit-test-started` so stale reports are ignored.
|
|
309
|
+
|
|
310
|
+
Gate requirements:
|
|
311
|
+
- Generate or update tests only for this feature's changed files, changed public interfaces, and acceptance criteria.
|
|
312
|
+
- Do NOT use `.prizmkit/bugfix/` or `.prizmkit/refactor/` artifact directories.
|
|
313
|
+
- Record unrelated historical gaps under `Out of Scope Gaps`; do not generate tests for them and do not fail this feature because of them.
|
|
314
|
+
- Accept only a current-run `.prizmkit/test/*/test-report.md` whose `Scope` mode is `this-change`, whose `Artifact Dir` matches `.prizmkit/specs/{{FEATURE_SLUG}}/`, whose `Verdict` is `PASS`, whose `Boundary Completion Gate` reports `Completion gate passed: yes` with zero `Boundary-missing` and zero `Happy-path-only`, and whose `Boundary Validation` result is `passed`.
|
|
315
|
+
- Only after a valid `PASS`, write the report path to `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt` and append a 3-5 bullet `## PrizmKit Test Gate` summary to `context-snapshot.md`.
|
|
316
|
+
- If the report verdict is `NEEDS_FIXES`, fix in-scope implementation/test issues and rerun `/prizmkit-test`. If the report is `BLOCKED`, missing, stale, or for the wrong scope/artifact dir, write `failure-log.md` and stop for recovery.
|
|
317
|
+
|
|
304
318
|
### Phase 5: Review + Test — Reviewer Agent
|
|
305
319
|
|
|
306
320
|
Spawn Reviewer agent (Agent tool, subagent_type="prizm-dev-team-reviewer", run_in_background=false).
|
|
@@ -311,9 +325,10 @@ Prompt:
|
|
|
311
325
|
> "Read {{REVIEWER_SUBAGENT_PATH}}. For feature {{FEATURE_ID}} (slug: {{FEATURE_SLUG}}):
|
|
312
326
|
> 1. Read `.prizmkit/specs/{{FEATURE_SLUG}}/spec.md` for goals and acceptance criteria
|
|
313
327
|
> 2. Read `.prizmkit/specs/{{FEATURE_SLUG}}/plan.md` for architecture decisions and completed tasks
|
|
314
|
-
> 3.
|
|
315
|
-
> 4. Run
|
|
316
|
-
> 5.
|
|
328
|
+
> 3. Read `.prizmkit/specs/{{FEATURE_SLUG}}/test-report-path.txt`, then read the referenced `/prizmkit-test` report. Review generated/updated tests, `In-Scope Failures`, `Baseline Failures`, `Out of Scope Gaps`, boundary coverage if present, and the report verdict.
|
|
329
|
+
> 4. Run /prizmkit-code-review with artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/. The skill runs an internal review-fix loop (Reviewer → filter → Dev fix, max 3 rounds) and writes review-report.md.
|
|
330
|
+
> 5. Run the full test suite using `{{TEST_CMD}}`. When running tests: `({{TEST_CMD}}) 2>&1 | tee /tmp/review-test-out.txt | tail -20`, then grep `/tmp/review-test-out.txt` for details — do NOT re-run the suite multiple times.
|
|
331
|
+
> 6. review-report.md will be written to .prizmkit/specs/{{FEATURE_SLUG}}/ by prizmkit-code-review.
|
|
317
332
|
> Report: verdict (PASS/NEEDS_FIXES), number of rounds, findings fixed/rejected."
|
|
318
333
|
|
|
319
334
|
Wait for Reviewer to return.
|
|
@@ -70,13 +70,13 @@ python3 $PIPELINE_DIR/scripts/update-checkpoint.py \
|
|
|
70
70
|
--status completed
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
### TERMINAL STOP —
|
|
73
|
+
### TERMINAL STOP — Commit Completed
|
|
74
74
|
|
|
75
75
|
After all of the following are true, the session is **TERMINAL**:
|
|
76
76
|
|
|
77
77
|
- `/prizmkit-committer` has completed
|
|
78
78
|
- `git status --short` is clean (except ignored/local-only pipeline artifacts)
|
|
79
79
|
- `.prizmkit/specs/{{FEATURE_SLUG}}/completion-summary.json` is written
|
|
80
|
-
- the
|
|
80
|
+
- the `prizmkit-committer` checkpoint is marked `completed`
|
|
81
81
|
|
|
82
82
|
At that point, stop immediately. Do **not** process delayed teammate/reviewer findings, do **not** edit files, do **not** amend the commit, and do **not** start any new tool calls. Any findings that arrive after S09 is completed are follow-up work for a later session, not part of this feature commit.
|
|
@@ -63,13 +63,13 @@ python3 $PIPELINE_DIR/scripts/update-checkpoint.py \
|
|
|
63
63
|
--status completed
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
### TERMINAL STOP —
|
|
66
|
+
### TERMINAL STOP — Commit Completed
|
|
67
67
|
|
|
68
68
|
After all of the following are true, the session is **TERMINAL**:
|
|
69
69
|
|
|
70
70
|
- `/prizmkit-committer` has completed
|
|
71
71
|
- `git status --short` is clean (except ignored/local-only pipeline artifacts)
|
|
72
72
|
- `.prizmkit/specs/{{FEATURE_SLUG}}/completion-summary.json` is written
|
|
73
|
-
- the
|
|
73
|
+
- the `prizmkit-committer` checkpoint is marked `completed`
|
|
74
74
|
|
|
75
75
|
At that point, stop immediately. Do **not** process delayed teammate/reviewer findings, do **not** edit files, do **not** amend the commit, and do **not** start any new tool calls. Any findings that arrive after S09 is completed are follow-up work for a later session, not part of this feature commit.
|