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.
- syncade/__init__.py +3 -0
- syncade/__main__.py +6 -0
- syncade/adapters/__init__.py +0 -0
- syncade/adapters/anthropic.py +457 -0
- syncade/adapters/base.py +221 -0
- syncade/adapters/fake.py +73 -0
- syncade/adapters/fake_common.py +29 -0
- syncade/adapters/fake_producer_audit_draft.py +460 -0
- syncade/adapters/fake_reviewer_synth.py +310 -0
- syncade/adapters/openai.py +484 -0
- syncade/adapters/openai_parsing.py +119 -0
- syncade/adapters/producer.py +221 -0
- syncade/adapters/producer_anthropic.py +300 -0
- syncade/adapters/producer_openai.py +226 -0
- syncade/adapters/registry.py +81 -0
- syncade/auth_check.py +554 -0
- syncade/auth_preflight.py +342 -0
- syncade/base_resolution.py +214 -0
- syncade/billing.py +141 -0
- syncade/checks_config.py +113 -0
- syncade/cli/__init__.py +546 -0
- syncade/cli/auth_gate.py +59 -0
- syncade/cli/config_keys.py +135 -0
- syncade/cli/config_list.py +82 -0
- syncade/cli/config_menu_rows.py +166 -0
- syncade/cli/config_mode.py +609 -0
- syncade/cli/config_overrides.py +122 -0
- syncade/cli/config_tui.py +476 -0
- syncade/cli/doctor_mode.py +72 -0
- syncade/cli/gc_mode.py +109 -0
- syncade/cli/install_skill.py +514 -0
- syncade/cli/metrics_mode.py +363 -0
- syncade/cli/modes.py +573 -0
- syncade/cli/parser.py +450 -0
- syncade/cli/parser_types.py +137 -0
- syncade/cli/paths.py +38 -0
- syncade/cli/preflight_paths.py +90 -0
- syncade/cli/resolve.py +116 -0
- syncade/cli/resume_mode.py +324 -0
- syncade/cli/toml_writer.py +410 -0
- syncade/cli/validate.py +421 -0
- syncade/config.py +478 -0
- syncade/config_auth.py +310 -0
- syncade/config_cold.py +209 -0
- syncade/config_gc.py +55 -0
- syncade/config_loader.py +182 -0
- syncade/config_loop.py +282 -0
- syncade/config_producer.py +222 -0
- syncade/config_retry.py +49 -0
- syncade/config_types.py +59 -0
- syncade/diff_filter.py +437 -0
- syncade/dispatcher.py +571 -0
- syncade/doctor.py +425 -0
- syncade/doctor_env.py +218 -0
- syncade/doctor_preview.py +524 -0
- syncade/doctor_types.py +28 -0
- syncade/exit_codes.py +82 -0
- syncade/findings.py +242 -0
- syncade/findings_json.py +456 -0
- syncade/gc.py +211 -0
- syncade/gc_execute.py +372 -0
- syncade/gc_protection.py +129 -0
- syncade/gc_types.py +50 -0
- syncade/gc_worktrees.py +200 -0
- syncade/git_object_id.py +12 -0
- syncade/git_preconditions.py +389 -0
- syncade/logging.py +289 -0
- syncade/metrics/__init__.py +32 -0
- syncade/metrics/aggregate.py +550 -0
- syncade/metrics/schema.py +221 -0
- syncade/orchestrator/__init__.py +61 -0
- syncade/orchestrator/_runs_dir.py +24 -0
- syncade/orchestrator/branch_advance.py +165 -0
- syncade/orchestrator/branch_guard.py +98 -0
- syncade/orchestrator/budget.py +107 -0
- syncade/orchestrator/escalation_coverage.py +81 -0
- syncade/orchestrator/loop.py +611 -0
- syncade/orchestrator/loop_dispatch_check.py +112 -0
- syncade/orchestrator/loop_finalize.py +404 -0
- syncade/orchestrator/loop_preflight.py +131 -0
- syncade/orchestrator/loop_resume.py +91 -0
- syncade/orchestrator/loop_rmtree.py +70 -0
- syncade/orchestrator/loop_round_step.py +599 -0
- syncade/orchestrator/prior_round.py +336 -0
- syncade/orchestrator/producer_phase.py +169 -0
- syncade/orchestrator/results.py +306 -0
- syncade/orchestrator/resume.py +96 -0
- syncade/orchestrator/resume_load.py +483 -0
- syncade/orchestrator/resume_plan.py +554 -0
- syncade/orchestrator/resume_target.py +215 -0
- syncade/orchestrator/resume_types.py +182 -0
- syncade/orchestrator/reviewer_template_failure.py +99 -0
- syncade/orchestrator/round.py +573 -0
- syncade/orchestrator/round_checks.py +91 -0
- syncade/orchestrator/round_no_changes.py +369 -0
- syncade/orchestrator/round_predispatch.py +212 -0
- syncade/orchestrator/verdict.py +279 -0
- syncade/persistence/__init__.py +189 -0
- syncade/persistence/_atomic.py +33 -0
- syncade/persistence/_clusters.py +70 -0
- syncade/persistence/_findings_verdict.py +201 -0
- syncade/persistence/_markdown.py +286 -0
- syncade/persistence/_validation.py +37 -0
- syncade/persistence/checks.py +249 -0
- syncade/persistence/decision_needed.py +289 -0
- syncade/persistence/findings_md.py +389 -0
- syncade/persistence/handoff.py +389 -0
- syncade/persistence/handoff_classify.py +196 -0
- syncade/persistence/last_reviewed.py +67 -0
- syncade/persistence/loop_manifest.py +165 -0
- syncade/persistence/loop_summary.py +352 -0
- syncade/persistence/loop_summary_text.py +428 -0
- syncade/persistence/producer.py +250 -0
- syncade/persistence/reviewer.py +198 -0
- syncade/persistence/round_manifest.py +238 -0
- syncade/persistence/run_init.py +153 -0
- syncade/persistence/run_summary.py +585 -0
- syncade/persistence/run_summary_next_steps.py +443 -0
- syncade/persistence/synth.py +242 -0
- syncade/persistence/test_run.py +152 -0
- syncade/presets.py +36 -0
- syncade/pricing_config.py +72 -0
- syncade/process.py +600 -0
- syncade/producer.py +189 -0
- syncade/producer_attempt.py +463 -0
- syncade/producer_escalation.py +146 -0
- syncade/producer_git.py +199 -0
- syncade/producer_result.py +205 -0
- syncade/prompts.py +448 -0
- syncade/prompts_loader.py +238 -0
- syncade/retry.py +159 -0
- syncade/run_inputs.py +40 -0
- syncade/run_status.py +198 -0
- syncade/selfcheck.py +471 -0
- syncade/skills/claude/README.md +221 -0
- syncade/skills/claude/SKILL.md +625 -0
- syncade/skills/codex/README.md +116 -0
- syncade/skills/codex/SKILL.md +574 -0
- syncade/snapshot.py +598 -0
- syncade/spec_audit.py +437 -0
- syncade/spec_audit_schema.py +190 -0
- syncade/spec_draft.py +423 -0
- syncade/spec_source.py +135 -0
- syncade/synthesis.py +428 -0
- syncade/synthesis_clusters.py +203 -0
- syncade/synthesis_repair.py +230 -0
- syncade/synthesis_schema.py +65 -0
- syncade/synthesizer/__init__.py +38 -0
- syncade/synthesizer/constants.py +33 -0
- syncade/synthesizer/driver.py +531 -0
- syncade/synthesizer/rendering.py +63 -0
- syncade/synthesizer/result.py +73 -0
- syncade/synthesizer/validation.py +421 -0
- syncade/synthesizer/workspace.py +208 -0
- syncade/templates/presets/balanced.toml +13 -0
- syncade/templates/presets/cheap.toml +12 -0
- syncade/templates/presets/thorough.toml +9 -0
- syncade/templates/producer.md +231 -0
- syncade/templates/reviewer.md +279 -0
- syncade/templates/reviewer_adversarial.md +164 -0
- syncade/templates/reviewer_codex.md +165 -0
- syncade/templates/spec_audit.md +168 -0
- syncade/templates/spec_draft.md +62 -0
- syncade/templates/synthesizer.md +204 -0
- syncade/test_runner.py +476 -0
- syncade/test_runner_classify.py +98 -0
- syncade/transcript.py +150 -0
- syncade/usage.py +407 -0
- syncade/worktree.py +497 -0
- syncade/worktree_env.py +133 -0
- syncade/worktree_paths.py +139 -0
- syncade-0.6.2.dist-info/METADATA +314 -0
- syncade-0.6.2.dist-info/RECORD +177 -0
- syncade-0.6.2.dist-info/WHEEL +5 -0
- syncade-0.6.2.dist-info/entry_points.txt +2 -0
- syncade-0.6.2.dist-info/licenses/LICENSE +202 -0
- 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
|
+
)
|