scc-cli 1.4.1__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.

Potentially problematic release.


This version of scc-cli might be problematic. Click here for more details.

Files changed (113) hide show
  1. scc_cli/__init__.py +15 -0
  2. scc_cli/audit/__init__.py +37 -0
  3. scc_cli/audit/parser.py +191 -0
  4. scc_cli/audit/reader.py +180 -0
  5. scc_cli/auth.py +145 -0
  6. scc_cli/claude_adapter.py +485 -0
  7. scc_cli/cli.py +259 -0
  8. scc_cli/cli_admin.py +706 -0
  9. scc_cli/cli_audit.py +245 -0
  10. scc_cli/cli_common.py +166 -0
  11. scc_cli/cli_config.py +527 -0
  12. scc_cli/cli_exceptions.py +705 -0
  13. scc_cli/cli_helpers.py +244 -0
  14. scc_cli/cli_init.py +272 -0
  15. scc_cli/cli_launch.py +1454 -0
  16. scc_cli/cli_org.py +1428 -0
  17. scc_cli/cli_support.py +322 -0
  18. scc_cli/cli_team.py +892 -0
  19. scc_cli/cli_worktree.py +865 -0
  20. scc_cli/config.py +583 -0
  21. scc_cli/console.py +562 -0
  22. scc_cli/constants.py +79 -0
  23. scc_cli/contexts.py +377 -0
  24. scc_cli/deprecation.py +54 -0
  25. scc_cli/deps.py +189 -0
  26. scc_cli/docker/__init__.py +127 -0
  27. scc_cli/docker/core.py +466 -0
  28. scc_cli/docker/credentials.py +726 -0
  29. scc_cli/docker/launch.py +604 -0
  30. scc_cli/doctor/__init__.py +99 -0
  31. scc_cli/doctor/checks.py +1074 -0
  32. scc_cli/doctor/render.py +346 -0
  33. scc_cli/doctor/types.py +66 -0
  34. scc_cli/errors.py +288 -0
  35. scc_cli/evaluation/__init__.py +27 -0
  36. scc_cli/evaluation/apply_exceptions.py +207 -0
  37. scc_cli/evaluation/evaluate.py +97 -0
  38. scc_cli/evaluation/models.py +80 -0
  39. scc_cli/exit_codes.py +55 -0
  40. scc_cli/git.py +1521 -0
  41. scc_cli/json_command.py +166 -0
  42. scc_cli/json_output.py +96 -0
  43. scc_cli/kinds.py +62 -0
  44. scc_cli/marketplace/__init__.py +123 -0
  45. scc_cli/marketplace/adapter.py +74 -0
  46. scc_cli/marketplace/compute.py +377 -0
  47. scc_cli/marketplace/constants.py +87 -0
  48. scc_cli/marketplace/managed.py +135 -0
  49. scc_cli/marketplace/materialize.py +723 -0
  50. scc_cli/marketplace/normalize.py +548 -0
  51. scc_cli/marketplace/render.py +257 -0
  52. scc_cli/marketplace/resolve.py +459 -0
  53. scc_cli/marketplace/schema.py +506 -0
  54. scc_cli/marketplace/sync.py +260 -0
  55. scc_cli/marketplace/team_cache.py +195 -0
  56. scc_cli/marketplace/team_fetch.py +688 -0
  57. scc_cli/marketplace/trust.py +244 -0
  58. scc_cli/models/__init__.py +41 -0
  59. scc_cli/models/exceptions.py +273 -0
  60. scc_cli/models/plugin_audit.py +434 -0
  61. scc_cli/org_templates.py +269 -0
  62. scc_cli/output_mode.py +167 -0
  63. scc_cli/panels.py +113 -0
  64. scc_cli/platform.py +350 -0
  65. scc_cli/profiles.py +960 -0
  66. scc_cli/remote.py +443 -0
  67. scc_cli/schemas/__init__.py +1 -0
  68. scc_cli/schemas/org-v1.schema.json +456 -0
  69. scc_cli/schemas/team-config.v1.schema.json +163 -0
  70. scc_cli/sessions.py +425 -0
  71. scc_cli/setup.py +588 -0
  72. scc_cli/source_resolver.py +470 -0
  73. scc_cli/stats.py +378 -0
  74. scc_cli/stores/__init__.py +13 -0
  75. scc_cli/stores/exception_store.py +251 -0
  76. scc_cli/subprocess_utils.py +88 -0
  77. scc_cli/teams.py +382 -0
  78. scc_cli/templates/__init__.py +2 -0
  79. scc_cli/templates/org/__init__.py +0 -0
  80. scc_cli/templates/org/minimal.json +19 -0
  81. scc_cli/templates/org/reference.json +74 -0
  82. scc_cli/templates/org/strict.json +38 -0
  83. scc_cli/templates/org/teams.json +42 -0
  84. scc_cli/templates/statusline.sh +75 -0
  85. scc_cli/theme.py +348 -0
  86. scc_cli/ui/__init__.py +124 -0
  87. scc_cli/ui/branding.py +68 -0
  88. scc_cli/ui/chrome.py +395 -0
  89. scc_cli/ui/dashboard/__init__.py +62 -0
  90. scc_cli/ui/dashboard/_dashboard.py +677 -0
  91. scc_cli/ui/dashboard/loaders.py +395 -0
  92. scc_cli/ui/dashboard/models.py +184 -0
  93. scc_cli/ui/dashboard/orchestrator.py +390 -0
  94. scc_cli/ui/formatters.py +443 -0
  95. scc_cli/ui/gate.py +350 -0
  96. scc_cli/ui/help.py +157 -0
  97. scc_cli/ui/keys.py +538 -0
  98. scc_cli/ui/list_screen.py +431 -0
  99. scc_cli/ui/picker.py +700 -0
  100. scc_cli/ui/prompts.py +200 -0
  101. scc_cli/ui/wizard.py +675 -0
  102. scc_cli/update.py +680 -0
  103. scc_cli/utils/__init__.py +39 -0
  104. scc_cli/utils/fixit.py +264 -0
  105. scc_cli/utils/fuzzy.py +124 -0
  106. scc_cli/utils/locks.py +101 -0
  107. scc_cli/utils/ttl.py +376 -0
  108. scc_cli/validate.py +455 -0
  109. scc_cli-1.4.1.dist-info/METADATA +369 -0
  110. scc_cli-1.4.1.dist-info/RECORD +113 -0
  111. scc_cli-1.4.1.dist-info/WHEEL +4 -0
  112. scc_cli-1.4.1.dist-info/entry_points.txt +2 -0
  113. scc_cli-1.4.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,166 @@
1
+ """
2
+ JSON command decorator for first-class JSON output support.
3
+
4
+ Provide a decorator that wraps command output in JSON envelopes.
5
+
6
+ IMPORTANT: Commands using this decorator MUST explicitly declare these parameters:
7
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON")
8
+ pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON")
9
+
10
+ Usage:
11
+ from scc_cli.json_command import json_command
12
+ from scc_cli.kinds import Kind
13
+
14
+ @team_app.command("list")
15
+ @json_command(Kind.TEAM_LIST)
16
+ def team_list(
17
+ json_output: bool = typer.Option(False, "--json"),
18
+ pretty: bool = typer.Option(False, "--pretty"),
19
+ ) -> dict:
20
+ # Return data dict, decorator handles envelope
21
+ return {"teams": [...]}
22
+ """
23
+
24
+ from collections.abc import Callable
25
+ from functools import wraps
26
+ from typing import Any, TypeVar
27
+
28
+ import typer
29
+
30
+ from .errors import ConfigError, PolicyViolationError, PrerequisiteError, SCCError
31
+ from .exit_codes import (
32
+ EXIT_CANCELLED,
33
+ EXIT_CONFIG,
34
+ EXIT_ERROR,
35
+ EXIT_GOVERNANCE,
36
+ EXIT_PREREQ,
37
+ EXIT_SUCCESS,
38
+ EXIT_VALIDATION,
39
+ )
40
+ from .json_output import build_envelope
41
+ from .kinds import Kind
42
+ from .output_mode import _pretty_mode, json_command_mode, json_output_mode, print_json
43
+
44
+ # Type variable for decorated functions
45
+ F = TypeVar("F", bound=Callable[..., Any])
46
+
47
+
48
+ # ═══════════════════════════════════════════════════════════════════════════════
49
+ # Exception to Exit Code Mapping
50
+ # ═══════════════════════════════════════════════════════════════════════════════
51
+
52
+
53
+ def _get_exit_code_for_exception(exc: Exception) -> int:
54
+ """Map exception types to exit codes."""
55
+ if isinstance(exc, PolicyViolationError):
56
+ return EXIT_GOVERNANCE
57
+ if isinstance(exc, PrerequisiteError):
58
+ return EXIT_PREREQ
59
+ if isinstance(exc, ConfigError):
60
+ return EXIT_CONFIG
61
+ if isinstance(exc, SCCError):
62
+ # Check exit_code attribute if available
63
+ return getattr(exc, "exit_code", EXIT_ERROR)
64
+ # Check for validation-like errors by name
65
+ if "Validation" in type(exc).__name__:
66
+ return EXIT_VALIDATION
67
+ return EXIT_ERROR
68
+
69
+
70
+ # ═══════════════════════════════════════════════════════════════════════════════
71
+ # JSON Command Decorator
72
+ # ═══════════════════════════════════════════════════════════════════════════════
73
+
74
+
75
+ def json_command(kind: Kind) -> Callable[[F], F]:
76
+ """Decorator for commands with --json support.
77
+
78
+ This decorator:
79
+ 1. Checks for json_output and pretty parameters in kwargs
80
+ 2. Enters json_output_mode() when json_output=True
81
+ 3. Builds the envelope with the correct kind
82
+ 4. Catches exceptions and maps to exit codes
83
+ 5. Prints JSON output and exits with appropriate code
84
+
85
+ IMPORTANT: The decorated function MUST explicitly declare these parameters:
86
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON")
87
+ pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON")
88
+
89
+ Args:
90
+ kind: The JSON envelope kind for this command
91
+
92
+ Returns:
93
+ A decorator function
94
+
95
+ Usage:
96
+ @team_app.command("list")
97
+ @json_command(Kind.TEAM_LIST)
98
+ def team_list(
99
+ json_output: bool = typer.Option(False, "--json"),
100
+ pretty: bool = typer.Option(False, "--pretty"),
101
+ ) -> dict:
102
+ return {"teams": [...]}
103
+
104
+ Note:
105
+ The decorated function should return a dict that becomes
106
+ the "data" field in the JSON envelope. When not in JSON mode,
107
+ the function runs normally (for human-readable output).
108
+ """
109
+
110
+ def decorator(func: F) -> F:
111
+ @wraps(func)
112
+ def wrapper(**kwargs: Any) -> Any:
113
+ # Extract json_output and pretty from kwargs
114
+ json_output = kwargs.pop("json_output", False)
115
+ pretty = kwargs.pop("pretty", False)
116
+
117
+ # --pretty implies --json
118
+ if pretty:
119
+ json_output = True
120
+ _pretty_mode.set(True)
121
+
122
+ if json_output:
123
+ with json_output_mode(), json_command_mode():
124
+ try:
125
+ # Call the wrapped function
126
+ result = func(**kwargs)
127
+
128
+ # Build and print success envelope
129
+ envelope = build_envelope(kind, data=result or {})
130
+ print_json(envelope)
131
+ raise typer.Exit(EXIT_SUCCESS)
132
+
133
+ except typer.Exit:
134
+ # Re-raise typer exits (including our own)
135
+ raise
136
+
137
+ except KeyboardInterrupt:
138
+ envelope = build_envelope(
139
+ kind,
140
+ data={},
141
+ ok=False,
142
+ errors=["Cancelled"],
143
+ )
144
+ print_json(envelope)
145
+ raise typer.Exit(EXIT_CANCELLED)
146
+
147
+ except Exception as e:
148
+ # Map exception to exit code
149
+ exit_code = _get_exit_code_for_exception(e)
150
+
151
+ # Build and print error envelope
152
+ envelope = build_envelope(
153
+ kind,
154
+ data={},
155
+ ok=False,
156
+ errors=[str(e)],
157
+ )
158
+ print_json(envelope)
159
+ raise typer.Exit(exit_code)
160
+ else:
161
+ # Normal human-readable mode
162
+ return func(**kwargs)
163
+
164
+ return wrapper # type: ignore[return-value]
165
+
166
+ return decorator
scc_cli/json_output.py ADDED
@@ -0,0 +1,96 @@
1
+ """
2
+ JSON envelope builder for CLI output.
3
+
4
+ Provide structured, versioned JSON envelopes for machine-readable output.
5
+ All JSON output MUST use this builder to ensure consistency.
6
+
7
+ Usage:
8
+ from scc_cli.json_output import build_envelope
9
+ from scc_cli.kinds import Kind
10
+
11
+ envelope = build_envelope(Kind.TEAM_LIST, data={"teams": [...]})
12
+ """
13
+
14
+ from datetime import datetime, timezone
15
+ from typing import Any
16
+
17
+ from . import __version__
18
+ from .kinds import Kind
19
+
20
+ # ═══════════════════════════════════════════════════════════════════════════════
21
+ # Constants
22
+ # ═══════════════════════════════════════════════════════════════════════════════
23
+
24
+ API_VERSION = "scc.cli/v1"
25
+
26
+
27
+ # ═══════════════════════════════════════════════════════════════════════════════
28
+ # Envelope Builder
29
+ # ═══════════════════════════════════════════════════════════════════════════════
30
+
31
+
32
+ def build_envelope(
33
+ kind: Kind,
34
+ *,
35
+ data: dict[str, Any] | None = None,
36
+ ok: bool = True,
37
+ errors: list[str] | None = None,
38
+ warnings: list[str] | None = None,
39
+ ) -> dict[str, Any]:
40
+ """Build a JSON envelope with standard structure.
41
+
42
+ All JSON output follows this contract for consistency and parseability.
43
+
44
+ Args:
45
+ kind: The envelope kind (from Kind enum)
46
+ data: The command-specific payload
47
+ ok: Whether the operation was successful
48
+ errors: List of error messages (sets ok=False if non-empty)
49
+ warnings: List of warning messages
50
+
51
+ Returns:
52
+ A structured envelope dict ready for JSON serialization:
53
+ {
54
+ "apiVersion": "scc.cli/v1",
55
+ "kind": "TeamList",
56
+ "metadata": {
57
+ "generatedAt": "2025-12-23T10:00:00Z",
58
+ "cliVersion": "1.2.3"
59
+ },
60
+ "status": {
61
+ "ok": true,
62
+ "errors": [],
63
+ "warnings": []
64
+ },
65
+ "data": { ... }
66
+ }
67
+ """
68
+ # Normalize optional parameters
69
+ if data is None:
70
+ data = {}
71
+ if errors is None:
72
+ errors = []
73
+ if warnings is None:
74
+ warnings = []
75
+
76
+ # If errors provided, ok should be False
77
+ if errors and ok:
78
+ ok = False
79
+
80
+ # Generate ISO 8601 timestamp in UTC
81
+ generated_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
82
+
83
+ return {
84
+ "apiVersion": API_VERSION,
85
+ "kind": str(kind.value) if hasattr(kind, "value") else str(kind),
86
+ "metadata": {
87
+ "generatedAt": generated_at,
88
+ "cliVersion": __version__,
89
+ },
90
+ "status": {
91
+ "ok": ok,
92
+ "errors": errors,
93
+ "warnings": warnings,
94
+ },
95
+ "data": data,
96
+ }
scc_cli/kinds.py ADDED
@@ -0,0 +1,62 @@
1
+ """Define centralized JSON envelope kind names to prevent drift.
2
+
3
+ Define all JSON envelope `kind` values here as enum members.
4
+ This prevents inconsistencies like "TeamList" vs "TeamsList" across the codebase.
5
+
6
+ Usage:
7
+ from scc_cli.kinds import Kind
8
+
9
+ envelope = build_envelope(Kind.TEAM_LIST, data={...})
10
+ """
11
+
12
+ from enum import Enum
13
+
14
+
15
+ class Kind(str, Enum):
16
+ """Define JSON envelope kind identifiers.
17
+
18
+ Inherit from str so enum values serialize directly to JSON without .value.
19
+ Add new kinds here to ensure consistency across all commands.
20
+ """
21
+
22
+ # Team commands
23
+ TEAM_LIST = "TeamList"
24
+ TEAM_INFO = "TeamInfo"
25
+ TEAM_CURRENT = "TeamCurrent"
26
+ TEAM_SWITCH = "TeamSwitch"
27
+ TEAM_VALIDATE = "TeamValidate"
28
+
29
+ # Status/Doctor
30
+ STATUS = "Status"
31
+ DOCTOR_REPORT = "DoctorReport"
32
+
33
+ # Worktree commands
34
+ WORKTREE_LIST = "WorktreeList"
35
+ WORKTREE_CREATE = "WorktreeCreate"
36
+ WORKTREE_REMOVE = "WorktreeRemove"
37
+
38
+ # Session/Container
39
+ SESSION_LIST = "SessionList"
40
+ CONTAINER_LIST = "ContainerList"
41
+
42
+ # Org admin
43
+ ORG_VALIDATION = "OrgValidation"
44
+ ORG_SCHEMA = "OrgSchema"
45
+ ORG_STATUS = "OrgStatus"
46
+ ORG_IMPORT = "OrgImport"
47
+ ORG_IMPORT_PREVIEW = "OrgImportPreview"
48
+ ORG_INIT = "OrgInit"
49
+ ORG_TEMPLATE_LIST = "OrgTemplateList"
50
+ ORG_UPDATE = "OrgUpdate"
51
+
52
+ # Support
53
+ SUPPORT_BUNDLE = "SupportBundle"
54
+
55
+ # Config
56
+ CONFIG_EXPLAIN = "ConfigExplain"
57
+
58
+ # Start
59
+ START_DRY_RUN = "StartDryRun"
60
+
61
+ # Init
62
+ INIT_RESULT = "InitResult"
@@ -0,0 +1,123 @@
1
+ """
2
+ Marketplace and plugin management for SCC.
3
+
4
+ This package provides organization-level plugin governance with:
5
+ - Multi-source marketplace definitions (GitHub, Git, URL, directory)
6
+ - Team-based plugin sets with org defaults inheritance
7
+ - Security policies (blocked plugins with audit trail)
8
+ - Project-local materialization for Docker sandbox compatibility
9
+
10
+ Public API:
11
+ - Schema models: OrgConfig, MarketplaceSource, TeamProfile, SecurityConfig
12
+ - Constants: IMPLICIT_MARKETPLACES, EXIT_CODES
13
+ - Normalization: normalize_plugin(), matches_pattern()
14
+ - Computation: compute_effective_plugins(), EffectivePlugins
15
+ - Materialization: materialize_marketplace(), MaterializedMarketplace
16
+ - Settings: render_settings(), merge_settings()
17
+ - State: ManagedState, load_managed_state(), save_managed_state()
18
+
19
+ Example:
20
+ >>> from scc_cli.marketplace import OrgConfig, compute_effective_plugins
21
+ >>> config = OrgConfig.model_validate(json_data)
22
+ >>> effective = compute_effective_plugins(config, team_id="backend")
23
+ >>> print(effective.enabled_plugins)
24
+ """
25
+
26
+ from scc_cli.marketplace.compute import (
27
+ BlockedPlugin,
28
+ EffectivePlugins,
29
+ TeamNotFoundError,
30
+ compute_effective_plugins,
31
+ )
32
+ from scc_cli.marketplace.constants import (
33
+ EXIT_CODES,
34
+ IMPLICIT_MARKETPLACES,
35
+ MANAGED_STATE_FILE,
36
+ MARKETPLACE_CACHE_DIR,
37
+ )
38
+
39
+ # Managed State
40
+ from scc_cli.marketplace.managed import (
41
+ ManagedState,
42
+ clear_managed_state,
43
+ load_managed_state,
44
+ save_managed_state,
45
+ )
46
+
47
+ # Materialization
48
+ from scc_cli.marketplace.materialize import (
49
+ GitNotAvailableError,
50
+ InvalidMarketplaceError,
51
+ MaterializationError,
52
+ MaterializedMarketplace,
53
+ materialize_marketplace,
54
+ )
55
+ from scc_cli.marketplace.normalize import (
56
+ AmbiguousMarketplaceError,
57
+ InvalidPluginRefError,
58
+ matches_any_pattern,
59
+ matches_pattern,
60
+ normalize_plugin,
61
+ )
62
+
63
+ # Rendering
64
+ from scc_cli.marketplace.render import (
65
+ check_conflicts,
66
+ merge_settings,
67
+ render_settings,
68
+ )
69
+ from scc_cli.marketplace.schema import (
70
+ DefaultsConfig,
71
+ MarketplaceSource,
72
+ MarketplaceSourceDirectory,
73
+ MarketplaceSourceGit,
74
+ MarketplaceSourceGitHub,
75
+ MarketplaceSourceURL,
76
+ OrganizationConfig,
77
+ SecurityConfig,
78
+ TeamProfile,
79
+ )
80
+
81
+ __all__ = [
82
+ # Constants
83
+ "EXIT_CODES",
84
+ "IMPLICIT_MARKETPLACES",
85
+ "MARKETPLACE_CACHE_DIR",
86
+ "MANAGED_STATE_FILE",
87
+ # Schema models
88
+ "OrganizationConfig",
89
+ "MarketplaceSource",
90
+ "MarketplaceSourceGitHub",
91
+ "MarketplaceSourceGit",
92
+ "MarketplaceSourceURL",
93
+ "MarketplaceSourceDirectory",
94
+ "TeamProfile",
95
+ "SecurityConfig",
96
+ "DefaultsConfig",
97
+ # Normalization
98
+ "normalize_plugin",
99
+ "matches_pattern",
100
+ "matches_any_pattern",
101
+ "InvalidPluginRefError",
102
+ "AmbiguousMarketplaceError",
103
+ # Computation
104
+ "compute_effective_plugins",
105
+ "EffectivePlugins",
106
+ "BlockedPlugin",
107
+ "TeamNotFoundError",
108
+ # Materialization
109
+ "materialize_marketplace",
110
+ "MaterializedMarketplace",
111
+ "MaterializationError",
112
+ "GitNotAvailableError",
113
+ "InvalidMarketplaceError",
114
+ # Rendering
115
+ "render_settings",
116
+ "merge_settings",
117
+ "check_conflicts",
118
+ # Managed state
119
+ "ManagedState",
120
+ "load_managed_state",
121
+ "save_managed_state",
122
+ "clear_managed_state",
123
+ ]
@@ -0,0 +1,74 @@
1
+ """
2
+ Adapter for translating external org config format to internal Pydantic format.
3
+
4
+ This module implements the Anti-Corruption Layer pattern between:
5
+ - External format (JSON Schema): Human-readable, semver strings, nested organization
6
+ - Internal format (Pydantic): Python-native types, integer versioning, flat structure
7
+
8
+ The translation happens AFTER JSON Schema validation (Validation Gate) but
9
+ BEFORE Pydantic model_validate() to ensure proper type conversion.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import copy
15
+ from typing import Any
16
+
17
+
18
+ def translate_org_config(external: dict[str, Any]) -> dict[str, Any]:
19
+ """Translate external JSON format to internal Pydantic format.
20
+
21
+ External format (from org config JSON):
22
+ - organization.name, organization.id (nested)
23
+ - schema_version: "1.0.0" (semver string)
24
+
25
+ Internal format (for Pydantic):
26
+ - name (flat at root level)
27
+ - schema_version: 1 (integer, major version only)
28
+
29
+ Uses deepcopy to prevent side-effects on cached external configuration.
30
+
31
+ Args:
32
+ external: External org config dict (from cache or remote fetch)
33
+
34
+ Returns:
35
+ Internal format dict ready for Pydantic model_validate()
36
+
37
+ Examples:
38
+ >>> external = {
39
+ ... "schema_version": "1.0.0",
40
+ ... "organization": {"name": "Acme Corp", "id": "acme"},
41
+ ... "profiles": {}
42
+ ... }
43
+ >>> internal = translate_org_config(external)
44
+ >>> internal["name"]
45
+ 'Acme Corp'
46
+ >>> internal["schema_version"]
47
+ 1
48
+ """
49
+ # Use deepcopy to prevent cache poisoning via shallow copy mutations
50
+ internal = copy.deepcopy(external)
51
+
52
+ # ── Flatten organization object ───────────────────────────────────────────
53
+ # Pop the nested organization structure, default to empty dict if missing
54
+ org_data = internal.pop("organization", {})
55
+
56
+ # Only map 'name' if it wasn't already at the top level (precedence rule)
57
+ # This handles the case where config is already in internal format
58
+ if "name" not in internal and "name" in org_data:
59
+ internal["name"] = org_data["name"]
60
+
61
+ # ── Convert semver string to integer ──────────────────────────────────────
62
+ raw_version = internal.get("schema_version")
63
+ if isinstance(raw_version, str):
64
+ # Remove common 'v' prefix if present (e.g., "v1.0.0" -> "1.0.0")
65
+ clean_version = raw_version.lstrip("vV")
66
+ try:
67
+ # Handle both "1.0.0" -> 1 and "1" -> 1
68
+ internal["schema_version"] = int(clean_version.split(".")[0])
69
+ except ValueError:
70
+ # If parsing fails, leave as-is; Pydantic will catch the type error
71
+ # and provide better error context than a generic ValueError
72
+ pass
73
+
74
+ return internal