syncade 0.6.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. syncade/__init__.py +3 -0
  2. syncade/__main__.py +6 -0
  3. syncade/adapters/__init__.py +0 -0
  4. syncade/adapters/anthropic.py +457 -0
  5. syncade/adapters/base.py +221 -0
  6. syncade/adapters/fake.py +73 -0
  7. syncade/adapters/fake_common.py +29 -0
  8. syncade/adapters/fake_producer_audit_draft.py +460 -0
  9. syncade/adapters/fake_reviewer_synth.py +310 -0
  10. syncade/adapters/openai.py +484 -0
  11. syncade/adapters/openai_parsing.py +119 -0
  12. syncade/adapters/producer.py +221 -0
  13. syncade/adapters/producer_anthropic.py +300 -0
  14. syncade/adapters/producer_openai.py +226 -0
  15. syncade/adapters/registry.py +81 -0
  16. syncade/auth_check.py +554 -0
  17. syncade/auth_preflight.py +342 -0
  18. syncade/base_resolution.py +214 -0
  19. syncade/billing.py +141 -0
  20. syncade/checks_config.py +113 -0
  21. syncade/cli/__init__.py +546 -0
  22. syncade/cli/auth_gate.py +59 -0
  23. syncade/cli/config_keys.py +135 -0
  24. syncade/cli/config_list.py +82 -0
  25. syncade/cli/config_menu_rows.py +166 -0
  26. syncade/cli/config_mode.py +609 -0
  27. syncade/cli/config_overrides.py +122 -0
  28. syncade/cli/config_tui.py +476 -0
  29. syncade/cli/doctor_mode.py +72 -0
  30. syncade/cli/gc_mode.py +109 -0
  31. syncade/cli/install_skill.py +514 -0
  32. syncade/cli/metrics_mode.py +363 -0
  33. syncade/cli/modes.py +573 -0
  34. syncade/cli/parser.py +450 -0
  35. syncade/cli/parser_types.py +137 -0
  36. syncade/cli/paths.py +38 -0
  37. syncade/cli/preflight_paths.py +90 -0
  38. syncade/cli/resolve.py +116 -0
  39. syncade/cli/resume_mode.py +324 -0
  40. syncade/cli/toml_writer.py +410 -0
  41. syncade/cli/validate.py +421 -0
  42. syncade/config.py +478 -0
  43. syncade/config_auth.py +310 -0
  44. syncade/config_cold.py +209 -0
  45. syncade/config_gc.py +55 -0
  46. syncade/config_loader.py +182 -0
  47. syncade/config_loop.py +282 -0
  48. syncade/config_producer.py +222 -0
  49. syncade/config_retry.py +49 -0
  50. syncade/config_types.py +59 -0
  51. syncade/diff_filter.py +437 -0
  52. syncade/dispatcher.py +571 -0
  53. syncade/doctor.py +425 -0
  54. syncade/doctor_env.py +218 -0
  55. syncade/doctor_preview.py +524 -0
  56. syncade/doctor_types.py +28 -0
  57. syncade/exit_codes.py +82 -0
  58. syncade/findings.py +242 -0
  59. syncade/findings_json.py +456 -0
  60. syncade/gc.py +211 -0
  61. syncade/gc_execute.py +372 -0
  62. syncade/gc_protection.py +129 -0
  63. syncade/gc_types.py +50 -0
  64. syncade/gc_worktrees.py +200 -0
  65. syncade/git_object_id.py +12 -0
  66. syncade/git_preconditions.py +389 -0
  67. syncade/logging.py +289 -0
  68. syncade/metrics/__init__.py +32 -0
  69. syncade/metrics/aggregate.py +550 -0
  70. syncade/metrics/schema.py +221 -0
  71. syncade/orchestrator/__init__.py +61 -0
  72. syncade/orchestrator/_runs_dir.py +24 -0
  73. syncade/orchestrator/branch_advance.py +165 -0
  74. syncade/orchestrator/branch_guard.py +98 -0
  75. syncade/orchestrator/budget.py +107 -0
  76. syncade/orchestrator/escalation_coverage.py +81 -0
  77. syncade/orchestrator/loop.py +611 -0
  78. syncade/orchestrator/loop_dispatch_check.py +112 -0
  79. syncade/orchestrator/loop_finalize.py +404 -0
  80. syncade/orchestrator/loop_preflight.py +131 -0
  81. syncade/orchestrator/loop_resume.py +91 -0
  82. syncade/orchestrator/loop_rmtree.py +70 -0
  83. syncade/orchestrator/loop_round_step.py +599 -0
  84. syncade/orchestrator/prior_round.py +336 -0
  85. syncade/orchestrator/producer_phase.py +169 -0
  86. syncade/orchestrator/results.py +306 -0
  87. syncade/orchestrator/resume.py +96 -0
  88. syncade/orchestrator/resume_load.py +483 -0
  89. syncade/orchestrator/resume_plan.py +554 -0
  90. syncade/orchestrator/resume_target.py +215 -0
  91. syncade/orchestrator/resume_types.py +182 -0
  92. syncade/orchestrator/reviewer_template_failure.py +99 -0
  93. syncade/orchestrator/round.py +573 -0
  94. syncade/orchestrator/round_checks.py +91 -0
  95. syncade/orchestrator/round_no_changes.py +369 -0
  96. syncade/orchestrator/round_predispatch.py +212 -0
  97. syncade/orchestrator/verdict.py +279 -0
  98. syncade/persistence/__init__.py +189 -0
  99. syncade/persistence/_atomic.py +33 -0
  100. syncade/persistence/_clusters.py +70 -0
  101. syncade/persistence/_findings_verdict.py +201 -0
  102. syncade/persistence/_markdown.py +286 -0
  103. syncade/persistence/_validation.py +37 -0
  104. syncade/persistence/checks.py +249 -0
  105. syncade/persistence/decision_needed.py +289 -0
  106. syncade/persistence/findings_md.py +389 -0
  107. syncade/persistence/handoff.py +389 -0
  108. syncade/persistence/handoff_classify.py +196 -0
  109. syncade/persistence/last_reviewed.py +67 -0
  110. syncade/persistence/loop_manifest.py +165 -0
  111. syncade/persistence/loop_summary.py +352 -0
  112. syncade/persistence/loop_summary_text.py +428 -0
  113. syncade/persistence/producer.py +250 -0
  114. syncade/persistence/reviewer.py +198 -0
  115. syncade/persistence/round_manifest.py +238 -0
  116. syncade/persistence/run_init.py +153 -0
  117. syncade/persistence/run_summary.py +585 -0
  118. syncade/persistence/run_summary_next_steps.py +443 -0
  119. syncade/persistence/synth.py +242 -0
  120. syncade/persistence/test_run.py +152 -0
  121. syncade/presets.py +36 -0
  122. syncade/pricing_config.py +72 -0
  123. syncade/process.py +600 -0
  124. syncade/producer.py +189 -0
  125. syncade/producer_attempt.py +463 -0
  126. syncade/producer_escalation.py +146 -0
  127. syncade/producer_git.py +199 -0
  128. syncade/producer_result.py +205 -0
  129. syncade/prompts.py +448 -0
  130. syncade/prompts_loader.py +238 -0
  131. syncade/retry.py +159 -0
  132. syncade/run_inputs.py +40 -0
  133. syncade/run_status.py +198 -0
  134. syncade/selfcheck.py +471 -0
  135. syncade/skills/claude/README.md +221 -0
  136. syncade/skills/claude/SKILL.md +625 -0
  137. syncade/skills/codex/README.md +116 -0
  138. syncade/skills/codex/SKILL.md +574 -0
  139. syncade/snapshot.py +598 -0
  140. syncade/spec_audit.py +437 -0
  141. syncade/spec_audit_schema.py +190 -0
  142. syncade/spec_draft.py +423 -0
  143. syncade/spec_source.py +135 -0
  144. syncade/synthesis.py +428 -0
  145. syncade/synthesis_clusters.py +203 -0
  146. syncade/synthesis_repair.py +230 -0
  147. syncade/synthesis_schema.py +65 -0
  148. syncade/synthesizer/__init__.py +38 -0
  149. syncade/synthesizer/constants.py +33 -0
  150. syncade/synthesizer/driver.py +531 -0
  151. syncade/synthesizer/rendering.py +63 -0
  152. syncade/synthesizer/result.py +73 -0
  153. syncade/synthesizer/validation.py +421 -0
  154. syncade/synthesizer/workspace.py +208 -0
  155. syncade/templates/presets/balanced.toml +13 -0
  156. syncade/templates/presets/cheap.toml +12 -0
  157. syncade/templates/presets/thorough.toml +9 -0
  158. syncade/templates/producer.md +231 -0
  159. syncade/templates/reviewer.md +279 -0
  160. syncade/templates/reviewer_adversarial.md +164 -0
  161. syncade/templates/reviewer_codex.md +165 -0
  162. syncade/templates/spec_audit.md +168 -0
  163. syncade/templates/spec_draft.md +62 -0
  164. syncade/templates/synthesizer.md +204 -0
  165. syncade/test_runner.py +476 -0
  166. syncade/test_runner_classify.py +98 -0
  167. syncade/transcript.py +150 -0
  168. syncade/usage.py +407 -0
  169. syncade/worktree.py +497 -0
  170. syncade/worktree_env.py +133 -0
  171. syncade/worktree_paths.py +139 -0
  172. syncade-0.6.2.dist-info/METADATA +314 -0
  173. syncade-0.6.2.dist-info/RECORD +177 -0
  174. syncade-0.6.2.dist-info/WHEEL +5 -0
  175. syncade-0.6.2.dist-info/entry_points.txt +2 -0
  176. syncade-0.6.2.dist-info/licenses/LICENSE +202 -0
  177. syncade-0.6.2.dist-info/top_level.txt +1 -0
syncade/findings.py ADDED
@@ -0,0 +1,242 @@
1
+ """Pydantic v2 models for reviewer-emitted findings.
2
+
3
+ Schema mirrors PRD Appendix B exactly: a top-level ``ReviewerOutput`` with
4
+ a ``SHIP``/``NO-SHIP`` ``verdict`` and a list of ``Finding`` records
5
+ grouped by ``severity``. Any field not listed here is rejected
6
+ (``extra="forbid"``) so reviewer drift surfaces as a parse error instead
7
+ of being silently absorbed.
8
+
9
+ ``parse_reviewer_output`` is the main entry point. It accepts bare JSON,
10
+ markdown-fenced JSON, or JSON embedded in prose. Verdict-block selection and
11
+ its failure semantics live in :mod:`syncade.findings_json`, shared with the
12
+ synthesizer, spec-audit, and spec-draft parsers so they cannot drift; every
13
+ clause of the rule there is the scar of a reproduced false SHIP.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Literal
19
+
20
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
21
+
22
+ from syncade.findings_json import decode_and_validate, validate_dropping_forbidden_extras
23
+
24
+ Severity = Literal["blocker", "minor", "nit"]
25
+ """Per-finding severity classification, per PRD Appendix B."""
26
+
27
+ Verdict = Literal["SHIP", "NO-SHIP"]
28
+ """Top-level reviewer verdict, per PRD Appendix B."""
29
+
30
+
31
+ class ReviewerOutputError(Exception):
32
+ """Raised when reviewer stdout can't be parsed as a :class:`ReviewerOutput`.
33
+
34
+ The message names which block was selected as the verdict (the last
35
+ ``json`` fence, or the whole response), why it was rejected, and the
36
+ reviewer's ``.stdout`` artifact — so the CLI can surface a useful error via
37
+ exit code 70 (``REVIEWER_OUTPUT_UNPARSEABLE``) without further
38
+ introspection.
39
+ """
40
+
41
+
42
+ class Finding(BaseModel):
43
+ """A single reviewer-emitted finding, per PRD Appendix B.
44
+
45
+ ``file`` is optional: ``None`` means the finding is repo-wide
46
+ rather than tied to a specific file (e.g., a commit-message
47
+ issue, a hygiene problem about which files are committed,
48
+ repository-level configuration). Rejecting repo-wide findings on schema
49
+ would discard legitimate blocker-level concerns.
50
+
51
+ ``line`` is also optional: ``None`` means the finding is
52
+ file-level rather than line-specific.
53
+
54
+ ``evidence_cmd`` and ``evidence_output`` are optional —
55
+ reviewers should populate them when they ran a concrete repro
56
+ command, but bare assertions (e.g., a doc-level concern) don't
57
+ need them.
58
+ """
59
+
60
+ model_config = ConfigDict(extra="forbid")
61
+
62
+ severity: Severity
63
+ file: str | None = None
64
+ line: int | None = None
65
+ spec_clause: str
66
+ finding: str
67
+ evidence_cmd: str | None = None
68
+ evidence_output: str | None = None
69
+
70
+
71
+ class ReviewerOutput(BaseModel):
72
+ """A complete reviewer-run output: a verdict, a list of findings, and
73
+ narrative-surface fields that capture the reviewer's summary,
74
+ prioritization, coverage gaps, and dismissed concerns.
75
+
76
+ Reviewers MAY return ``verdict="SHIP"`` with a non-empty findings list
77
+ (e.g., minor / nit findings that don't block ship). The orchestrator
78
+ decides what to do; the schema doesn't enforce verdict↔findings
79
+ consistency.
80
+
81
+ ``summary``, ``priority_order``, ``coverage_gaps``, and
82
+ ``dismissed_concerns`` are required so the reviewer must consciously assert
83
+ each answer rather than silently omit it. ``coverage_gaps`` and
84
+ ``dismissed_concerns`` may be ``[]`` (explicit "nothing to flag").
85
+ ``priority_order`` is constrained to be a complete permutation of
86
+ ``range(len(findings))`` by :meth:`_validate_priority_order`.
87
+ """
88
+
89
+ model_config = ConfigDict(extra="forbid")
90
+
91
+ verdict: Verdict
92
+ findings: list[Finding] = Field(default_factory=list)
93
+
94
+ summary: str = Field(
95
+ ...,
96
+ min_length=1,
97
+ description=(
98
+ "Reviewer's headline narrative: what was verified, what stood out, "
99
+ "why this verdict. Must contain non-whitespace content even when "
100
+ "verdict=SHIP and findings is empty — a SHIP without verification "
101
+ "narrative is not useful to the operator or to the synthesizer. "
102
+ "This structured field is the verification summary. ``min_length=1`` "
103
+ "catches pure-empty strings; :meth:`_validate_summary_nonblank` "
104
+ "catches whitespace-only strings (``' '``, ``'\\n\\t '``, etc.)."
105
+ ),
106
+ )
107
+
108
+ priority_order: list[int] = Field(
109
+ ...,
110
+ description=(
111
+ "Indices into ``findings`` in priority order — most urgent first. "
112
+ "Must be a complete permutation of range(len(findings)): every "
113
+ "finding gets exactly one priority position. The orchestrator and "
114
+ "synthesizer use this to rank within-severity-tier; a list of 3 "
115
+ "blockers without explicit ordering is much less actionable than "
116
+ "the same 3 with the reviewer's judgment about which to fix first. "
117
+ "Empty list iff ``findings`` is empty."
118
+ ),
119
+ )
120
+
121
+ coverage_gaps: list[str] = Field(
122
+ ...,
123
+ description=(
124
+ "What the reviewer did NOT verify, and why. Surfaces honest "
125
+ "operational limits ('I couldn't reach the staging DB', 'I "
126
+ "didn't test mobile viewports', 'I trusted the producer's "
127
+ "claim about backend tests'). Empty list means 'I verified "
128
+ "everything the spec asked for' — forces the reviewer to "
129
+ "consciously assert that rather than silently omit."
130
+ ),
131
+ )
132
+
133
+ dismissed_concerns: list[str] = Field(
134
+ ...,
135
+ description=(
136
+ "Issues the reviewer noticed but ruled out as non-issues, with "
137
+ "rationale. Surfaces the rigor of the review — a NO-SHIP with "
138
+ "zero dismissed concerns is suspicious; a SHIP with several "
139
+ "dismissed concerns suggests the reviewer actively looked for "
140
+ "issues rather than pattern-matching against the spec."
141
+ ),
142
+ )
143
+
144
+ @field_validator("summary")
145
+ @classmethod
146
+ def _validate_summary_nonblank(cls, v: str) -> str:
147
+ """``summary`` must contain at least one non-whitespace character.
148
+
149
+ ``Field(min_length=1)`` rejects empty strings but accepts
150
+ ``' '`` / ``'\\n\\t '`` / other pure-whitespace values, which
151
+ carry no narrative information and defeat the field's whole
152
+ purpose (forcing the reviewer to assert what they actually
153
+ verified). Brief explicitly calls this out — whitespace-only
154
+ summary must fail validation.
155
+ """
156
+ if not v.strip():
157
+ raise ValueError(
158
+ "summary must contain non-whitespace content; got an "
159
+ "all-whitespace value which provides no verification "
160
+ "narrative to the operator or the synthesizer"
161
+ )
162
+ return v
163
+
164
+ @model_validator(mode="after")
165
+ def _validate_priority_order(self) -> ReviewerOutput:
166
+ """``priority_order`` must be a complete permutation of
167
+ ``range(len(findings))``.
168
+
169
+ Strict on purpose. A partial ordering is harder to consume
170
+ downstream and easier to get wrong silently. Forcing reviewers to
171
+ permute all findings means they consciously rank each one.
172
+
173
+ - ``len(priority_order)`` must equal ``len(findings)``
174
+ - ``sorted(priority_order)`` must equal ``list(range(len(findings)))``
175
+ - no duplicates, no out-of-range indices, no missing positions
176
+ - empty list iff findings is empty
177
+ """
178
+ expected = list(range(len(self.findings)))
179
+ if sorted(self.priority_order) != expected:
180
+ raise ValueError(
181
+ f"priority_order must be a complete permutation of "
182
+ f"range({len(self.findings)}); got {self.priority_order}"
183
+ )
184
+ return self
185
+
186
+
187
+ def get_findings_schema_string() -> str:
188
+ """Return the JSON schema string for :class:`ReviewerOutput`,
189
+ formatted for inclusion in a reviewer prompt.
190
+
191
+ Single source of truth — the orchestrator and any future
192
+ prompt-rendering caller pulls from here rather than hand-rolling
193
+ the schema. If :class:`Finding` or :class:`ReviewerOutput` evolves
194
+ shape, this function changes once and every caller picks up the
195
+ update.
196
+
197
+ Output is a JSON-ish skeleton with type annotations as comments,
198
+ NOT a strict JSON Schema document — it's prompt input, intended
199
+ for the model to read, not a validator. The shape mirrors
200
+ :class:`ReviewerOutput` and :class:`Finding`, which are the canonical
201
+ pydantic models. Inline ``//`` comments call out the
202
+ contracts the reviewer prompt relies on (which fields are
203
+ required, which can be ``[]``, what permutation rule
204
+ ``priority_order`` must satisfy).
205
+ """
206
+ return (
207
+ "{\n"
208
+ ' "verdict": "SHIP" | "NO-SHIP",\n'
209
+ ' "findings": [\n'
210
+ ' {"severity": "blocker"|"minor"|"nit", '
211
+ '"file": "path"|null, "line": int|null, '
212
+ '"spec_clause": "string", "finding": "string", '
213
+ '"evidence_cmd": "string"|null, '
214
+ '"evidence_output": "string"|null}\n'
215
+ " ],\n"
216
+ ' "summary": "string (required, non-empty narrative)",\n'
217
+ ' "priority_order": [int], // indices into findings, '
218
+ "complete permutation; [] when findings is []\n"
219
+ ' "coverage_gaps": [string], // required; [] if none\n'
220
+ ' "dismissed_concerns": [string] // required; [] if none\n'
221
+ "}"
222
+ )
223
+
224
+
225
+ def parse_reviewer_output(raw: str) -> ReviewerOutput:
226
+ """Parse a reviewer's raw stdout text into a :class:`ReviewerOutput`.
227
+
228
+ Selection, failure semantics, and the reasons for both live in
229
+ :mod:`syncade.findings_json`; this is the reviewer's binding of them.
230
+ """
231
+ return decode_and_validate(
232
+ raw,
233
+ # A verdict whose only defect is a forbidden extra key is repaired rather than
234
+ # discarded (PR-h-field-05). Narrow by construction: see the function's docstring.
235
+ validate=lambda payload: validate_dropping_forbidden_extras(
236
+ payload, ReviewerOutput.model_validate, label="reviewer"
237
+ ),
238
+ error=ReviewerOutputError,
239
+ label="reviewer",
240
+ model_name="ReviewerOutput",
241
+ artifact="the reviewer's .stdout in the round directory",
242
+ )