invar-tools 1.8.0__py3-none-any.whl → 1.11.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.
- invar/__init__.py +8 -0
- invar/core/doc_edit.py +187 -0
- invar/core/doc_parser.py +563 -0
- invar/core/language.py +88 -0
- invar/core/models.py +106 -0
- invar/core/patterns/detector.py +6 -1
- invar/core/patterns/p0_exhaustive.py +15 -3
- invar/core/patterns/p0_literal.py +15 -3
- invar/core/patterns/p0_newtype.py +15 -3
- invar/core/patterns/p0_nonempty.py +15 -3
- invar/core/patterns/p0_validation.py +15 -3
- invar/core/patterns/registry.py +5 -1
- invar/core/patterns/types.py +5 -1
- invar/core/property_gen.py +4 -0
- invar/core/rules.py +84 -18
- invar/core/sync_helpers.py +27 -1
- invar/core/ts_parsers.py +286 -0
- invar/core/ts_sig_parser.py +310 -0
- invar/mcp/handlers.py +408 -0
- invar/mcp/server.py +288 -143
- invar/node_tools/MANIFEST +7 -0
- invar/node_tools/__init__.py +51 -0
- invar/node_tools/fc-runner/cli.js +77 -0
- invar/node_tools/quick-check/cli.js +28 -0
- invar/node_tools/ts-analyzer/cli.js +480 -0
- invar/shell/claude_hooks.py +35 -12
- invar/shell/commands/doc.py +409 -0
- invar/shell/commands/guard.py +41 -1
- invar/shell/commands/init.py +154 -16
- invar/shell/commands/perception.py +157 -33
- invar/shell/commands/skill.py +187 -0
- invar/shell/commands/template_sync.py +65 -13
- invar/shell/commands/uninstall.py +60 -12
- invar/shell/commands/update.py +6 -14
- invar/shell/contract_coverage.py +1 -0
- invar/shell/doc_tools.py +459 -0
- invar/shell/fs.py +67 -13
- invar/shell/pi_hooks.py +6 -0
- invar/shell/prove/crosshair.py +3 -0
- invar/shell/prove/guard_ts.py +902 -0
- invar/shell/skill_manager.py +355 -0
- invar/shell/template_engine.py +28 -4
- invar/shell/templates.py +4 -4
- invar/templates/claude-md/python/critical-rules.md +33 -0
- invar/templates/claude-md/python/quick-reference.md +24 -0
- invar/templates/claude-md/typescript/critical-rules.md +40 -0
- invar/templates/claude-md/typescript/quick-reference.md +24 -0
- invar/templates/claude-md/universal/check-in.md +25 -0
- invar/templates/claude-md/universal/skills.md +73 -0
- invar/templates/claude-md/universal/workflow.md +55 -0
- invar/templates/commands/{audit.md → audit.md.jinja} +18 -1
- invar/templates/config/AGENT.md.jinja +58 -0
- invar/templates/config/CLAUDE.md.jinja +16 -209
- invar/templates/config/context.md.jinja +19 -0
- invar/templates/examples/{README.md → python/README.md} +2 -0
- invar/templates/examples/{conftest.py → python/conftest.py} +1 -1
- invar/templates/examples/{contracts.py → python/contracts.py} +81 -4
- invar/templates/examples/python/core_shell.py +227 -0
- invar/templates/examples/python/functional.py +613 -0
- invar/templates/examples/typescript/README.md +31 -0
- invar/templates/examples/typescript/contracts.ts +163 -0
- invar/templates/examples/typescript/core_shell.ts +374 -0
- invar/templates/examples/typescript/functional.ts +601 -0
- invar/templates/examples/typescript/workflow.md +95 -0
- invar/templates/hooks/PostToolUse.sh.jinja +10 -1
- invar/templates/hooks/PreToolUse.sh.jinja +38 -0
- invar/templates/hooks/Stop.sh.jinja +1 -1
- invar/templates/hooks/UserPromptSubmit.sh.jinja +7 -0
- invar/templates/hooks/pi/invar.ts.jinja +9 -0
- invar/templates/manifest.toml +7 -6
- invar/templates/onboard/assessment.md.jinja +214 -0
- invar/templates/onboard/patterns/python.md +347 -0
- invar/templates/onboard/patterns/typescript.md +452 -0
- invar/templates/onboard/roadmap.md.jinja +168 -0
- invar/templates/protocol/INVAR.md.jinja +51 -0
- invar/templates/protocol/python/architecture-examples.md +41 -0
- invar/templates/protocol/python/contracts-syntax.md +56 -0
- invar/templates/protocol/python/markers.md +44 -0
- invar/templates/protocol/python/tools.md +24 -0
- invar/templates/protocol/python/troubleshooting.md +38 -0
- invar/templates/protocol/typescript/architecture-examples.md +52 -0
- invar/templates/protocol/typescript/contracts-syntax.md +73 -0
- invar/templates/protocol/typescript/markers.md +48 -0
- invar/templates/protocol/typescript/tools.md +65 -0
- invar/templates/protocol/typescript/troubleshooting.md +104 -0
- invar/templates/protocol/universal/architecture.md +36 -0
- invar/templates/protocol/universal/completion.md +14 -0
- invar/templates/protocol/universal/contracts-concept.md +37 -0
- invar/templates/protocol/universal/header.md +17 -0
- invar/templates/protocol/universal/session.md +17 -0
- invar/templates/protocol/universal/six-laws.md +10 -0
- invar/templates/protocol/universal/usbv.md +14 -0
- invar/templates/protocol/universal/visible-workflow.md +25 -0
- invar/templates/skills/develop/SKILL.md.jinja +85 -3
- invar/templates/skills/extensions/_registry.yaml +93 -0
- invar/templates/skills/extensions/acceptance/SKILL.md +383 -0
- invar/templates/skills/extensions/invar-onboard/SKILL.md +448 -0
- invar/templates/skills/extensions/invar-onboard/patterns/python.md +347 -0
- invar/templates/skills/extensions/invar-onboard/patterns/typescript.md +452 -0
- invar/templates/skills/extensions/invar-onboard/templates/assessment.md.jinja +214 -0
- invar/templates/skills/extensions/invar-onboard/templates/roadmap.md.jinja +168 -0
- invar/templates/skills/extensions/security/SKILL.md +382 -0
- invar/templates/skills/extensions/security/patterns/_common.yaml +126 -0
- invar/templates/skills/extensions/security/patterns/python.yaml +155 -0
- invar/templates/skills/extensions/security/patterns/typescript.yaml +194 -0
- invar/templates/skills/review/SKILL.md.jinja +220 -248
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/METADATA +336 -12
- invar_tools-1.11.0.dist-info/RECORD +178 -0
- invar/templates/examples/core_shell.py +0 -127
- invar/templates/protocol/INVAR.md +0 -310
- invar_tools-1.8.0.dist-info/RECORD +0 -116
- /invar/templates/examples/{workflow.md → python/workflow.md} +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -70,8 +70,12 @@ def sync_templates(path: Path, config: SyncConfig) -> Result[SyncReport, str]:
|
|
|
70
70
|
manifest = manifest_result.unwrap()
|
|
71
71
|
report = SyncReport()
|
|
72
72
|
|
|
73
|
-
# Build variables for template rendering
|
|
74
|
-
variables = {
|
|
73
|
+
# Build variables for template rendering (LX-05: include language)
|
|
74
|
+
variables = {
|
|
75
|
+
**manifest.get("variables", {}),
|
|
76
|
+
"syntax": config.syntax,
|
|
77
|
+
"language": config.language,
|
|
78
|
+
}
|
|
75
79
|
|
|
76
80
|
# Load project additions if enabled
|
|
77
81
|
project_additions = _load_project_additions(path) if config.inject_project_additions else ""
|
|
@@ -83,7 +87,12 @@ def sync_templates(path: Path, config: SyncConfig) -> Result[SyncReport, str]:
|
|
|
83
87
|
for dest_rel, src_rel in fully_managed:
|
|
84
88
|
if should_skip_file(dest_rel, config.skip_patterns):
|
|
85
89
|
continue
|
|
86
|
-
|
|
90
|
+
# Get template type from manifest (LX-05: support jinja for fully_managed)
|
|
91
|
+
template_config = manifest.get("templates", {}).get(dest_rel, {})
|
|
92
|
+
template_type = template_config.get("type", "copy")
|
|
93
|
+
result = _sync_fully_managed(
|
|
94
|
+
path, templates_dir, dest_rel, src_rel, template_type, variables, config, report
|
|
95
|
+
)
|
|
87
96
|
if isinstance(result, Failure):
|
|
88
97
|
report.errors.append(result.failure())
|
|
89
98
|
|
|
@@ -124,29 +133,44 @@ def _load_project_additions(path: Path) -> str:
|
|
|
124
133
|
return ""
|
|
125
134
|
|
|
126
135
|
|
|
127
|
-
# @shell_complexity: File I/O with multiple existence/content checks
|
|
136
|
+
# @shell_complexity: File I/O with multiple existence/content checks and Jinja rendering
|
|
128
137
|
def _sync_fully_managed(
|
|
129
138
|
path: Path,
|
|
130
139
|
templates_dir: Path,
|
|
131
140
|
dest_rel: str,
|
|
132
141
|
src_rel: str,
|
|
142
|
+
template_type: str,
|
|
143
|
+
variables: dict,
|
|
133
144
|
config: SyncConfig,
|
|
134
145
|
report: SyncReport,
|
|
135
146
|
) -> Result[str, str]:
|
|
136
|
-
"""Sync a fully managed file (direct overwrite).
|
|
147
|
+
"""Sync a fully managed file (direct overwrite).
|
|
148
|
+
|
|
149
|
+
LX-05: Now supports Jinja templates for composition.
|
|
150
|
+
"""
|
|
137
151
|
dest_file = path / dest_rel
|
|
138
152
|
src_file = templates_dir / src_rel
|
|
139
153
|
|
|
140
154
|
if not src_file.exists():
|
|
141
155
|
return Failure(f"Template not found: {src_rel}")
|
|
142
156
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
157
|
+
# LX-05: Render Jinja templates, copy plain files
|
|
158
|
+
if template_type == "jinja":
|
|
159
|
+
render_result = render_template_file(src_file, variables)
|
|
160
|
+
if isinstance(render_result, Failure):
|
|
161
|
+
return render_result
|
|
162
|
+
new_content = render_result.unwrap()
|
|
163
|
+
else:
|
|
164
|
+
try:
|
|
165
|
+
new_content = src_file.read_text()
|
|
166
|
+
except OSError as e:
|
|
167
|
+
return Failure(f"Failed to read template {src_rel}: {e}")
|
|
168
|
+
|
|
169
|
+
# Track if file exists BEFORE write (for correct created/updated reporting)
|
|
170
|
+
file_existed = dest_file.exists()
|
|
147
171
|
|
|
148
172
|
# Check if update needed
|
|
149
|
-
if
|
|
173
|
+
if file_existed and not config.force:
|
|
150
174
|
try:
|
|
151
175
|
if dest_file.read_text() == new_content:
|
|
152
176
|
report.skipped.append(dest_rel)
|
|
@@ -162,7 +186,11 @@ def _sync_fully_managed(
|
|
|
162
186
|
except OSError as e:
|
|
163
187
|
return Failure(f"Failed to write {dest_rel}: {e}")
|
|
164
188
|
|
|
165
|
-
|
|
189
|
+
# Report based on pre-write existence
|
|
190
|
+
if file_existed:
|
|
191
|
+
report.updated.append(dest_rel)
|
|
192
|
+
else:
|
|
193
|
+
report.created.append(dest_rel)
|
|
166
194
|
return Success("synced")
|
|
167
195
|
|
|
168
196
|
|
|
@@ -341,6 +369,14 @@ def _merge_region_content(
|
|
|
341
369
|
|
|
342
370
|
else:
|
|
343
371
|
# Missing: no Invar markers - preserve entire content as user content
|
|
372
|
+
# Handle empty content - just return fresh template
|
|
373
|
+
if not existing_content.strip():
|
|
374
|
+
if dest_rel == "CLAUDE.md" and project_additions:
|
|
375
|
+
parsed = parse_invar_regions(new_content)
|
|
376
|
+
if "project" in parsed.regions:
|
|
377
|
+
return reconstruct_file(parsed, {"project": project_additions})
|
|
378
|
+
return new_content
|
|
379
|
+
|
|
344
380
|
preserved = format_preserved_content(existing_content, date.today().isoformat())
|
|
345
381
|
parsed = parse_invar_regions(new_content)
|
|
346
382
|
if user_region in parsed.regions:
|
|
@@ -371,7 +407,8 @@ def _sync_create_only(
|
|
|
371
407
|
report.skipped.append(dest_rel)
|
|
372
408
|
return Success("skipped")
|
|
373
409
|
|
|
374
|
-
|
|
410
|
+
# LX-05: Skip existence check for copy_dir_lang (has {language} placeholder)
|
|
411
|
+
if template_type != "copy_dir_lang" and not src_file.exists():
|
|
375
412
|
return Failure(f"Template not found: {src_rel}")
|
|
376
413
|
|
|
377
414
|
try:
|
|
@@ -386,9 +423,24 @@ def _sync_create_only(
|
|
|
386
423
|
dest_file.write_text(result.unwrap())
|
|
387
424
|
elif template_type == "copy_dir":
|
|
388
425
|
if src_file.is_dir():
|
|
389
|
-
|
|
426
|
+
# Ignore Python bytecode and cache directories
|
|
427
|
+
ignore = shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo")
|
|
428
|
+
shutil.copytree(src_file, dest_file, ignore=ignore)
|
|
390
429
|
else:
|
|
391
430
|
return Failure(f"Expected directory: {src_rel}")
|
|
431
|
+
elif template_type == "copy_dir_lang":
|
|
432
|
+
# LX-05 hotfix: Language-aware directory copy
|
|
433
|
+
lang = variables.get("language", "python")
|
|
434
|
+
lang_src_rel = src_rel.replace("{language}", lang)
|
|
435
|
+
lang_src_file = templates_dir / lang_src_rel
|
|
436
|
+
if not lang_src_file.exists():
|
|
437
|
+
return Failure(f"Language-specific template not found: {lang_src_rel}")
|
|
438
|
+
if lang_src_file.is_dir():
|
|
439
|
+
# Ignore Python bytecode and cache directories
|
|
440
|
+
ignore = shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo")
|
|
441
|
+
shutil.copytree(lang_src_file, dest_file, ignore=ignore)
|
|
442
|
+
else:
|
|
443
|
+
return Failure(f"Expected directory: {lang_src_rel}")
|
|
392
444
|
|
|
393
445
|
report.created.append(dest_rel)
|
|
394
446
|
return Success("created")
|
|
@@ -16,6 +16,7 @@ import typer
|
|
|
16
16
|
from rich.console import Console
|
|
17
17
|
|
|
18
18
|
from invar.shell.claude_hooks import is_invar_hook
|
|
19
|
+
from invar.shell.skill_manager import CORE_SKILLS, has_user_extensions
|
|
19
20
|
|
|
20
21
|
console = Console()
|
|
21
22
|
|
|
@@ -199,13 +200,19 @@ def remove_hooks_from_settings(path: Path) -> tuple[bool, str]:
|
|
|
199
200
|
|
|
200
201
|
|
|
201
202
|
# @shell_complexity: Multi-file type detection requires comprehensive branching
|
|
202
|
-
def collect_removal_targets(path: Path) -> dict:
|
|
203
|
-
"""Collect files and directories to remove/modify.
|
|
203
|
+
def collect_removal_targets(path: Path, remove_extensions: bool = False) -> dict:
|
|
204
|
+
"""Collect files and directories to remove/modify.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
path: Project root path
|
|
208
|
+
remove_extensions: If True, also remove extension skills
|
|
209
|
+
"""
|
|
204
210
|
targets = {
|
|
205
211
|
"delete_dirs": [],
|
|
206
212
|
"delete_files": [],
|
|
207
213
|
"modify_files": [],
|
|
208
214
|
"skip": [],
|
|
215
|
+
"extensions_preserved": [], # Extension skills that will be kept
|
|
209
216
|
}
|
|
210
217
|
|
|
211
218
|
# Directories to delete entirely
|
|
@@ -222,21 +229,45 @@ def collect_removal_targets(path: Path) -> dict:
|
|
|
222
229
|
if file_path.exists():
|
|
223
230
|
targets["delete_files"].append((file_name, description))
|
|
224
231
|
|
|
225
|
-
# Skills
|
|
232
|
+
# Skills - distinguish core vs extension
|
|
226
233
|
skills_dir = path / ".claude" / "skills"
|
|
227
234
|
if skills_dir.exists():
|
|
228
235
|
for skill_dir in skills_dir.iterdir():
|
|
229
236
|
if skill_dir.is_dir():
|
|
237
|
+
skill_name = skill_dir.name
|
|
230
238
|
skill_file = skill_dir / "SKILL.md"
|
|
231
|
-
|
|
232
|
-
|
|
239
|
+
|
|
240
|
+
if not skill_file.exists():
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
# Core skills are always removed
|
|
244
|
+
if skill_name in CORE_SKILLS:
|
|
245
|
+
targets["delete_dirs"].append(
|
|
246
|
+
(f".claude/skills/{skill_name}/", "core skill")
|
|
247
|
+
)
|
|
248
|
+
elif has_invar_marker(skill_file):
|
|
249
|
+
# Extension skill with Invar marker
|
|
250
|
+
if remove_extensions:
|
|
251
|
+
has_custom = has_user_extensions(skill_dir)
|
|
252
|
+
desc = "extension skill"
|
|
253
|
+
if has_custom:
|
|
254
|
+
desc += " (has custom content)"
|
|
233
255
|
targets["delete_dirs"].append(
|
|
234
|
-
(f".claude/skills/{
|
|
256
|
+
(f".claude/skills/{skill_name}/", desc)
|
|
235
257
|
)
|
|
236
258
|
else:
|
|
237
|
-
|
|
238
|
-
|
|
259
|
+
# Preserve extension skill
|
|
260
|
+
has_custom = has_user_extensions(skill_dir)
|
|
261
|
+
desc = "extension skill"
|
|
262
|
+
if has_custom:
|
|
263
|
+
desc += ", has custom content"
|
|
264
|
+
targets["extensions_preserved"].append(
|
|
265
|
+
(f".claude/skills/{skill_name}/", desc)
|
|
239
266
|
)
|
|
267
|
+
else:
|
|
268
|
+
targets["skip"].append(
|
|
269
|
+
(f".claude/skills/{skill_name}/", "no _invar marker")
|
|
270
|
+
)
|
|
240
271
|
|
|
241
272
|
# Commands with _invar marker
|
|
242
273
|
commands_dir = path / ".claude" / "commands"
|
|
@@ -358,6 +389,14 @@ def show_preview(targets: dict) -> None:
|
|
|
358
389
|
for item, desc in targets["modify_files"]:
|
|
359
390
|
console.print(f" {item:40} ({desc})")
|
|
360
391
|
|
|
392
|
+
if targets.get("extensions_preserved"):
|
|
393
|
+
console.print("\n[cyan]Will PRESERVE (extension skills):[/cyan]")
|
|
394
|
+
for item, desc in targets["extensions_preserved"]:
|
|
395
|
+
console.print(f" {item:40} ({desc})")
|
|
396
|
+
console.print(
|
|
397
|
+
"\n[dim]Use --remove-extensions to also remove extension skills[/dim]"
|
|
398
|
+
)
|
|
399
|
+
|
|
361
400
|
if targets["skip"]:
|
|
362
401
|
console.print("\n[dim]Will SKIP:[/dim]")
|
|
363
402
|
for item, desc in targets["skip"]:
|
|
@@ -446,16 +485,25 @@ def uninstall(
|
|
|
446
485
|
"-f",
|
|
447
486
|
help="Skip confirmation prompt",
|
|
448
487
|
),
|
|
488
|
+
remove_extensions: bool = typer.Option(
|
|
489
|
+
False,
|
|
490
|
+
"--remove-extensions",
|
|
491
|
+
help="Also remove extension skills (security, acceptance, etc.)",
|
|
492
|
+
),
|
|
449
493
|
) -> None:
|
|
450
494
|
"""Remove Invar from a project.
|
|
451
495
|
|
|
452
496
|
Safely removes Invar-generated files and configurations while
|
|
453
497
|
preserving user content. Uses marker-based detection.
|
|
454
498
|
|
|
499
|
+
By default, extension skills are preserved. Use --remove-extensions
|
|
500
|
+
to also remove them.
|
|
501
|
+
|
|
455
502
|
Examples:
|
|
456
|
-
invar uninstall --dry-run
|
|
457
|
-
invar uninstall
|
|
458
|
-
invar uninstall --force
|
|
503
|
+
invar uninstall --dry-run # Preview changes
|
|
504
|
+
invar uninstall # Remove with confirmation
|
|
505
|
+
invar uninstall --force # Remove without confirmation
|
|
506
|
+
invar uninstall --remove-extensions # Also remove extension skills
|
|
459
507
|
"""
|
|
460
508
|
# Check if this is an Invar project
|
|
461
509
|
invar_toml = path / "invar.toml"
|
|
@@ -468,7 +516,7 @@ def uninstall(
|
|
|
468
516
|
raise typer.Exit(1)
|
|
469
517
|
|
|
470
518
|
# Collect targets
|
|
471
|
-
targets = collect_removal_targets(path)
|
|
519
|
+
targets = collect_removal_targets(path, remove_extensions=remove_extensions)
|
|
472
520
|
|
|
473
521
|
# Check if there's anything to do
|
|
474
522
|
if not any([targets["delete_dirs"], targets["delete_files"], targets["modify_files"]]):
|
invar/shell/commands/update.py
CHANGED
|
@@ -19,9 +19,7 @@ console = Console()
|
|
|
19
19
|
|
|
20
20
|
def update(
|
|
21
21
|
path: Path = typer.Argument(Path(), help="Project root directory"),
|
|
22
|
-
|
|
23
|
-
force: bool = typer.Option(False, "--force", "-f", help="Update even if current"),
|
|
24
|
-
yes: bool = typer.Option(False, "--yes", "-y", help="Accept defaults without prompting"),
|
|
22
|
+
preview: bool = typer.Option(False, "--preview", "--check", help="Preview changes (dry run)"),
|
|
25
23
|
) -> None:
|
|
26
24
|
"""
|
|
27
25
|
Alias for 'invar init' (DX-55).
|
|
@@ -29,20 +27,14 @@ def update(
|
|
|
29
27
|
Maintained for backwards compatibility.
|
|
30
28
|
Both commands are now idempotent and do the same thing.
|
|
31
29
|
|
|
32
|
-
Use 'invar init --
|
|
33
|
-
Use 'invar init --force' to refresh even if current.
|
|
30
|
+
Use 'invar init --preview' to preview changes.
|
|
34
31
|
"""
|
|
35
32
|
console.print("[dim]Note: 'update' is now an alias for 'init'[/dim]")
|
|
36
|
-
#
|
|
33
|
+
# Call init with matching parameters (DX-70 signature)
|
|
37
34
|
return init_command(
|
|
38
35
|
path=path,
|
|
39
36
|
claude=False,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
skills=True,
|
|
44
|
-
yes=yes,
|
|
45
|
-
check=check,
|
|
46
|
-
force=force,
|
|
47
|
-
reset=False,
|
|
37
|
+
pi=False,
|
|
38
|
+
language=None,
|
|
39
|
+
preview=preview,
|
|
48
40
|
)
|
invar/shell/contract_coverage.py
CHANGED
|
@@ -266,6 +266,7 @@ def detect_batch_creation(
|
|
|
266
266
|
return Success(None)
|
|
267
267
|
|
|
268
268
|
|
|
269
|
+
# @shell_orchestration: Report formatting for CLI output display
|
|
269
270
|
# @shell_complexity: Report formatting with multiple conditional sections
|
|
270
271
|
def format_contract_coverage_report(report: ContractCoverageReport) -> str:
|
|
271
272
|
"""Format coverage report for human-readable output."""
|