diff --git a/.claude/skills/quality-management-gh/findings-report.md.j2 b/.claude/skills/quality-management-gh/findings-report.md.j2 index 1c0972ee..67b26d10 100644 --- a/.claude/skills/quality-management-gh/findings-report.md.j2 +++ b/.claude/skills/quality-management-gh/findings-report.md.j2 @@ -1,6 +1,6 @@ --- name: findings-report -version: 1.0.0 +version: 1.0.1 description: Multi-pass QA findings report template for FAIL and IN-FLIGHT updates. format: markdown required_variables: @@ -36,22 +36,22 @@ Verdict: **{{ verdict }}** ## Machine Status (JSON) ```json { - "sprint": "{{ sprint_id }}", - "task": "{{ task_id }}", - "branch": "{{ branch }}", - "commit": "{{ commit }}", - "pr": "{{ pr_number }}", - "verdict": "{{ verdict }}", + "sprint": {{ sprint_id | tojson }}, + "task": {{ task_id | tojson }}, + "branch": {{ branch | tojson }}, + "commit": {{ commit | tojson }}, + "pr": {{ pr_number | tojson }}, + "verdict": {{ verdict | tojson }}, "findings": { - "blocking": "{{ findings_blocking }}", - "important": "{{ findings_important }}", - "minor": "{{ findings_minor }}" + "blocking": {{ findings_blocking | tojson }}, + "important": {{ findings_important | tojson }}, + "minor": {{ findings_minor | tojson }} }, - "blocking_ids": "{{ blocking_ids }}", - "merge_readiness": "{{ merge_readiness }}", - "merge_reason": "{{ merge_reason }}", - "next_action": "{{ next_action }}", - "owner": "{{ action_owner }}" + "blocking_ids": {{ blocking_ids | tojson }}, + "merge_readiness": {{ merge_readiness | tojson }}, + "merge_reason": {{ merge_reason | tojson }}, + "next_action": {{ next_action | tojson }}, + "owner": {{ action_owner | tojson }} } ``` diff --git a/.claude/skills/quality-management-gh/quality-report.md.j2 b/.claude/skills/quality-management-gh/quality-report.md.j2 index fff04dd2..d39a9875 100644 --- a/.claude/skills/quality-management-gh/quality-report.md.j2 +++ b/.claude/skills/quality-management-gh/quality-report.md.j2 @@ -1,6 +1,6 @@ --- name: quality-report -version: 1.0.0 +version: 1.0.1 description: Final QA closeout report template for PASS verdicts. format: markdown required_variables: @@ -34,23 +34,23 @@ Final Verdict: **{{ verdict }}** ## Machine Status (JSON) ```json { - "sprint": "{{ sprint_id }}", - "task": "{{ task_id }}", - "branch": "{{ branch }}", - "commit": "{{ commit }}", - "pr": "{{ pr_number }}", - "verdict": "{{ verdict }}", + "sprint": {{ sprint_id | tojson }}, + "task": {{ task_id | tojson }}, + "branch": {{ branch | tojson }}, + "commit": {{ commit | tojson }}, + "pr": {{ pr_number | tojson }}, + "verdict": {{ verdict | tojson }}, "findings": { - "blocking": "{{ findings_blocking }}", - "important": "{{ findings_important }}", - "minor": "{{ findings_minor }}" + "blocking": {{ findings_blocking | tojson }}, + "important": {{ findings_important | tojson }}, + "minor": {{ findings_minor | tojson }} }, "blocking_ids": "", - "merge_readiness": "{{ merge_readiness }}", - "merge_reason": "{{ merge_reason }}", + "merge_readiness": {{ merge_readiness | tojson }}, + "merge_reason": {{ merge_reason | tojson }}, "next_action": "none", "action_owner": "none", - "recommendation": "{{ recommendation }}" + "recommendation": {{ recommendation | tojson }} } ``` diff --git a/bindings/sc-lint-py/python/sc_lint/tests/test_orchestration_contracts.py b/bindings/sc-lint-py/python/sc_lint/tests/test_orchestration_contracts.py index d89f2396..9ef93da7 100644 --- a/bindings/sc-lint-py/python/sc_lint/tests/test_orchestration_contracts.py +++ b/bindings/sc-lint-py/python/sc_lint/tests/test_orchestration_contracts.py @@ -285,3 +285,50 @@ def test_carry_forward_findings_render_as_json_arrays(self) -> None: vars_path.write_text(json.dumps(variables), encoding="utf-8") payload = json.loads(self.compose(template, vars_path)) self.assertEqual(payload["carry_forward_findings"], expected) + + def test_quality_report_machine_json_round_trips_strings(self) -> None: + adversarial = 'quoted "value" \\ path\nline &' + common = { + "generated_at": "2026-09-20T20:00:00Z", + "qa_pass": 1, + "sprint_id": adversarial, + "task_id": adversarial, + "branch": adversarial, + "commit": adversarial, + "pr_number": 123, + "verdict": adversarial, + "findings_blocking": 1, + "findings_important": 2, + "findings_minor": 3, + "merge_readiness": adversarial, + "merge_reason": adversarial, + } + cases = { + "findings-report.md.j2": { + **common, + "blocking_ids": adversarial, + "blocking_findings_md": "- finding", + "detailed_findings_md": "- detail", + "next_action": adversarial, + "action_owner": adversarial, + }, + "quality-report.md.j2": { + **common, + "validated_scope_md": "- scope", + "residual_risks_md": "- none", + "recommendation": adversarial, + }, + } + for template_name, variables in cases.items(): + template = REPO / ".claude/skills/quality-management-gh" / template_name + with self.subTest(template=template_name), tempfile.TemporaryDirectory() as directory: + vars_path = Path(directory) / "vars.json" + vars_path.write_text(json.dumps(variables), encoding="utf-8") + rendered = self.compose(template, vars_path) + match = re.search(r"```json\s*\n(.*?)\n```", rendered, re.S) + self.assertIsNotNone(match) + payload = json.loads(match.group(1)) + for field in ("sprint", "task", "branch", "commit", "verdict", "merge_readiness", "merge_reason"): + self.assertEqual(payload[field], adversarial) + self.assertEqual(payload["pr"], 123) + self.assertEqual(payload["findings"], {"blocking": 1, "important": 2, "minor": 3})