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
@@ -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."""