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/adapters/base.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Per-provider adapter contract for reviewer dispatch.
|
|
2
|
+
|
|
3
|
+
An adapter translates a :class:`~syncade.config.ReviewerConfig`, a
|
|
4
|
+
worktree path, and a rendered prompt into a concrete
|
|
5
|
+
:class:`Invocation` that :func:`syncade.process.run_subprocess` can
|
|
6
|
+
execute. It then translates the resulting :class:`SubprocessResult`
|
|
7
|
+
back into a typed :class:`~syncade.findings.ReviewerOutput`.
|
|
8
|
+
|
|
9
|
+
Adapters never touch the network or filesystem themselves — they only build and
|
|
10
|
+
parse. The dispatcher actually runs subprocesses. This split lets adapters be
|
|
11
|
+
pure-ish and unit-testable without spawning reviewer CLIs.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Protocol, runtime_checkable
|
|
19
|
+
|
|
20
|
+
from syncade.config import ReviewerConfig
|
|
21
|
+
from syncade.findings import ReviewerOutput
|
|
22
|
+
from syncade.process import SubprocessResult
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class Invocation:
|
|
27
|
+
"""Resolved subprocess invocation for a single reviewer run.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
argv: The argument vector to pass to
|
|
31
|
+
:func:`syncade.process.run_subprocess`. ``argv[0]`` is the
|
|
32
|
+
binary to execute.
|
|
33
|
+
cwd: Working directory the subprocess runs from. Typically the
|
|
34
|
+
reviewer's worktree path.
|
|
35
|
+
env: Full environment for the subprocess. Adapters usually
|
|
36
|
+
inherit the parent's env so the user's existing auth
|
|
37
|
+
(keychain, OAuth, ``ANTHROPIC_API_KEY``) flows through.
|
|
38
|
+
stdin_text: If not ``None``, written to the subprocess's stdin
|
|
39
|
+
and stdin is then closed.
|
|
40
|
+
timeout_seconds: Maximum wall-clock seconds the dispatcher
|
|
41
|
+
should wait. ``None`` means no timeout — the dispatcher
|
|
42
|
+
may apply its own ceiling regardless.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
argv: list[str]
|
|
46
|
+
cwd: Path
|
|
47
|
+
env: dict[str, str]
|
|
48
|
+
stdin_text: str | None = None
|
|
49
|
+
timeout_seconds: float | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ReviewerInvocationError(Exception):
|
|
53
|
+
"""Raised by :meth:`ReviewerAdapter.parse_output` when a reviewer
|
|
54
|
+
subprocess failed.
|
|
55
|
+
|
|
56
|
+
This is the "subprocess itself blew up" path — auth failure, model
|
|
57
|
+
unavailable, network error, CLI usage bug. Distinct from
|
|
58
|
+
:class:`~syncade.findings.ReviewerOutputError` (the subprocess ran
|
|
59
|
+
fine but its output didn't parse). The orchestrator maps this to
|
|
60
|
+
syncade exit code 40 (``REVIEWER_FAILURE``); parse failures map to
|
|
61
|
+
exit code 70 (``REVIEWER_OUTPUT_UNPARSEABLE``).
|
|
62
|
+
|
|
63
|
+
"Failed" here means either a non-zero ``returncode`` OR a zero
|
|
64
|
+
return code with the provider's structured-failure signal set
|
|
65
|
+
(``claude``'s ``is_error: true`` envelope field, for example).
|
|
66
|
+
Adapters that surface a structured failure should populate
|
|
67
|
+
:attr:`api_error_status` so the dispatcher can distinguish
|
|
68
|
+
transient (5xx, 429) from terminal (4xx) provider errors.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
returncode: The subprocess's exit code.
|
|
72
|
+
stdout: Captured stdout (may include partial reviewer output
|
|
73
|
+
or, for Anthropic, the failure envelope).
|
|
74
|
+
stderr: Captured stderr.
|
|
75
|
+
api_error_status: Provider HTTP-style status code if available
|
|
76
|
+
(e.g. 404 for a missing model). ``None`` when not
|
|
77
|
+
applicable, when the failure happened before reaching the
|
|
78
|
+
provider, or when the adapter doesn't surface this.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
message: str,
|
|
84
|
+
*,
|
|
85
|
+
returncode: int,
|
|
86
|
+
stdout: str,
|
|
87
|
+
stderr: str,
|
|
88
|
+
api_error_status: int | None = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
super().__init__(message)
|
|
91
|
+
self.returncode = returncode
|
|
92
|
+
self.stdout = stdout
|
|
93
|
+
self.stderr = stderr
|
|
94
|
+
self.api_error_status = api_error_status
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@runtime_checkable
|
|
98
|
+
class ReviewerAdapter(Protocol):
|
|
99
|
+
"""Per-provider adapter protocol.
|
|
100
|
+
|
|
101
|
+
Implementations:
|
|
102
|
+
|
|
103
|
+
- :class:`syncade.adapters.anthropic.AnthropicAdapter` — production
|
|
104
|
+
adapter for the ``claude`` CLI.
|
|
105
|
+
- :class:`syncade.adapters.openai.CodexAdapter` — production
|
|
106
|
+
adapter for the OpenAI ``codex`` CLI.
|
|
107
|
+
- :class:`syncade.adapters.fake.FakeAdapter` — test double; never
|
|
108
|
+
shells out.
|
|
109
|
+
|
|
110
|
+
:attr:`name` is the stable identifier that must match
|
|
111
|
+
:attr:`syncade.config.ReviewerConfig.provider` so the dispatcher
|
|
112
|
+
can pick the right adapter for each configured reviewer.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
name: str
|
|
116
|
+
|
|
117
|
+
def build_invocation(
|
|
118
|
+
self,
|
|
119
|
+
reviewer_config: ReviewerConfig,
|
|
120
|
+
worktree_path: Path,
|
|
121
|
+
prompt: str,
|
|
122
|
+
) -> Invocation:
|
|
123
|
+
"""Build a subprocess invocation from a reviewer's config + worktree
|
|
124
|
+
+ already-rendered prompt. No side effects."""
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
def parse_output(self, result: SubprocessResult) -> ReviewerOutput:
|
|
128
|
+
"""Parse a finished subprocess result into a typed reviewer
|
|
129
|
+
output. Raises :class:`ReviewerInvocationError` on non-zero
|
|
130
|
+
returncode; :class:`~syncade.findings.ReviewerOutputError` on
|
|
131
|
+
unparseable output."""
|
|
132
|
+
...
|
|
133
|
+
|
|
134
|
+
def extract_final_text(
|
|
135
|
+
self,
|
|
136
|
+
result: SubprocessResult,
|
|
137
|
+
*,
|
|
138
|
+
empty_output_exception_class: type[Exception],
|
|
139
|
+
) -> str:
|
|
140
|
+
"""Return the model's final response text, stripped of whatever
|
|
141
|
+
envelope the provider's CLI wraps it in.
|
|
142
|
+
|
|
143
|
+
This is the provider-agnostic seam. :meth:`parse_output` is only
|
|
144
|
+
one thing you can do with a finished subprocess: parse the text as
|
|
145
|
+
a :class:`~syncade.findings.ReviewerOutput`. The cold synthesizer
|
|
146
|
+
parses the SAME text as a
|
|
147
|
+
:class:`~syncade.synthesis.SynthesizerOutput`, the spec drafter as
|
|
148
|
+
a ``SpecDraftOutput``, the producer as free narrative. Each needs
|
|
149
|
+
the text without the schema, so the text extraction lives here and
|
|
150
|
+
``parse_output`` is a thin wrapper over it.
|
|
151
|
+
|
|
152
|
+
Without this on the Protocol, any caller that wants raw text has to
|
|
153
|
+
name a concrete adapter class — which is exactly how the judge ended
|
|
154
|
+
up hardwired to codex.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
result: The finished subprocess.
|
|
158
|
+
empty_output_exception_class: Raised when the subprocess
|
|
159
|
+
SUCCEEDED but produced no usable text. Callers pass the
|
|
160
|
+
exception type matching the shape they were going to parse
|
|
161
|
+
into (``ReviewerOutputError``, ``SynthesizerOutputError``,
|
|
162
|
+
…) so a content failure lands in that caller's error
|
|
163
|
+
taxonomy rather than the adapter's. All map to exit 70.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
The model's final response text. Never empty.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
ReviewerInvocationError: The subprocess itself failed — non-zero
|
|
170
|
+
returncode, or a zero returncode carrying the provider's
|
|
171
|
+
structured-failure signal (claude's ``is_error: true``
|
|
172
|
+
envelope, codex's ``turn.failed`` event). Exit 40, and the
|
|
173
|
+
dispatcher/synthesizer consult
|
|
174
|
+
:mod:`syncade.retry` on it to decide whether to retry.
|
|
175
|
+
empty_output_exception_class: Ran fine, said nothing usable.
|
|
176
|
+
"""
|
|
177
|
+
...
|
|
178
|
+
|
|
179
|
+
def check_auth(self) -> None:
|
|
180
|
+
"""Optional pre-flight: verify the provider's CLI is logged in
|
|
181
|
+
and ready for headless invocation.
|
|
182
|
+
|
|
183
|
+
Adapters with a cheap, deterministic auth-check command (e.g.
|
|
184
|
+
``codex login status``) override this to short-circuit the
|
|
185
|
+
whole dispatch batch when one reviewer's auth is broken — the
|
|
186
|
+
dispatcher then fails fast rather than letting the broken
|
|
187
|
+
reviewer chew through tokens or retry loops before its sibling
|
|
188
|
+
reviewers finish.
|
|
189
|
+
|
|
190
|
+
Implementations should:
|
|
191
|
+
|
|
192
|
+
- Be fast (no network call if possible).
|
|
193
|
+
- Raise :class:`ReviewerInvocationError` with a message that
|
|
194
|
+
tells the user EXACTLY what to fix (e.g. "run ``codex login``
|
|
195
|
+
to authenticate").
|
|
196
|
+
- When the provider has no cheap pre-flight, define an
|
|
197
|
+
explicit no-op (``def check_auth(self) -> None: return None``)
|
|
198
|
+
so the dispatcher's auth phase skips this adapter cleanly
|
|
199
|
+
and :func:`isinstance(adapter, ReviewerAdapter)` keeps
|
|
200
|
+
working. See the note below on Protocol conformance.
|
|
201
|
+
|
|
202
|
+
**No-op via inheritance is NOT enough.** This is a
|
|
203
|
+
``@runtime_checkable`` Protocol, so
|
|
204
|
+
``isinstance(adapter, ReviewerAdapter)`` checks that the
|
|
205
|
+
attribute exists on the *instance*. A class that omits
|
|
206
|
+
``check_auth`` entirely does NOT pick up the ellipsis body
|
|
207
|
+
below as a real implementation — the isinstance check fails
|
|
208
|
+
outright. Every concrete adapter must define ``check_auth``,
|
|
209
|
+
even if it's only ``return None``.
|
|
210
|
+
:class:`syncade.adapters.anthropic.AnthropicAdapter` defines
|
|
211
|
+
this explicit no-op; its auth failures surface via the
|
|
212
|
+
``is_error: true`` envelope in :meth:`parse_output` during
|
|
213
|
+
the actual run.
|
|
214
|
+
:class:`syncade.adapters.openai.CodexAdapter` overrides with
|
|
215
|
+
a real ``codex login status`` check (see
|
|
216
|
+
the codex CLI output notes).
|
|
217
|
+
|
|
218
|
+
The dispatcher always calls ``check_auth`` before starting parallel
|
|
219
|
+
runs.
|
|
220
|
+
"""
|
|
221
|
+
...
|
syncade/adapters/fake.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Test-double adapters for :class:`~syncade.adapters.base.ReviewerAdapter`,
|
|
2
|
+
the synthesizer's :class:`~syncade.adapters.openai.CodexAdapter`
|
|
3
|
+
surface, and the producer's
|
|
4
|
+
:class:`~syncade.adapters.producer.ProducerAdapter` surface.
|
|
5
|
+
|
|
6
|
+
The fakes never shell out: ``build_invocation`` returns an
|
|
7
|
+
:class:`~syncade.adapters.base.Invocation` pointing at a no-op shell
|
|
8
|
+
command (so the dispatcher / synthesizer can still flow through
|
|
9
|
+
``process.run_subprocess`` without errors during integration tests),
|
|
10
|
+
and the parse / extract methods ignore the
|
|
11
|
+
:class:`~syncade.process.SubprocessResult` entirely and return
|
|
12
|
+
whatever was configured at construction time.
|
|
13
|
+
|
|
14
|
+
These aren't features — they're primitives for testing other code.
|
|
15
|
+
The dispatcher tests compose :class:`FakeAdapter` instances to
|
|
16
|
+
exercise N-reviewer scheduling, partial-failure handling, etc. The
|
|
17
|
+
orchestrator tests compose :class:`FakeSynthesizerAdapter`
|
|
18
|
+
instances to exercise the synthesizer phase without spawning a real
|
|
19
|
+
codex CLI.
|
|
20
|
+
|
|
21
|
+
Threading: the dispatcher's pre-flight phase calls ``check_auth`` from a
|
|
22
|
+
ThreadPoolExecutor and the dispatch phase calls
|
|
23
|
+
``build_invocation``/``parse_output`` from one too. ``check_auth_calls``,
|
|
24
|
+
``parse_output_calls``, and the ``invocations`` log all use a
|
|
25
|
+
:class:`threading.Lock` so concurrent updates from multiple worker
|
|
26
|
+
threads don't lose increments or interleave list appends. Tests in
|
|
27
|
+
practice use one adapter instance per reviewer (matching production —
|
|
28
|
+
each reviewer gets a fresh adapter from the registry), but the lock
|
|
29
|
+
makes the test double safe under any thread layout.
|
|
30
|
+
|
|
31
|
+
The synthesizer fake doesn't take that locking step because the
|
|
32
|
+
synthesizer phase is single-threaded (one codex subprocess per round,
|
|
33
|
+
not N parallel). If a future PR introduces parallel synthesizer
|
|
34
|
+
dispatch, revisit this.
|
|
35
|
+
|
|
36
|
+
this module was decomposed into ``fake_common`` (the shared
|
|
37
|
+
``_noop_argv`` helper), ``fake_reviewer_synth`` (the reviewer + synthesizer
|
|
38
|
+
doubles), and ``fake_producer_audit_draft`` (the producer / auditor / drafter
|
|
39
|
+
doubles). This module re-exports all of them so the
|
|
40
|
+
``syncade.adapters.fake.<name>`` import paths are unchanged.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
from .fake_common import _noop_argv
|
|
46
|
+
from .fake_producer_audit_draft import (
|
|
47
|
+
FakeAuditorAdapter,
|
|
48
|
+
FakeDrafterAdapter,
|
|
49
|
+
FakeProducerAdapter,
|
|
50
|
+
_default_canned_audit_output,
|
|
51
|
+
_default_canned_draft_output,
|
|
52
|
+
_default_canned_producer_output,
|
|
53
|
+
)
|
|
54
|
+
from .fake_reviewer_synth import (
|
|
55
|
+
FakeAdapter,
|
|
56
|
+
FakeSynthesizerAdapter,
|
|
57
|
+
_default_canned_output,
|
|
58
|
+
_default_canned_synth_output,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"_noop_argv",
|
|
63
|
+
"FakeAdapter",
|
|
64
|
+
"_default_canned_output",
|
|
65
|
+
"FakeSynthesizerAdapter",
|
|
66
|
+
"_default_canned_synth_output",
|
|
67
|
+
"FakeProducerAdapter",
|
|
68
|
+
"_default_canned_producer_output",
|
|
69
|
+
"FakeAuditorAdapter",
|
|
70
|
+
"_default_canned_audit_output",
|
|
71
|
+
"FakeDrafterAdapter",
|
|
72
|
+
"_default_canned_draft_output",
|
|
73
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Shared helper for the fake adapters.
|
|
2
|
+
|
|
3
|
+
``_noop_argv`` is the do-nothing-exit-0 argv every fake's ``build_invocation``
|
|
4
|
+
points at, so the dispatcher / synthesizer / producer can still flow through
|
|
5
|
+
``process.run_subprocess`` without errors during integration tests.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _noop_argv() -> list[str]:
|
|
15
|
+
"""The argv for a "do nothing, exit 0" subprocess.
|
|
16
|
+
|
|
17
|
+
POSIX gets a path discovered via ``shutil.which("true")``; Windows
|
|
18
|
+
gets ``cmd /c exit 0``. Using ``shutil.which`` rather than a
|
|
19
|
+
hard-coded path covers the GNU coreutils install (``/usr/bin/true``)
|
|
20
|
+
AND the BSD/macOS layout (``/usr/bin/true``, with ``/bin/true``
|
|
21
|
+
absent on recent macOS). Falls back to ``["true"]`` if not on PATH —
|
|
22
|
+
that path is virtually never exercised because every POSIX system
|
|
23
|
+
has ``true`` somewhere, but it keeps imports safe even in stripped
|
|
24
|
+
environments.
|
|
25
|
+
"""
|
|
26
|
+
if sys.platform == "win32": # pragma: no cover - macOS/Linux dev path
|
|
27
|
+
return ["cmd", "/c", "exit", "0"]
|
|
28
|
+
resolved = shutil.which("true")
|
|
29
|
+
return [resolved or "true"]
|