uncoded 0.5.0__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.
@@ -0,0 +1,203 @@
1
+ """Generate configuration files for Serena + ty LSP integration.
2
+
3
+ Writes three files that wire a repo up to Serena's MCP bridge with ty as
4
+ the Python language-server backend, in the shape Claude Code picks up
5
+ automatically:
6
+
7
+ * ``.mcp.json`` — registers the Serena MCP server so Claude Code launches
8
+ it via ``uvx`` on session start, with the web dashboard disabled.
9
+ * ``.serena/project.yml`` — selects ty over Serena's default backend
10
+ (pyright), keeps Serena out of uncoded's generated stubs, and narrows
11
+ Serena's surface to pure LSP operations: memory, onboarding,
12
+ dashboard, and shell-exec tools are all excluded. uncoded's namespace
13
+ map and stubs already give agents a project-wide view, so Serena's
14
+ memory-based project understanding is redundant and noisy alongside
15
+ it.
16
+ * ``.claude/settings.json`` — enables the Serena server and allowlists
17
+ the eight LSP tools (symbol lookup, reference search, and the edit
18
+ family) so they run without a prompt.
19
+
20
+ JSON files merge into existing content: pre-existing non-Serena MCP
21
+ servers and permissions are preserved, while the Serena entry itself
22
+ refreshes to the current ``SERENA_VERSION`` so re-running after a bump
23
+ propagates the pin. The YAML project file is only written when absent,
24
+ to avoid clobbering hand-edited Serena config.
25
+ """
26
+
27
+ import json
28
+ import tomllib
29
+ from pathlib import Path
30
+
31
+ from uncoded.config import find_pyproject_toml
32
+
33
+ # Pin the Serena version so every repo that runs setup-serena gets the
34
+ # same, tested integration. On bump, re-run `uncoded setup-serena` to
35
+ # refresh the pin in existing repos — the sync overwrites the serena
36
+ # entry in .mcp.json with the current MCP_SERVER_SERENA value. A
37
+ # dogfooding test in tests/test_serena_setup.py guards against drift.
38
+ SERENA_VERSION = "1.1.2"
39
+
40
+ MCP_SERVER_SERENA = {
41
+ "command": "uvx",
42
+ "args": [
43
+ "--from",
44
+ f"serena-agent=={SERENA_VERSION}",
45
+ "serena",
46
+ "start-mcp-server",
47
+ "--context",
48
+ "claude-code",
49
+ "--transport",
50
+ "stdio",
51
+ "--project-from-cwd",
52
+ "--open-web-dashboard",
53
+ "false",
54
+ ],
55
+ }
56
+
57
+ SERENA_PROJECT_YML = """\
58
+ project_name: "{project_name}"
59
+ languages: ["python_ty"]
60
+ ignored_paths:
61
+ - ".uncoded"
62
+ excluded_tools:
63
+ - execute_shell_command
64
+ - list_memories
65
+ - read_memory
66
+ - write_memory
67
+ - edit_memory
68
+ - delete_memory
69
+ - rename_memory
70
+ - onboarding
71
+ - check_onboarding_performed
72
+ - open_dashboard
73
+ """
74
+
75
+ SERENA_ALLOWED_TOOLS = [
76
+ "mcp__serena__initial_instructions",
77
+ "mcp__serena__find_symbol",
78
+ "mcp__serena__find_referencing_symbols",
79
+ "mcp__serena__get_symbols_overview",
80
+ "mcp__serena__rename_symbol",
81
+ "mcp__serena__safe_delete_symbol",
82
+ "mcp__serena__insert_before_symbol",
83
+ "mcp__serena__insert_after_symbol",
84
+ "mcp__serena__replace_symbol_body",
85
+ ]
86
+
87
+ _STATUS_VERB = {
88
+ "wrote": "Wrote",
89
+ "updated": "Updated",
90
+ "unchanged": "Unchanged",
91
+ }
92
+
93
+
94
+ def read_project_name() -> str:
95
+ """Read the project name from pyproject.toml, falling back to the cwd name."""
96
+ toml_path = find_pyproject_toml()
97
+ if toml_path is None:
98
+ return Path.cwd().name
99
+ with toml_path.open("rb") as f:
100
+ data = tomllib.load(f)
101
+ try:
102
+ return data["project"]["name"]
103
+ except KeyError:
104
+ return Path.cwd().name
105
+
106
+
107
+ def _sync_mcp_json(path: Path) -> str:
108
+ """Write or merge Serena into ``.mcp.json``.
109
+
110
+ Non-Serena MCP servers already in the file are preserved. The
111
+ ``serena`` entry itself is always refreshed to ``MCP_SERVER_SERENA``
112
+ so a ``SERENA_VERSION`` bump flows into existing repos on the next
113
+ ``setup-serena`` run. Anyone who has hand-customised the ``serena``
114
+ entry should keep their edits out of this file (add a sibling entry
115
+ instead, or re-apply after refresh).
116
+
117
+ Returns a one-word status: ``wrote``, ``updated``, or ``unchanged``.
118
+ """
119
+ if path.exists():
120
+ data = json.loads(path.read_text())
121
+ servers = data.setdefault("mcpServers", {})
122
+ if servers.get("serena") == MCP_SERVER_SERENA:
123
+ return "unchanged"
124
+ servers["serena"] = MCP_SERVER_SERENA
125
+ status = "updated"
126
+ else:
127
+ data = {"mcpServers": {"serena": MCP_SERVER_SERENA}}
128
+ status = "wrote"
129
+ path.parent.mkdir(parents=True, exist_ok=True)
130
+ path.write_text(json.dumps(data, indent=2) + "\n")
131
+ return status
132
+
133
+
134
+ def _sync_serena_project(path: Path, project_name: str) -> str:
135
+ """Write ``.serena/project.yml`` if absent.
136
+
137
+ Returns ``wrote`` or ``unchanged``. An existing file is never touched:
138
+ YAML merging preserves neither comments nor key order, and a user who
139
+ has customised their Serena config should keep it.
140
+ """
141
+ if path.exists():
142
+ return "unchanged"
143
+ path.parent.mkdir(parents=True, exist_ok=True)
144
+ path.write_text(SERENA_PROJECT_YML.format(project_name=project_name))
145
+ return "wrote"
146
+
147
+
148
+ def _sync_claude_settings(path: Path) -> str:
149
+ """Write or merge Serena allowlist into ``.claude/settings.json``.
150
+
151
+ Returns ``wrote``, ``updated``, or ``unchanged``.
152
+ """
153
+ if path.exists():
154
+ data = json.loads(path.read_text())
155
+ status = "unchanged"
156
+ else:
157
+ data = {}
158
+ status = "wrote"
159
+
160
+ enabled = data.setdefault("enabledMcpjsonServers", [])
161
+ if "serena" not in enabled:
162
+ enabled.append("serena")
163
+ if status == "unchanged":
164
+ status = "updated"
165
+
166
+ permissions = data.setdefault("permissions", {})
167
+ allow = permissions.setdefault("allow", [])
168
+ for tool in SERENA_ALLOWED_TOOLS:
169
+ if tool not in allow:
170
+ allow.append(tool)
171
+ if status == "unchanged":
172
+ status = "updated"
173
+
174
+ if status == "unchanged":
175
+ return status
176
+ path.parent.mkdir(parents=True, exist_ok=True)
177
+ path.write_text(json.dumps(data, indent=2) + "\n")
178
+ return status
179
+
180
+
181
+ def setup_serena(root: Path | None = None) -> int:
182
+ """Generate Serena + ty + Claude Code configuration under ``root``.
183
+
184
+ JSON files merge into existing content, refreshing the Serena
185
+ entries so a re-run picks up a bumped ``SERENA_VERSION``. The
186
+ Serena YAML project file is only written when absent.
187
+ """
188
+ if root is None:
189
+ root = Path.cwd()
190
+ project_name = read_project_name()
191
+
192
+ mcp_path = root / ".mcp.json"
193
+ serena_path = root / ".serena" / "project.yml"
194
+ claude_path = root / ".claude" / "settings.json"
195
+
196
+ results = [
197
+ (mcp_path, _sync_mcp_json(mcp_path)),
198
+ (serena_path, _sync_serena_project(serena_path, project_name)),
199
+ (claude_path, _sync_claude_settings(claude_path)),
200
+ ]
201
+ for path, status in results:
202
+ print(f"{_STATUS_VERB[status]} {path.relative_to(root)}")
203
+ return 0
uncoded/skill.py ADDED
@@ -0,0 +1,388 @@
1
+ """Generate the coherence-review skill file for the target repository."""
2
+
3
+ from pathlib import Path
4
+
5
+ from uncoded.sync import remove_file, sync_file
6
+
7
+ SKILL_OUTPUTS = [
8
+ Path(".claude/skills/coherence-review/SKILL.md"), # Claude Code
9
+ Path(".agents/skills/coherence-review/SKILL.md"), # Codex
10
+ ]
11
+
12
+ LEGACY_SKILL_OUTPUTS = [
13
+ Path(".claude/skills/uncoded-review/SKILL.md"), # Claude Code
14
+ Path(".agents/skills/uncoded-review/SKILL.md"), # Codex
15
+ ]
16
+
17
+ _SKILL_CONTENT = """\
18
+ ---
19
+ name: coherence-review
20
+ description: "Perform a coherence review of a Python codebase: a diagnostic sweep \
21
+ for semantic drift, naming inconsistency, promissory mismatch, and structural \
22
+ incoherence. Produces a Markdown report of findings with verbatim evidence and \
23
+ confidence levels, for human investigation. Assumes uncoded is installed \
24
+ (.uncoded/namespace.yaml and .uncoded/stubs/ present)."
25
+ ---
26
+
27
+ # Coherence Review
28
+
29
+ A diagnostic sweep of a Python codebase for specific, observable symptoms of
30
+ semantic drift and incoherence. Output is a structured Markdown report of
31
+ candidate regions, each with verbatim evidence and a confidence level, for a
32
+ human to investigate. Not a bug hunt, not a lint pass, not a refactor.
33
+
34
+ ## Why coherence, and what you are looking for
35
+
36
+ A codebase accumulates corruption differently from how it accumulates bugs. Bugs
37
+ announce themselves — tests fail, users complain, exceptions raise. Corruption
38
+ passes every integrity check. It is the slow divergence between what the code
39
+ claims to mean and what it actually does, between how one part of the codebase
40
+ names a concept and how another names the same concept, between an architecture
41
+ as declared and an architecture as practised. Each local decision that
42
+ contributed to it was reasonable. The accumulation is not.
43
+
44
+ Corruption's one observable signature is **internal inconsistency**. Not
45
+ absolute wrongness — that requires a reference outside the code, which is not
46
+ available here. Just pairwise disagreement between things that ought to agree:
47
+ a name and its behaviour, two names for the same concept, a docstring and the
48
+ signature it sits on, a declared architecture and the actual import graph. Every
49
+ symptom in the sweeps below is a form of inconsistency. The review's job is to
50
+ find these disagreements — not to diagnose root cause and not to fix them.
51
+
52
+ ## What this skill is not
53
+
54
+ - **Not a bug-finder.** Bugs are the job of testing and code review. A coherence
55
+ review can run on code that passes every test and still find plenty.
56
+ - **Not a style pass.** Tabs versus spaces, docstring format, import ordering —
57
+ irrelevant. Linters exist.
58
+ - **Not a refactor.** No proposing fixes, no suggesting renames, no rewriting
59
+ code. The output is findings. The human decides what to do with them.
60
+ - **Not a general code review.** Performance, security, correctness — out of
61
+ scope unless they happen to manifest as a coherence symptom.
62
+
63
+ ## Prerequisites
64
+
65
+ Verify by reading `.uncoded/namespace.yaml` — if it exists and is non-empty,
66
+ proceed. If not, stop and tell the user to run `uncoded sync` first; the review
67
+ depends on the index.
68
+
69
+ If Serena MCP tools are available (`mcp__serena__*`), the structural sweep has
70
+ more leverage. The review still works without Serena but will be weaker on
71
+ cross-file reference checks.
72
+
73
+ ## Workflow
74
+
75
+ The review proceeds in four sweeps, each building on the previous:
76
+
77
+ 1. **Orient** — load the navigation index and form a mental map.
78
+ 2. **Lexical sweep** — read the namespace, look for naming-level inconsistency.
79
+ 3. **Promissory sweep** — read stubs, check each symbol's name / signature /
80
+ docstring for internal disagreement.
81
+ 4. **Structural sweep** — combine namespace and imports to find boundary and
82
+ shape symptoms.
83
+
84
+ Do the sweeps in order. Write findings as you go — do not hold them in memory
85
+ until the end.
86
+
87
+ ## Step 1: Orient
88
+
89
+ Read `.uncoded/namespace.yaml` in full. This is the map — directories, files,
90
+ classes, methods, functions — in source order. Do not skim. Every public symbol
91
+ in the codebase is listed here, and the shape of the namespace itself is
92
+ evidence.
93
+
94
+ While reading, note:
95
+
96
+ - The vocabulary the codebase uses for its core concepts
97
+ - The organisational logic (domain-driven? layered? feature-based? ad hoc?)
98
+ - Anything that surprises you — odd names, asymmetric organisation, suspicious
99
+ clusters
100
+
101
+ Also read `CLAUDE.md` if present, and follow any repo-specific navigation
102
+ protocol throughout the review.
103
+
104
+ Before starting the sweeps, tell the user how many public symbols were indexed.
105
+ Then proceed without asking for further confirmation.
106
+
107
+ ## Step 2: Lexical sweep
108
+
109
+ Working from the namespace alone, look for four categories of naming-level
110
+ symptom.
111
+
112
+ **Concept duplication under different names.** The same concept referred to by
113
+ different names in different parts of the codebase. Examples: `fetch_user`,
114
+ `get_user`, `load_user`, `retrieve_user` all appearing as separate functions
115
+ doing substantively the same thing. `compute_*`, `calculate_*`, `derive_*` used
116
+ interchangeably. Two classes called `UserRecord` and `AccountProfile` that
117
+ model the same entity.
118
+
119
+ Detection: scan the namespace for symbol clusters with verb or noun overlap.
120
+ Where suspicion arises, spot-check the relevant stubs to confirm they overlap
121
+ in meaning.
122
+
123
+ **Qualifier accretion.** Names carrying modifiers that are fossils of
124
+ iteration: `_new`, `_v2`, `_updated`, `_legacy`, `_real`, `_proper`, `_final`,
125
+ `_fixed`. Also prefix forms: `new_`, `old_`, `real_`. These are almost always
126
+ worth flagging — someone needed to distinguish a new thing from an old thing
127
+ and the distinction was never resolved.
128
+
129
+ Detection: scan the namespace for these qualifier patterns.
130
+
131
+ **Vocabulary islands.** A subregion of the codebase (a directory, a module
132
+ cluster) using a distinct vocabulary that doesn't overlap with the rest. Often
133
+ the result of an unintegrated contribution, or a session that added a feature
134
+ without looking outward.
135
+
136
+ Detection: look for directories whose namespace entries share few word-roots
137
+ with the rest of the codebase.
138
+
139
+ **Collision with drift.** The same name appearing in multiple places with
140
+ subtly different meanings — visible as different signatures, different docstring
141
+ content, or different domain associations.
142
+
143
+ Detection: identify name collisions in the namespace, then examine the stubs to
144
+ see whether the uses agree.
145
+
146
+ ## Step 3: Promissory sweep
147
+
148
+ Working from stubs, examine each public symbol's name / signature / docstring
149
+ triple for internal disagreement.
150
+
151
+ For each non-trivial public symbol (skip trivial one-liners and `__init__` with
152
+ no meaningful body):
153
+
154
+ **Name–signature mismatch.** Does the name's verb fit the signature's return? A
155
+ function called `validate_*` that returns the validated object rather than
156
+ raising or returning bool. A `get_*` that mutates. A noun-named thing that is a
157
+ verb's worth of work. A boolean-returning function whose name doesn't start with
158
+ `is_`, `has_`, `can_`, or similar.
159
+
160
+ **Docstring–signature mismatch.** Does the docstring refer to parameters not in
161
+ the signature, or fail to mention parameters that are? Does the docstring
162
+ describe return behaviour that contradicts the type annotation?
163
+
164
+ **Docstring–name mismatch.** Does the docstring describe an operation noticeably
165
+ more specific, more general, or simply different from what the name advertises?
166
+ "Normalises and validates the record" on a function called `check_record`.
167
+
168
+ **Defensive docstrings.** Docstrings that warn about the function rather than
169
+ describe it. "Note: this does not actually X despite the name." "Do not use
170
+ this for Y; use Z instead." These are confessions — someone noticed drift and
171
+ documented it rather than fixing it.
172
+
173
+ The stub itself is the evidence. Quote the stub excerpt (name, signature,
174
+ first-line docstring) verbatim in the finding.
175
+
176
+ **When to read source.** The stub is usually sufficient for discovery — the
177
+ inconsistency IS the mismatch between name, signature, and docstring, all of
178
+ which the stub provides. Read the symbol body when a finding is already
179
+ identified but confidence is genuinely uncertain: an undocumented parameter
180
+ where significance depends on what it controls; a name–behaviour mismatch where
181
+ the stub alone doesn't confirm it; a defensive docstring you want to verify is
182
+ accurate. Use Serena's `find_symbol` with `include_body=True` — targeted to the
183
+ symbol, no offset arithmetic, no risk of over-reading. Never read a whole source
184
+ file during this sweep.
185
+
186
+ ## Step 4: Structural sweep
187
+
188
+ Combine the namespace with the import graph and, if available, Serena's
189
+ reference resolution.
190
+
191
+ **Overgrown public surfaces / god modules.** A module or class whose public
192
+ namespace is much larger than its siblings, or spans obviously different
193
+ concerns. Look for outlier symbol counts: a file with forty public symbols where
194
+ its neighbours have five; a class with thirty methods covering multiple domains.
195
+
196
+ **Boundary violations.** One module importing private symbols (leading
197
+ underscore) from another. Scan `from module import _thing` patterns across
198
+ stubs' import sections. Each instance is a finding.
199
+
200
+ **Cross-vocabulary imports.** Imports that cross domain boundaries in suspicious
201
+ directions — a `core/` or `utils/` module importing from a specific business
202
+ domain; a module in domain A importing from domain B when those domains appear
203
+ meant to be independent. Flag candidates and note the direction; let the human
204
+ decide.
205
+
206
+ **Zero-caller public symbols.** A public symbol (no leading underscore) with no
207
+ references anywhere in the codebase. Either dead code or an unused API surface.
208
+
209
+ Check systematically, not by spot-check:
210
+
211
+ 1. From the namespace map, list all public symbols in each source module.
212
+ 2. Cross-reference with stub import sections — any symbol imported by another
213
+ source module is live; remove it from the candidate list. This culls the
214
+ obvious cases cheaply.
215
+ 3. For remaining candidates, use Serena's `find_referencing_symbols` to verify.
216
+ If Serena is unavailable, note findings as lower confidence.
217
+ 4. Distinguish two sub-cases when reporting:
218
+ - *No callers anywhere* — dead code; highest priority.
219
+ - *Callers only in tests* — the symbol is tested but not used in source;
220
+ may be an exposed internal that should be private.
221
+
222
+ **Redundant public surface.** A public constant and a public parameterless
223
+ function in the same module where the function's sole body is `return
224
+ <constant>`. Both symbols being public exposes an implementation detail
225
+ unnecessarily — only one needs to be public. Detection: use the stubs to find
226
+ public parameterless functions near public constants, then verify each
227
+ candidate body with Serena's `find_symbol` with `include_body=True` before
228
+ reporting.
229
+
230
+ ## Report format
231
+
232
+ Save the report as `.uncoded/reviews/YYYY-MM-DD-HHMMSS.md`, using today's date
233
+ and current time (timestamped to preserve multiple runs on the same day).
234
+ Create the directory if it does not exist.
235
+
236
+ Use this structure:
237
+
238
+ ```markdown
239
+ # Coherence Review — <repo name>
240
+
241
+ **Date:** YYYY-MM-DD
242
+ **Symbols indexed:** N
243
+ **Sweeps run:** lexical, promissory, structural
244
+ **Findings:** N total (N lexical · N promissory · N structural)
245
+
246
+ ## Priority regions
247
+
248
+ Regions with two or more findings — examine these first:
249
+
250
+ - `path/to/file.py` — N findings
251
+ - ...
252
+
253
+ *(Omit this section if no region has more than one finding.)*
254
+
255
+ ## Findings
256
+
257
+ ### 1 · <symptom summary>
258
+
259
+ **Category:** lexical | promissory | structural
260
+ **Symptom:** concept-duplication | qualifier-accretion | vocabulary-island |
261
+ collision-with-drift | name-signature-mismatch | docstring-signature-mismatch |
262
+ docstring-name-mismatch | defensive-docstring | god-module |
263
+ boundary-violation | cross-vocabulary-import | zero-caller
264
+ **Location:** `path/to/file.py` · `ClassName/method_name`
265
+ **Confidence:** high | medium | low
266
+
267
+ **Evidence:**
268
+ > Verbatim quote from namespace.yaml, stub, or import statement.
269
+
270
+ One or two sentences describing the inconsistency. Not a diagnosis. Not a fix.
271
+
272
+ ---
273
+
274
+ ### 2 · ...
275
+ ```
276
+
277
+ ## Principles
278
+
279
+ **Coverage, not filtering.** Report every finding at its confidence level. Do
280
+ not silently drop findings judged low-severity. A low-confidence finding with
281
+ clear evidence is useful — the human can filter. A dropped finding is not.
282
+
283
+ **Confidence is part of the finding, not a gate.**
284
+
285
+ - `high` — the inconsistency is explicit; evidence is directly in the stub or
286
+ namespace
287
+ - `medium` — strongly implied but depends on judgement about intent
288
+ - `low` — pattern-based suspicion that needs human interpretation
289
+
290
+ **Evidence must be verbatim.** Quote the relevant namespace line, stub excerpt,
291
+ or import statement exactly. A finding the human cannot quickly verify is worse
292
+ than no finding.
293
+
294
+ **One finding per inconsistency.** If a single symbol has a name–signature
295
+ mismatch and a docstring–name mismatch, that is two findings on the same
296
+ symbol, not one combined finding. Let the report show the density.
297
+
298
+ **Do not propose fixes.** No renaming suggestions, no refactoring proposals, no
299
+ "this should be moved to". The finding describes what is inconsistent. The
300
+ human owns remediation.
301
+
302
+ **Do not flag style.** Docstring format, type annotation style, import ordering,
303
+ naming conventions — out of scope. Coherence is about semantic consistency, not
304
+ surface consistency.
305
+
306
+ **Do not fabricate.** Every finding must be anchored to code you actually
307
+ examined. If a sweep suggests a pattern but you cannot find concrete instances,
308
+ do not include it.
309
+
310
+ ## Scope control
311
+
312
+ If the codebase has more than ~1000 public symbols:
313
+
314
+ 1. Complete the lexical sweep in full (the namespace is compact enough).
315
+ 2. For the promissory sweep, prioritise: core domain modules (identified from
316
+ the namespace structure), any module that appeared in a lexical finding, and
317
+ a representative sample of the rest (~30% of remaining stubs).
318
+ 3. For the structural sweep, focus on the module and package level first,
319
+ descending to individual symbols only where the higher-level scan raised
320
+ flags.
321
+
322
+ Note the scope chosen in the report summary so the human knows what was and was
323
+ not examined.
324
+
325
+ ## Examples
326
+
327
+ <example>
328
+ <scenario>Three public functions: `fetch_user(id)`, `get_user_by_id(id)`,
329
+ `load_user(user_id)`, in three different files, all returning a User and all
330
+ doing substantively the same lookup.</scenario>
331
+ <flag>Yes. Concept duplication, high confidence. Evidence: three stub excerpts
332
+ quoted verbatim.</flag>
333
+ </example>
334
+
335
+ <example>
336
+ <scenario>A function `validate_user(user)` whose signature returns `User` rather
337
+ than `bool` or `None`, and whose docstring says "Validates and returns the user
338
+ if valid, raising UserValidationError otherwise."</scenario>
339
+ <flag>Yes. Name–signature mismatch, medium confidence. The name reads as a
340
+ predicate but the behaviour is a validator-filter. Stub quoted as
341
+ evidence.</flag>
342
+ </example>
343
+
344
+ <example>
345
+ <scenario>Two modules, `storage/cache.py` and `runtime/memoize.py`, both
346
+ defining functions that wrap callables with LRU caching, with slightly different
347
+ cache-size defaults.</scenario>
348
+ <flag>Yes. Concept duplication, medium confidence. Both stubs quoted. Note the
349
+ difference — the human may determine it is intentional.</flag>
350
+ </example>
351
+
352
+ <example>
353
+ <scenario>A class `OrderProcessor` with 34 methods spanning order creation,
354
+ validation, payment capture, fulfilment dispatch, refund handling, and
355
+ reporting.</scenario>
356
+ <flag>Yes. God module, high confidence. Method list quoted from
357
+ namespace.</flag>
358
+ </example>
359
+
360
+ <example>
361
+ <scenario>A function uses `x` as a parameter name in a mathematical formula
362
+ module.</scenario>
363
+ <flag>No. Short variable names in mathematical contexts are conventional and do
364
+ not indicate drift.</flag>
365
+ </example>
366
+
367
+ <example>
368
+ <scenario>A function is 80 lines of non-trivial logic.</scenario>
369
+ <flag>No, not by itself. Complexity is not incoherence. Flag only if the
370
+ complexity manifests as inconsistency — e.g. the function's behaviour has
371
+ drifted from what its name or docstring promise.</flag>
372
+ </example>
373
+
374
+ <example>
375
+ <scenario>The codebase uses both `Optional[X]` and `X | None` in different
376
+ files.</scenario>
377
+ <flag>No. Both are valid Python and mean the same thing. Flag only if the
378
+ semantics of absence differ — e.g. some functions return None on failure while
379
+ others raise, for the same kind of operation.</flag>
380
+ </example>
381
+ """
382
+
383
+
384
+ def sync_skill(*, check: bool) -> bool:
385
+ """Write the coherence-review skill file to all supported agent locations."""
386
+ results = [sync_file(path, _SKILL_CONTENT, check=check) for path in SKILL_OUTPUTS]
387
+ results.extend(remove_file(path, check=check) for path in LEGACY_SKILL_OUTPUTS)
388
+ return any(results)