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
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""The synthesizer parser's retry-repair tier.
|
|
2
|
+
|
|
3
|
+
:func:`parse_synthesizer_output` validates a candidate STRICTLY first. Only if that
|
|
4
|
+
fails does it apply this module — a narrow, closed list of KNOWN, information-free
|
|
5
|
+
ways a model deviates from the synthesizer schema — and validate once more.
|
|
6
|
+
|
|
7
|
+
This is a repair LIST, not a tolerance. Every repair here must be provably lossless:
|
|
8
|
+
it may only discard data that carries no information and can never affect the
|
|
9
|
+
verdict. Any deviation NOT on the list still fails to exit 70, and that is the
|
|
10
|
+
point — a schema we quietly bend is a schema we no longer have.
|
|
11
|
+
|
|
12
|
+
The two entries, and where each came from:
|
|
13
|
+
|
|
14
|
+
- **Reviewer-only ``line`` / ``spec_clause``** copied into a consolidated finding or
|
|
15
|
+
a cluster evidence quote. Neither is in the synthesizer schema, both models are
|
|
16
|
+
``extra="forbid"``, so a stray copied coordinate turned a valid consolidation into
|
|
17
|
+
exit 70. (Seen live on Codex.)
|
|
18
|
+
- **EMPTY ``root_cause_clusters``** — no members, no evidence. A cluster GROUPS
|
|
19
|
+
findings; one that references nothing groups nothing, so discarding it is provably
|
|
20
|
+
lossless. (Seen live on Anthropic in PR-v2-23: handed a single finding — nothing to
|
|
21
|
+
cluster — ``claude-sonnet-4-6`` emitted an empty cluster where ``gpt-5.5`` emits
|
|
22
|
+
``[]``. The synth prompt had been tuned to one model, and only running a second
|
|
23
|
+
provider surfaced it.)
|
|
24
|
+
|
|
25
|
+
Note EMPTY, not "degenerate". A ONE-member cluster is NOT repaired: it named a
|
|
26
|
+
finding, so it makes a claim, and a wrong claim must be loud. That distinction is
|
|
27
|
+
the difference between a lossless repair and a silent one — see
|
|
28
|
+
:class:`_EmptyClusterProbe`.
|
|
29
|
+
|
|
30
|
+
Eligibility is decided by validating against a relaxed model, never by a checklist of
|
|
31
|
+
what might be wrong. Enumerating defects is unbounded; inverting the question is not.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from pydantic import Field, ValidationError
|
|
37
|
+
|
|
38
|
+
from syncade.synthesis_clusters import ClusterMemberEvidence, RootCauseCluster
|
|
39
|
+
|
|
40
|
+
_REVIEWER_ONLY_FINDING_FIELDS = frozenset({"line", "spec_clause"})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _EmptyClusterProbe(RootCauseCluster):
|
|
44
|
+
"""A :class:`RootCauseCluster` with ONLY the ``>=2`` length floors relaxed.
|
|
45
|
+
|
|
46
|
+
Used by :func:`_drop_empty_clusters` to decide a cluster is safe to
|
|
47
|
+
discard. Every OTHER constraint of the real model is inherited and enforced —
|
|
48
|
+
``extra="forbid"`` at both levels, field types, non-blank ``anchor_file`` /
|
|
49
|
+
``quote``, and ``_validate_self_consistency`` — so it keeps being enforced when
|
|
50
|
+
someone adds a constraint to ``RootCauseCluster`` and never opens this file.
|
|
51
|
+
|
|
52
|
+
**Two rounds of syncade's own panel shaped this, and both lessons are load-bearing.**
|
|
53
|
+
|
|
54
|
+
*Enumerating defects is unbounded.* The first version gated on cluster key names.
|
|
55
|
+
A reviewer found unknown keys nested in ``evidence``; a nested-key check was
|
|
56
|
+
added; the next reviewer found unknown value TYPES going through the same hole.
|
|
57
|
+
Keys, then nested keys, then types — each patch was answered by the next level
|
|
58
|
+
down. Inverting the question ("does this validate as a relaxed cluster?" rather
|
|
59
|
+
than "can I list what's wrong with it?") is bounded and total.
|
|
60
|
+
|
|
61
|
+
*And the repair must match the OBSERVED deviation, not a generalisation of it.*
|
|
62
|
+
Even inverted, the gate first allowed anything with ``<2`` members — which
|
|
63
|
+
quietly included the ONE-member case. A reviewer then pointed out that a
|
|
64
|
+
cluster's real invariants include cross-checks against ``consolidated_findings``
|
|
65
|
+
(member indices in range, ``anchor_file`` equal to the members' file, clusters
|
|
66
|
+
disjoint) that a per-cluster probe cannot see. Dead right: a 1-member cluster
|
|
67
|
+
with an out-of-range index would validate here and be dropped.
|
|
68
|
+
|
|
69
|
+
The bug was the generalisation. What ``claude-sonnet-4-6`` actually emits is an
|
|
70
|
+
EMPTY cluster — no members, no evidence — where ``gpt-5.5`` emits ``[]``. A
|
|
71
|
+
1-member cluster is a different animal: it NAMED a finding, so it makes a claim,
|
|
72
|
+
and a wrong claim must be loud. Narrowing the drop to genuinely-empty clusters
|
|
73
|
+
ends the whole line of attack, because a cluster that references nothing cannot
|
|
74
|
+
violate a cross-check — there is nothing to cross-check. The probe's blindness
|
|
75
|
+
to ``consolidated_findings`` stops mattering rather than being papered over.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
member_finding_indices: list[int] = Field(...)
|
|
79
|
+
evidence: list[ClusterMemberEvidence] = Field(...)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _is_empty_cluster(cluster: object) -> bool:
|
|
83
|
+
"""Whether ``cluster`` is well-formed but references NOTHING.
|
|
84
|
+
|
|
85
|
+
Both conditions matter. The probe rules out any defect other than the empty
|
|
86
|
+
lists; the emptiness itself is what makes dropping provably lossless and
|
|
87
|
+
context-free — no members means no index to be out of range, no file to
|
|
88
|
+
mismatch ``anchor_file``, and no member to overlap another cluster.
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
probe = _EmptyClusterProbe.model_validate(cluster)
|
|
92
|
+
except ValidationError:
|
|
93
|
+
return False # something ELSE is wrong — keep it, let the strict pass fail
|
|
94
|
+
return not probe.member_finding_indices and not probe.evidence
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _strip_reviewer_only_keys(items: list) -> tuple[list[object], bool]:
|
|
98
|
+
"""Drop known reviewer-only keys from each dict in ``items``.
|
|
99
|
+
|
|
100
|
+
Returns the repaired list plus whether any key was removed. Non-dict
|
|
101
|
+
entries pass through untouched. Shared by the consolidated-finding and
|
|
102
|
+
cluster-evidence repairs so both strip the same field set identically.
|
|
103
|
+
"""
|
|
104
|
+
repaired: list[object] = []
|
|
105
|
+
changed = False
|
|
106
|
+
for item in items:
|
|
107
|
+
if not isinstance(item, dict):
|
|
108
|
+
repaired.append(item)
|
|
109
|
+
continue
|
|
110
|
+
stripped = {k: v for k, v in item.items() if k not in _REVIEWER_ONLY_FINDING_FIELDS}
|
|
111
|
+
if len(stripped) != len(item):
|
|
112
|
+
changed = True
|
|
113
|
+
repaired.append(stripped)
|
|
114
|
+
return repaired, changed
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _strip_cluster_evidence(clusters: list) -> tuple[list[object], bool]:
|
|
118
|
+
"""Mirror the finding repair onto each cluster's ``evidence`` entries.
|
|
119
|
+
|
|
120
|
+
A model that copies ``line``/``spec_clause`` next to a cluster ``quote``
|
|
121
|
+
would otherwise be rejected by ``ClusterMemberEvidence`` (``extra="forbid"``).
|
|
122
|
+
Clusters/evidence not shaped as dict/list pass through untouched — the
|
|
123
|
+
strict validation pass already rejects those.
|
|
124
|
+
"""
|
|
125
|
+
repaired: list[object] = []
|
|
126
|
+
changed = False
|
|
127
|
+
for cluster in clusters:
|
|
128
|
+
evidence = cluster.get("evidence") if isinstance(cluster, dict) else None
|
|
129
|
+
if not isinstance(evidence, list):
|
|
130
|
+
repaired.append(cluster)
|
|
131
|
+
continue
|
|
132
|
+
new_evidence, ev_changed = _strip_reviewer_only_keys(evidence)
|
|
133
|
+
if not ev_changed:
|
|
134
|
+
repaired.append(cluster)
|
|
135
|
+
continue
|
|
136
|
+
new_cluster = dict(cluster)
|
|
137
|
+
new_cluster["evidence"] = new_evidence
|
|
138
|
+
repaired.append(new_cluster)
|
|
139
|
+
changed = True
|
|
140
|
+
return repaired, changed
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _drop_empty_clusters(clusters: list) -> tuple[list[object], bool]:
|
|
144
|
+
"""Drop clusters that reference NOTHING — no members, no evidence.
|
|
145
|
+
|
|
146
|
+
A root-cause cluster GROUPS findings — ``member_finding_indices`` requires
|
|
147
|
+
``>=2``. A cluster with NO members therefore asserts nothing at all: it groups
|
|
148
|
+
nothing and references nothing. But it fails ``min_length=2`` validation, and it
|
|
149
|
+
takes the whole (otherwise perfect) consolidation to exit 70 with it.
|
|
150
|
+
|
|
151
|
+
Observed live on ``claude-sonnet-4-6`` (PR-v2-23), which — handed a single
|
|
152
|
+
finding, i.e. nothing to cluster — emitted one cluster with empty
|
|
153
|
+
``member_finding_indices`` and empty ``evidence`` rather than the ``[]`` the
|
|
154
|
+
schema asks for. gpt-5.5 emits ``[]`` correctly, which is exactly why this
|
|
155
|
+
never surfaced while the judge was hardwired to codex.
|
|
156
|
+
|
|
157
|
+
Dropping is safe, and specifically it cannot launder a finding away:
|
|
158
|
+
``root_cause_clusters`` is ADVISORY and never affects the verdict (see
|
|
159
|
+
:class:`SynthesizerOutput`), and a degenerate cluster names no findings to
|
|
160
|
+
begin with. The alternative — burning a run, and both reviewers' spend, over
|
|
161
|
+
a null-valued optional field — is strictly worse.
|
|
162
|
+
|
|
163
|
+
Only genuinely EMPTY clusters are dropped — see :class:`_EmptyClusterProbe` for
|
|
164
|
+
why the earlier "<2 members" rule was too broad, and why an empty cluster is the
|
|
165
|
+
only one that can be discarded without reasoning about the findings list.
|
|
166
|
+
|
|
167
|
+
Every other defect — an unknown key at either level, a wrong value type, a blank
|
|
168
|
+
``anchor_file``, evidence inconsistent with its members, and now a 1-member
|
|
169
|
+
cluster (which NAMED a finding, so it makes a claim) — leaves the cluster in
|
|
170
|
+
place and the strict pass rejects the whole output at exit 70. Unexplained drift
|
|
171
|
+
stays loud even in data we would have discarded: that is what keeps this a
|
|
172
|
+
repair LIST and not a tolerance.
|
|
173
|
+
|
|
174
|
+
(The known-repairable ``line``/``spec_clause`` keys are already gone by this
|
|
175
|
+
point — :func:`_strip_cluster_evidence` runs first — so anything the probe
|
|
176
|
+
still rejects is genuinely unexplained.)
|
|
177
|
+
|
|
178
|
+
Returns the repaired list plus whether anything was dropped.
|
|
179
|
+
"""
|
|
180
|
+
repaired: list[object] = []
|
|
181
|
+
changed = False
|
|
182
|
+
for cluster in clusters:
|
|
183
|
+
if _is_empty_cluster(cluster):
|
|
184
|
+
changed = True
|
|
185
|
+
continue
|
|
186
|
+
repaired.append(cluster)
|
|
187
|
+
return repaired, changed
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _repair_known_model_deviations(parsed: object) -> object:
|
|
191
|
+
"""Repair the known, information-free ways a model deviates from the schema.
|
|
192
|
+
|
|
193
|
+
Two repairs, both narrow and both losing nothing:
|
|
194
|
+
|
|
195
|
+
1. Reviewer-only ``line`` / ``spec_clause`` fields copied into
|
|
196
|
+
``consolidated_findings`` entries or into a
|
|
197
|
+
``root_cause_clusters[*].evidence[*]`` quote object. Neither is part of
|
|
198
|
+
the synthesizer schema and neither is used for verdicts, but
|
|
199
|
+
``ConsolidatedFinding`` and ``ClusterMemberEvidence`` are
|
|
200
|
+
``extra="forbid"`` — so a stray copied coordinate turns an otherwise valid
|
|
201
|
+
consolidation into exit 70. (Seen live on Codex.)
|
|
202
|
+
2. Degenerate ``root_cause_clusters`` with fewer than two members — see
|
|
203
|
+
:func:`_drop_empty_clusters`. (Seen live on Anthropic.)
|
|
204
|
+
|
|
205
|
+
Everything else still fails validation: this is a repair list, not a
|
|
206
|
+
tolerance. Returns ``parsed`` unchanged (same object) when nothing was
|
|
207
|
+
repaired, so the caller can skip the retry.
|
|
208
|
+
"""
|
|
209
|
+
if not isinstance(parsed, dict):
|
|
210
|
+
return parsed
|
|
211
|
+
|
|
212
|
+
repaired_parsed = dict(parsed)
|
|
213
|
+
repaired_any = False
|
|
214
|
+
|
|
215
|
+
findings = parsed.get("consolidated_findings")
|
|
216
|
+
if isinstance(findings, list):
|
|
217
|
+
repaired_findings, changed = _strip_reviewer_only_keys(findings)
|
|
218
|
+
if changed:
|
|
219
|
+
repaired_parsed["consolidated_findings"] = repaired_findings
|
|
220
|
+
repaired_any = True
|
|
221
|
+
|
|
222
|
+
clusters = parsed.get("root_cause_clusters")
|
|
223
|
+
if isinstance(clusters, list):
|
|
224
|
+
repaired_clusters, stripped = _strip_cluster_evidence(clusters)
|
|
225
|
+
repaired_clusters, dropped = _drop_empty_clusters(repaired_clusters)
|
|
226
|
+
if stripped or dropped:
|
|
227
|
+
repaired_parsed["root_cause_clusters"] = repaired_clusters
|
|
228
|
+
repaired_any = True
|
|
229
|
+
|
|
230
|
+
return repaired_parsed if repaired_any else parsed
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Synthesizer JSON schema string.
|
|
2
|
+
|
|
3
|
+
``get_synthesizer_schema_string`` — the synthesizer prompt template's
|
|
4
|
+
``{json_schema}`` source. Pulls the root-cause-cluster fragment from
|
|
5
|
+
``synthesis_clusters`` (no dependency on ``synthesis`` itself), so it extracts
|
|
6
|
+
without a circular import. Re-exported from ``synthesis`` so the
|
|
7
|
+
``syncade.synthesis.get_synthesizer_schema_string`` import path is unchanged.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from syncade.synthesis_clusters import cluster_schema_fragment
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_synthesizer_schema_string() -> str:
|
|
16
|
+
"""Return the JSON schema string for :class:`SynthesizerOutput`,
|
|
17
|
+
formatted for inclusion in the synthesizer prompt template's
|
|
18
|
+
``{json_schema}`` placeholder.
|
|
19
|
+
|
|
20
|
+
Single source of truth — the synthesizer subprocess
|
|
21
|
+
pulls from here rather than hand-rolling the schema. Parallel to
|
|
22
|
+
:func:`syncade.findings.get_findings_schema_string`.
|
|
23
|
+
|
|
24
|
+
Output is a JSON-ish skeleton with inline ``//`` comments calling
|
|
25
|
+
out the contracts the prompt relies on (which fields are required,
|
|
26
|
+
which can be ``[]``, the cannot-invent and cannot-deactivate-unanimous-
|
|
27
|
+
blocker rules). NOT a strict JSON Schema document — it's prompt
|
|
28
|
+
input, intended for the model to read, not a validator.
|
|
29
|
+
"""
|
|
30
|
+
return (
|
|
31
|
+
"{\n"
|
|
32
|
+
' "consolidated_findings": [\n'
|
|
33
|
+
" {\n"
|
|
34
|
+
' "description": "string (required, non-empty)",\n'
|
|
35
|
+
' "file": "path"|null,\n'
|
|
36
|
+
' "severity": "blocker"|"minor"|"nit", '
|
|
37
|
+
"// synthesizer's final severity\n"
|
|
38
|
+
" // Do NOT include reviewer-only fields here: no line, "
|
|
39
|
+
"spec_clause, finding, priority_order, coverage_gaps, or "
|
|
40
|
+
"dismissed_concerns\n"
|
|
41
|
+
' "provenance": [ // required, min length 1; '
|
|
42
|
+
"cannot invent findings\n"
|
|
43
|
+
" {\n"
|
|
44
|
+
' "reviewer_name": "string",\n'
|
|
45
|
+
' "original_severity": "blocker"|"minor"|"nit",\n'
|
|
46
|
+
' "original_index": int, // index into that '
|
|
47
|
+
"reviewer's original findings list\n"
|
|
48
|
+
' "original_description": "string" // reviewer\'s '
|
|
49
|
+
"verbatim wording\n"
|
|
50
|
+
" }\n"
|
|
51
|
+
" ],\n"
|
|
52
|
+
' "dismissed": bool, // default false\n'
|
|
53
|
+
' "dismissal_rationale": "string"|null, '
|
|
54
|
+
"// required iff dismissed=true; cannot dismiss OR downgrade "
|
|
55
|
+
"a finding two distinct reviewers flagged at blocker "
|
|
56
|
+
"(unanimous-blocker rule, schema-enforced)\n"
|
|
57
|
+
' "severity_change_rationale": "string"|null '
|
|
58
|
+
"// required iff severity differs from EVERY reviewer's "
|
|
59
|
+
"original_severity\n"
|
|
60
|
+
" }\n"
|
|
61
|
+
" ],\n" + cluster_schema_fragment() + ",\n"
|
|
62
|
+
' "synthesis_summary": "string (required, non-empty narrative '
|
|
63
|
+
'about how the consolidation went)"\n'
|
|
64
|
+
"}"
|
|
65
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from . import validation as _validation
|
|
4
|
+
from .constants import (
|
|
5
|
+
SYNTHESIZER_MODEL,
|
|
6
|
+
SYNTHESIZER_NAME,
|
|
7
|
+
)
|
|
8
|
+
from .constants import (
|
|
9
|
+
SYNTHESIZER_PERMISSIONS as SYNTHESIZER_PERMISSIONS,
|
|
10
|
+
)
|
|
11
|
+
from .constants import (
|
|
12
|
+
SYNTHESIZER_PROVIDER as SYNTHESIZER_PROVIDER,
|
|
13
|
+
)
|
|
14
|
+
from .constants import (
|
|
15
|
+
SYNTHESIZER_THINKING as SYNTHESIZER_THINKING,
|
|
16
|
+
)
|
|
17
|
+
from .driver import run_synthesizer
|
|
18
|
+
from .result import SynthesizerResult
|
|
19
|
+
from .workspace import (
|
|
20
|
+
_init_workspace_git as _init_workspace_git,
|
|
21
|
+
)
|
|
22
|
+
from .workspace import (
|
|
23
|
+
_scrub_env_for_cold_synth as _scrub_env_for_cold_synth,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_validate_cluster_quotes_against_reviewers = _validation._validate_cluster_quotes_against_reviewers
|
|
27
|
+
_validate_duplicate_blockers_not_split_deactivated = (
|
|
28
|
+
_validation._validate_duplicate_blockers_not_split_deactivated
|
|
29
|
+
)
|
|
30
|
+
_validate_provenance_against_reviewers = _validation._validate_provenance_against_reviewers
|
|
31
|
+
_validate_reviewer_blockers_passed_through = _validation._validate_reviewer_blockers_passed_through
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"SYNTHESIZER_MODEL",
|
|
35
|
+
"SYNTHESIZER_NAME",
|
|
36
|
+
"SynthesizerResult",
|
|
37
|
+
"run_synthesizer",
|
|
38
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Synthesizer constants.
|
|
2
|
+
|
|
3
|
+
The four model knobs moved to :mod:`syncade.config_cold` in PR-v2-23, where they
|
|
4
|
+
are the DEFAULTS of the ``[synthesizer]`` config block. They are re-exported here
|
|
5
|
+
because ``persistence`` and ``metrics`` legitimately still want the *historical
|
|
6
|
+
default* rather than the operator's current config: when a legacy run's artifacts
|
|
7
|
+
never recorded which model the judge used, "the default as it was" is the only
|
|
8
|
+
honest fallback. Anything that wants the model actually in effect for THIS run must
|
|
9
|
+
read ``SyncadeConfig.synthesizer``, not these.
|
|
10
|
+
|
|
11
|
+
``SYNTHESIZER_NAME`` stays a real constant — it is an artifact basename, not a knob.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from syncade.config_cold import (
|
|
17
|
+
SYNTHESIZER_MODEL as SYNTHESIZER_MODEL,
|
|
18
|
+
)
|
|
19
|
+
from syncade.config_cold import (
|
|
20
|
+
SYNTHESIZER_PERMISSIONS as SYNTHESIZER_PERMISSIONS,
|
|
21
|
+
)
|
|
22
|
+
from syncade.config_cold import (
|
|
23
|
+
SYNTHESIZER_PROVIDER as SYNTHESIZER_PROVIDER,
|
|
24
|
+
)
|
|
25
|
+
from syncade.config_cold import (
|
|
26
|
+
SYNTHESIZER_THINKING as SYNTHESIZER_THINKING,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
SYNTHESIZER_NAME = "synthesizer"
|
|
30
|
+
"""Persistence basename for the synthesizer's artifacts
|
|
31
|
+
(``<round_dir>/synthesizer.{stdout,stderr,parsed.json,error.txt}``). Reused as the
|
|
32
|
+
``name`` of the synthetic :class:`ReviewerConfig` the driver builds, so the adapter's
|
|
33
|
+
provider-validation guards pass and the on-disk file naming stays consistent."""
|