Coverage for src/keel/artifacts.py: 100%

85 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-16 18:07 +0000

1"""Canonical Markdown renderers for ship artifacts. 

2 

3These helpers keep public GitHub artifacts deterministic and consumer-neutral. 

4Adapters should post the rendered bodies verbatim instead of hand-writing PR 

5descriptions, review verdicts, jury verdicts, or extension result summaries. 

6""" 

7 

8from __future__ import annotations 

9 

10from typing import Any 

11 

12from . import evidence 

13 

14SCHEMA_VERSION = "keel.artifacts.v1" 

15EXTENSION_RESULT_MARKER = "<!-- keel.extension-result.v1 -->" 

16ISSUE_UPDATE_MARKER = "<!-- keel.issue-update.v1 -->" 

17STEP_HANDOFF_MARKER = "<!-- keel.step-handoff.v1 -->" 

18RUN_CONTROL_HALT_MARKER = "<!-- keel.run-control-halt.v1 -->" 

19 

20 

21def contract_as_dict() -> dict[str, Any]: 

22 """Return the canonical artifact renderer contract for ship-like flows.""" 

23 return { 

24 "schema_version": SCHEMA_VERSION, 

25 "consumer_neutral": True, 

26 "deterministic": True, 

27 "renderers": { 

28 "pr_body": "keel.artifacts.render_pr_body", 

29 "issue_update": "keel.artifacts.render_issue_update", 

30 "review_verdict": "keel.artifacts.render_review_verdict", 

31 "jury_verdict": "keel.artifacts.render_jury_verdict", 

32 "extension_result": "keel.artifacts.render_extension_result", 

33 "step_handoff": "keel.artifacts.render_step_handoff", 

34 "run_control_halt": "keel.artifacts.render_run_control_halt", 

35 }, 

36 "markers": { 

37 "review_verdict": evidence.REVIEW_VERDICT_MARKER, 

38 "jury_verdict": evidence.JURY_VERDICT_MARKER, 

39 "issue_update": ISSUE_UPDATE_MARKER, 

40 "extension_result": EXTENSION_RESULT_MARKER, 

41 "step_handoff": STEP_HANDOFF_MARKER, 

42 "run_control_halt": RUN_CONTROL_HALT_MARKER, 

43 }, 

44 "adapter_rule": "post rendered markdown verbatim when available", 

45 } 

46 

47 

48def render_pr_body( 

49 *, 

50 issue_number: int | None = None, 

51 issue_intake: dict[str, Any] | None = None, 

52 changed_files: list[str] | tuple[str, ...] = (), 

53 testing: list[str] | tuple[str, ...] = (), 

54 docs_impact: str | None = None, 

55) -> str: 

56 """Render the canonical PR body used by ship implementers.""" 

57 intake = issue_intake if isinstance(issue_intake, dict) else {} 

58 lines = [ 

59 "## Summary", 

60 f"- { _value(intake.get('deliverable'), 'Implement the requested change.') }", 

61 "", 

62 "## Context / Root Cause", 

63 _value(intake.get("objective"), "See the linked issue for context."), 

64 "", 

65 "## Changes Made", 

66 ] 

67 files = [file for file in changed_files if isinstance(file, str)] 

68 if files: 

69 lines.extend(f"- Updated `{file}`." for file in files) 

70 else: 

71 lines.append("- No changed files recorded yet.") 

72 lines.extend(["", "## Testing"]) 

73 tests = [item for item in testing if isinstance(item, str) and item.strip()] 

74 lines.extend(f"- {item.strip()}" for item in tests) if tests else lines.append( 

75 "- Not run yet; update this section before marking the PR ready." 

76 ) 

77 lines.extend([ 

78 "", 

79 "## Docs Impact", 

80 _value(docs_impact, "Docs Impact: none — no operator-facing behavior changed."), 

81 "", 

82 _closing_reference(issue_number), 

83 ]) 

84 return "\n".join(lines).rstrip() + "\n" 

85 

86 

87def render_issue_update( 

88 *, 

89 issue_number: int | None = None, 

90 pull_request: int | None = None, 

91 status: str = "in-progress", 

92 summary: str | None = None, 

93 next_step: str | None = None, 

94) -> str: 

95 """Render a stable issue progress/update comment.""" 

96 lines = [ 

97 ISSUE_UPDATE_MARKER, 

98 "", 

99 "## Ship update", 

100 "", 

101 f"- **Issue:** {_issue(issue_number)}", 

102 f"- **Pull request:** {_pr(pull_request)}", 

103 f"- **Status:** {_value(status, 'in-progress')}", 

104 f"- **Summary:** {_value(summary, 'No summary recorded.')}", 

105 f"- **Next step:** {_value(next_step, 'Continue the ship workflow.')}", 

106 ] 

107 return "\n".join(lines) + "\n" 

108 

109 

110def render_review_verdict( 

111 *, 

112 reviewer: str, 

113 head_sha: str | None, 

114 verdict: str = "LGTM", 

115 scope: str | None = None, 

116 findings: list[dict[str, Any]] | tuple[dict[str, Any], ...] = (), 

117 testing: str | None = None, 

118 vendor: str | None = None, 

119 model: str | None = None, 

120) -> str: 

121 """Render a head-bound reviewer verdict comment accepted by evidence verification. 

122 

123 When ``vendor`` (and optionally ``model``) is supplied, structured 

124 ``vendor:`` / ``model:`` provenance lines are emitted so evidence 

125 verification can enforce vendor distinctness across required verdicts. The 

126 fields use the same vendor/model conventions as ``keel.provenance`` and are 

127 omitted entirely when not supplied, so the default rendering is unchanged. 

128 """ 

129 lines = [ 

130 evidence.REVIEW_VERDICT_MARKER, 

131 f"reviewer: {_slug(reviewer)}", 

132 f"head: {_value(head_sha, '<head-sha>')}", 

133 ] 

134 if isinstance(vendor, str) and vendor.strip(): 

135 lines.append(f"vendor: {_slug(vendor)}") 

136 if isinstance(model, str) and model.strip(): 

137 lines.append(f"model: {_slug(model)}") 

138 lines.extend([ 

139 "", 

140 f"Verdict: {_value(verdict, 'LGTM')}", 

141 "", 

142 f"Scope reviewed: {_value(scope, 'Full changed-file diff and relevant contracts.')}", 

143 "", 

144 "Findings:", 

145 ]) 

146 lines.extend(_finding_lines(findings)) 

147 lines.extend(["", f"Testing noted: {_value(testing, 'See PR Testing section.')}"]) 

148 return "\n".join(lines) + "\n" 

149 

150 

151def render_jury_verdict( 

152 *, 

153 head_sha: str | None, 

154 participants: list[str] | tuple[str, ...] = (), 

155 verdict: str = "LGTM", 

156 findings_summary: list[str] | tuple[str, ...] = (), 

157 remaining_risks: str | None = None, 

158) -> str: 

159 """Render a head-bound jury verdict comment accepted by evidence verification.""" 

160 people = [person.strip() for person in participants if isinstance(person, str) 

161 and person.strip()] 

162 lines = [ 

163 evidence.JURY_VERDICT_MARKER, 

164 f"head: {_value(head_sha, '<head-sha>')}", 

165 "", 

166 f"AI Jury verdict: {_value(verdict, 'LGTM')}.", 

167 "", 

168 f"Participants: {', '.join(people) if people else 'not recorded'}.", 

169 "", 

170 "Findings summary:", 

171 ] 

172 summaries = [item.strip() for item in findings_summary if isinstance(item, str) 

173 and item.strip()] 

174 lines.extend(f"- {item}" for item in summaries) if summaries else lines.append("- none") 

175 lines.extend(["", f"Remaining risks: {_value(remaining_risks, 'none identified')}."]) 

176 return "\n".join(lines) + "\n" 

177 

178 

179def render_extension_result( 

180 *, 

181 slot: str, 

182 extension_id: str, 

183 status: str, 

184 mode: str, 

185 summary: str | None = None, 

186 artifacts: list[str] | tuple[str, ...] = (), 

187 follow_ups: list[str] | tuple[str, ...] = (), 

188) -> str: 

189 """Render a canonical extension result block/comment.""" 

190 lines = [ 

191 EXTENSION_RESULT_MARKER, 

192 "", 

193 "## Extension result", 

194 "", 

195 f"- **Slot:** `{_value(slot, 'unknown')}`", 

196 f"- **Extension:** `{_value(extension_id, 'unknown')}`", 

197 f"- **Status:** {_value(status, 'not-recorded')}", 

198 f"- **Mode:** {_value(mode, 'advisory')}", 

199 f"- **Summary:** {_value(summary, 'No summary recorded.')}", 

200 "- **Artifacts:**", 

201 ] 

202 artifact_lines = _string_bullets(artifacts) 

203 lines.extend(artifact_lines if artifact_lines else [" - none"]) 

204 lines.append("- **Follow-ups:**") 

205 follow_up_lines = _string_bullets(follow_ups) 

206 lines.extend(follow_up_lines if follow_up_lines else [" - none"]) 

207 return "\n".join(lines) + "\n" 

208 

209 

210def render_step_handoff( 

211 *, 

212 step_id: str, 

213 step_name: str | None = None, 

214 status: str = "complete", 

215 summary: str | None = None, 

216 next_step: str | None = None, 

217 evidence_ids: list[str] | tuple[str, ...] = (), 

218) -> str: 

219 """Render the canonical structured handoff between backbone steps.""" 

220 lines = [ 

221 STEP_HANDOFF_MARKER, 

222 "", 

223 "## Step handoff", 

224 "", 

225 f"- **Step:** `{_value(step_id, 'unknown')}`", 

226 f"- **Name:** {_value(step_name, 'not recorded')}", 

227 f"- **Status:** {_value(status, 'complete')}", 

228 f"- **Summary:** {_value(summary, 'No summary recorded.')}", 

229 f"- **Next step:** {_value(next_step, 'Continue the backbone plan.')}", 

230 "- **Evidence:**", 

231 ] 

232 evidence_lines = _string_bullets(evidence_ids) 

233 lines.extend(evidence_lines if evidence_lines else [" - none"]) 

234 return "\n".join(lines) + "\n" 

235 

236 

237def render_run_control_halt( 

238 *, 

239 control: str, 

240 reason: str, 

241 scope: str | None = None, 

242 observed: int | str | None = None, 

243 limit: int | str | None = None, 

244 action: str | None = None, 

245) -> str: 

246 """Render a stable hard-halt reason emitted by run controls.""" 

247 lines = [ 

248 RUN_CONTROL_HALT_MARKER, 

249 "", 

250 "## Run control halt", 

251 "", 

252 f"- **Control:** `{_value(control, 'unknown')}`", 

253 f"- **Reason:** {_value(reason, 'No reason recorded.')}", 

254 f"- **Scope:** {_value(scope, 'run')}", 

255 f"- **Observed:** {_value(observed, 'not recorded')}", 

256 f"- **Limit:** {_value(limit, 'not recorded')}", 

257 f"- **Action:** {_value(action, 'halt')}", 

258 ] 

259 return "\n".join(lines) + "\n" 

260 

261 

262def _finding_lines(findings: list[dict[str, Any]] | tuple[dict[str, Any], ...]) -> list[str]: 

263 if not findings: 

264 return ["- none"] 

265 lines: list[str] = [] 

266 for finding in findings: 

267 severity = _value(finding.get("severity") if isinstance(finding, dict) else None, "nit") 

268 message = _value(finding.get("message") if isinstance(finding, dict) else None, "") 

269 if message: 

270 lines.append(f"- {severity}: {message}") 

271 return lines or ["- none"] 

272 

273 

274def _string_bullets(values: list[str] | tuple[str, ...]) -> list[str]: 

275 return [f" - {value.strip()}" for value in values if isinstance(value, str) 

276 and value.strip()] 

277 

278 

279def _closing_reference(issue_number: int | None) -> str: 

280 return f"Closes #{issue_number}" if isinstance(issue_number, int) else "Refs #<issue-number>" 

281 

282 

283def _issue(issue_number: int | None) -> str: 

284 return f"#{issue_number}" if isinstance(issue_number, int) else "not recorded" 

285 

286 

287def _pr(pull_request: int | None) -> str: 

288 return f"#{pull_request}" if isinstance(pull_request, int) else "not opened" 

289 

290 

291def slug(value: str) -> str: 

292 """Stable, deterministic slug for reviewer/run-id sub-keys (public alias).""" 

293 clean = "".join(ch.lower() if ch.isalnum() else "-" for ch in value.strip()) 

294 return "-".join(part for part in clean.split("-") if part) or "reviewer" 

295 

296 

297def _slug(value: str) -> str: 

298 return slug(value) 

299 

300 

301def _value(value: Any, fallback: str) -> str: 

302 if isinstance(value, str) and value.strip(): 

303 return value.strip() 

304 return fallback