commit 7831fd97f101b4e6516c92e947db4801f7fd33eb
parent c63002bcc4d3f6656e93aabe4fca6bd771376629
Author: triesap <tyson@radroots.org>
Date: Tue, 1 Sep 2026 04:34:04 +0000
ios: ratchet maintainability debt
- freeze existing Swift and Python complexity debt as exact ceilings
- reject new oversized files and complex Python functions
- separate user-message and package-contract responsibilities
- bind locked lint tooling, tests, and standalone quality commands
Diffstat:
16 files changed, 1012 insertions(+), 269 deletions(-)
diff --git a/.swiftlint.yml b/.swiftlint.yml
@@ -10,8 +10,8 @@ disabled_rules:
- closure_parameter_position
- opening_brace
- trailing_comma
- # Existing package architecture is governed by tests and API checks; these
- # metric and naming policies are not retrofit as Step 070 admission gates.
+ # Complexity/size metrics are run separately by swift-quality.sh against a
+ # checked exact baseline, so existing debt cannot hide new regressions.
- cyclomatic_complexity
- file_length
- function_body_length
diff --git a/AGENTS.md b/AGENTS.md
@@ -82,7 +82,14 @@ This file applies to the complete standalone iOS app repository. A closer
`make persona-verifier-bootstrap`; do not bypass it with ambient Python.
- `make swift-quality` applies the checked-in SwiftFormat and SwiftLint policy
to repository-owned package, app, unit-test, API-test, and UI-test sources;
- generated bindings and dependency/build output are excluded.
+ generated bindings and dependency/build output are excluded. It also runs
+ the exact checked SwiftLint debt baseline, locked Ruff checks, and the
+ closed size/complexity ratchet. Do not broaden a legacy exception or add a
+ new exception to admit new code; decompose the new responsibility instead.
+- `make maintainability-check` is the narrow fail-closed physical-line and
+ Python-AST complexity gate. Its source revision records the pre-ratchet
+ inventory, exception ceilings may only decrease or disappear, and newly
+ bounded modules must remain below the fixed thresholds.
- `make linux-shared-rust` runs the locked source-lock workspace in the pinned
Linux x86_64 Rust runner while keeping Cargo caches and output under the
extbuild project root.
diff --git a/Makefile b/Makefile
@@ -8,7 +8,7 @@ SIMULATOR_DESTINATION := platform=iOS Simulator,name=$(SIMULATOR_NAME)
.NOTPARALLEL:
.PHONY: all doctor bootstrap persona-verifier-bootstrap ffi-bootstrap artifact-check package-contract-check \
- swift-quality \
+ swift-quality maintainability-check \
linux-shared-rust \
package-resolve package-build package-test project xcodegen xcode-resolve \
xcode-build-debug xcode-build-release unit-test ui-test api-snapshot-write \
@@ -34,6 +34,10 @@ package-contract-check: doctor
swift-quality: doctor
cargo extbuild run -- scripts/swift-quality.sh
+maintainability-check: doctor
+ cargo extbuild run -- uv run --offline --project scripts/persona-verifier \
+ python scripts/maintainability_ratchet.py verify
+
linux-shared-rust: doctor
cargo extbuild run -- scripts/linux-shared-rust.sh
diff --git a/Package.swift b/Package.swift
@@ -59,6 +59,7 @@ let package = Package(
"Runtime/RadrootsLifecycleCoordinator.swift",
"Runtime/RadrootsRuntimeClient.swift",
"Runtime/RadrootsRuntimeModels.swift",
+ "Runtime/RadrootsUserMessageClassifier.swift",
"Runtime/RadrootsUserMessages.swift",
"State/RadrootsAddStore.swift",
"State/RadrootsConfigurationStore.swift",
diff --git a/README.md b/README.md
@@ -55,6 +55,14 @@ policy and the pinned Linux x86_64 shared-Rust runner. Run those focused checks
independently with `make swift-quality` and `make linux-shared-rust`; both keep
their build output under extbuild.
+`make swift-quality` also applies the exact checked SwiftLint complexity
+baseline and the repository-owned Swift/Python maintainability ratchet. New
+Swift files are capped at 600 physical lines, new Python files at 800, and new
+Python functions at complexity 10. Existing larger files and functions are a
+closed, non-growing inventory; the newly separated user-message classifier
+and package verification modules must remain below the new-file limits. Run
+`make maintainability-check` for the narrow size and Python-complexity gate.
+
Use `SIMULATOR_NAME="Device Name" make verify` when the default simulator is
not installed. `make clean` removes only rebuildable external build output and
the ignored XCFramework; it preserves tracked generated bindings, locks,
@@ -172,6 +180,9 @@ bounded data. It also runs the locked fixture and verifier unit suites.
Comments, examples, unreachable source, and arbitrary matching text cannot
satisfy a behavior-bearing package assertion; application behavior is proven
by the compiled Swift and simulator test lanes.
+The contract also binds the exact Ruff development tool, both maintainability
+baselines, and their executable verifier, so local lint behavior cannot drift
+with an ambient Python installation.
The unsigned release-evidence lane also regenerates a deterministic CycloneDX
SBOM from the locked Rust and Swift dependency graphs and binds it to the
diff --git a/Radroots/Runtime/RadrootsUserMessageClassifier.swift b/Radroots/Runtime/RadrootsUserMessageClassifier.swift
@@ -0,0 +1,264 @@
+import Foundation
+import RadrootsKit
+
+enum RadrootsUserMessageClassifier {
+ static func key(
+ for error: Error,
+ fallback: RadrootsUserMessageKey
+ ) -> RadrootsUserMessageKey {
+ runtimeKey(for: error, fallback: fallback)
+ ?? platformKey(for: error)
+ ?? identityCustodyKey(for: error)
+ ?? fallback
+ }
+
+ private static func runtimeKey(
+ for error: Error,
+ fallback: RadrootsUserMessageKey
+ ) -> RadrootsUserMessageKey? {
+ if error is CancellationError {
+ return .operationCancelled
+ }
+ if let error = error as? RadrootsRuntimeClientError {
+ return runtimeClientKey(error, fallback: fallback)
+ }
+ if error is RadrootsRuntimeFailure {
+ return fallback
+ }
+ if error is RadrootsConfigurationError {
+ return .configurationInvalid
+ }
+ if error is RadrootsIdentityStoreError {
+ return .identityOperationFailed
+ }
+ return nil
+ }
+
+ private static func identityCustodyKey(for error: Error) -> RadrootsUserMessageKey? {
+ error is RadrootsIdentityCustodyError ? .secureStateUnavailable : nil
+ }
+
+ private static func platformKey(for error: Error) -> RadrootsUserMessageKey? {
+ if let error = error as? RadrootsCaptureIntakeError {
+ return captureKey(error)
+ }
+ if let error = error as? RadrootsBackgroundTransferError {
+ return backgroundTransferKey(error)
+ }
+ if let error = error as? RadrootsDocumentInterchangeError {
+ return documentKey(error)
+ }
+ if let error = error as? RadrootsAppLocalStateResetError {
+ return localStateResetKey(error)
+ }
+ if let error = error as? RadrootsAppleMediaPreparationError {
+ return mediaPreparationKey(error)
+ }
+ return secondaryPlatformKey(for: error)
+ }
+
+ private static func secondaryPlatformKey(
+ for error: Error
+ ) -> RadrootsUserMessageKey? {
+ if let error = error as? RadrootsTelemetryError {
+ return telemetryKey(error)
+ }
+ if let error = error as? RadrootsExternalActionError {
+ return externalActionKey(error)
+ }
+ if let error = error as? RadrootsAppleFileError {
+ return fileKey(error)
+ }
+ if let error = error as? RadrootsLocationServicesError {
+ return locationKey(error)
+ }
+ if let error = error as? RadrootsUserPresenceError {
+ return userPresenceKey(error)
+ }
+ return tertiaryPlatformKey(for: error)
+ }
+
+ private static func tertiaryPlatformKey(
+ for error: Error
+ ) -> RadrootsUserMessageKey? {
+ if let error = error as? RadrootsBackgroundTaskError {
+ return backgroundTaskKey(error)
+ }
+ if let error = error as? RadrootsAppleSecurityError {
+ return securityKey(error)
+ }
+ if let error = error as? RadrootsAppleMobileStoreError {
+ return mobileStoreKey(error)
+ }
+ if let error = error as? RadrootsVerifiedArtifactAccessError {
+ return verifiedArtifactKey(error)
+ }
+ return nil
+ }
+
+ private static func runtimeClientKey(
+ _ error: RadrootsRuntimeClientError,
+ fallback: RadrootsUserMessageKey
+ ) -> RadrootsUserMessageKey {
+ switch error {
+ case .invalidBufferCapacity: .invalidRequest
+ case .notRunning: .runtimeUnavailable
+ case .superseded: .operationCancelled
+ case .startup: .startupFailed
+ case .subscription: .runtimeObservationUnavailable
+ case .shutdown: .shutdownFailed
+ case .status, .today, .add, .support: fallback
+ }
+ }
+
+ private static func captureKey(
+ _ error: RadrootsCaptureIntakeError
+ ) -> RadrootsUserMessageKey {
+ switch error {
+ case .invalidRequest: .invalidRequest
+ case .unavailable: .addMediaUnavailable
+ case .permissionDenied: .permissionDenied
+ case .userCancelled: .operationCancelled
+ case .transientFailure: .tryAgain
+ case .permanentFailure: .addMediaUnavailable
+ }
+ }
+
+ private static func backgroundTransferKey(
+ _ error: RadrootsBackgroundTransferError
+ ) -> RadrootsUserMessageKey {
+ switch error {
+ case .invalidRequest: .invalidRequest
+ case .unavailable: .backgroundTransferUnavailable
+ case .transferFailure: .backgroundTransferFailed
+ case .persistenceFailure: .secureStateUnavailable
+ }
+ }
+
+ private static func documentKey(
+ _ error: RadrootsDocumentInterchangeError
+ ) -> RadrootsUserMessageKey {
+ switch error {
+ case .invalidRequest: .invalidRequest
+ case .notFound: .fileOperationFailed
+ case .userCancelled: .operationCancelled
+ case .permissionDenied: .permissionDenied
+ case .transientFailure: .tryAgain
+ case .permanentFailure: .fileOperationFailed
+ }
+ }
+
+ private static func localStateResetKey(
+ _ error: RadrootsAppLocalStateResetError
+ ) -> RadrootsUserMessageKey {
+ switch error {
+ case .invalidRequest: .invalidRequest
+ case .fileSystemFailure, .keychainFailure: .secureStateUnavailable
+ }
+ }
+
+ private static func mediaPreparationKey(
+ _ error: RadrootsAppleMediaPreparationError
+ ) -> RadrootsUserMessageKey {
+ switch error {
+ case .invalidRequest: .invalidRequest
+ case .unavailable, .preparationFailure: .addMediaUnavailable
+ }
+ }
+
+ private static func telemetryKey(
+ _ error: RadrootsTelemetryError
+ ) -> RadrootsUserMessageKey {
+ switch error {
+ case .invalidRequest: .invalidRequest
+ }
+ }
+
+ private static func externalActionKey(
+ _ error: RadrootsExternalActionError
+ ) -> RadrootsUserMessageKey {
+ switch error {
+ case .invalidRequest: .invalidRequest
+ case .blockedByPolicy, .unavailable: .permissionDenied
+ case .transientFailure: .tryAgain
+ case .permanentFailure: .runtimeOperationFailed
+ }
+ }
+
+ private static func fileKey(
+ _ error: RadrootsAppleFileError
+ ) -> RadrootsUserMessageKey {
+ switch error {
+ case .invalidRequest: .invalidRequest
+ case .notFound, .permanentFailure: .fileOperationFailed
+ case .permissionDenied: .permissionDenied
+ case .transientFailure: .tryAgain
+ }
+ }
+
+ private static func locationKey(
+ _ error: RadrootsLocationServicesError
+ ) -> RadrootsUserMessageKey {
+ switch error {
+ case .invalidRequest: .invalidRequest
+ case .permissionDenied: .permissionDenied
+ case .unavailable, .timeout, .cancelled, .transientFailure, .permanentFailure:
+ .locationUnavailable
+ }
+ }
+
+ private static func userPresenceKey(
+ _ error: RadrootsUserPresenceError
+ ) -> RadrootsUserMessageKey {
+ switch error {
+ case .invalidRequest: .invalidRequest
+ case .userCancelled: .operationCancelled
+ case .permissionDenied: .permissionDenied
+ case .unavailable, .timeout, .transientFailure, .permanentFailure:
+ .userPresenceUnavailable
+ }
+ }
+
+ private static func backgroundTaskKey(
+ _ error: RadrootsBackgroundTaskError
+ ) -> RadrootsUserMessageKey {
+ switch error {
+ case .invalidRequest: .invalidRequest
+ case .unavailable, .schedulerFailure: .runtimeUnavailable
+ }
+ }
+
+ private static func securityKey(
+ _ error: RadrootsAppleSecurityError
+ ) -> RadrootsUserMessageKey {
+ switch error {
+ case .invalidRequest: .invalidRequest
+ case .permissionDenied: .permissionDenied
+ case .userCancelled: .operationCancelled
+ case .notFound, .transientFailure, .unavailable, .permanentFailure,
+ .keychainFailure:
+ .secureStateUnavailable
+ }
+ }
+
+ private static func mobileStoreKey(
+ _ error: RadrootsAppleMobileStoreError
+ ) -> RadrootsUserMessageKey {
+ switch error {
+ case .invalidPublicKey: .invalidRequest
+ case .protectedDataUnavailable, .invalidDirectoryLayout, .fileSystemFailure:
+ .secureStateUnavailable
+ }
+ }
+
+ private static func verifiedArtifactKey(
+ _ error: RadrootsVerifiedArtifactAccessError
+ ) -> RadrootsUserMessageKey {
+ switch error {
+ case .invalidDescriptor: .invalidRequest
+ case .protectedDataUnavailable: .secureStateUnavailable
+ case .artifactUnavailable, .artifactCorrupt, .fileSystemFailure:
+ .fileOperationFailed
+ }
+ }
+}
diff --git a/Radroots/Runtime/RadrootsUserMessages.swift b/Radroots/Runtime/RadrootsUserMessages.swift
@@ -111,229 +111,7 @@ enum RadrootsUserMessages {
for error: Error,
fallback: RadrootsUserMessageKey
) -> RadrootsUserMessageKey {
- if error is CancellationError {
- return .operationCancelled
- }
- if let error = error as? RadrootsRuntimeClientError {
- return runtimeClientKey(error, fallback: fallback)
- }
- if error is RadrootsRuntimeFailure {
- return fallback
- }
- if error is RadrootsConfigurationError {
- return .configurationInvalid
- }
- if error is RadrootsIdentityStoreError {
- return .identityOperationFailed
- }
- if let error = error as? RadrootsCaptureIntakeError {
- return captureKey(error)
- }
- if let error = error as? RadrootsBackgroundTransferError {
- return backgroundTransferKey(error)
- }
- if let error = error as? RadrootsDocumentInterchangeError {
- return documentKey(error)
- }
- if let error = error as? RadrootsAppLocalStateResetError {
- return localStateResetKey(error)
- }
- if let error = error as? RadrootsAppleMediaPreparationError {
- return mediaPreparationKey(error)
- }
- if let error = error as? RadrootsTelemetryError {
- return telemetryKey(error)
- }
- if let error = error as? RadrootsExternalActionError {
- return externalActionKey(error)
- }
- if let error = error as? RadrootsAppleFileError {
- return fileKey(error)
- }
- if let error = error as? RadrootsLocationServicesError {
- return locationKey(error)
- }
- if let error = error as? RadrootsUserPresenceError {
- return userPresenceKey(error)
- }
- if let error = error as? RadrootsBackgroundTaskError {
- return backgroundTaskKey(error)
- }
- if let error = error as? RadrootsAppleSecurityError {
- return securityKey(error)
- }
- if let error = error as? RadrootsAppleMobileStoreError {
- return mobileStoreKey(error)
- }
- if let error = error as? RadrootsVerifiedArtifactAccessError {
- return verifiedArtifactKey(error)
- }
- if error is RadrootsIdentityCustodyError {
- return .secureStateUnavailable
- }
- return fallback
- }
-
- private static func runtimeClientKey(
- _ error: RadrootsRuntimeClientError,
- fallback: RadrootsUserMessageKey
- ) -> RadrootsUserMessageKey {
- switch error {
- case .invalidBufferCapacity:
- .invalidRequest
- case .notRunning:
- .runtimeUnavailable
- case .superseded:
- .operationCancelled
- case .startup:
- .startupFailed
- case .subscription:
- .runtimeObservationUnavailable
- case .shutdown:
- .shutdownFailed
- case .status, .today, .add, .support:
- fallback
- }
- }
-
- private static func captureKey(_ error: RadrootsCaptureIntakeError) -> RadrootsUserMessageKey {
- switch error {
- case .invalidRequest: .invalidRequest
- case .unavailable: .addMediaUnavailable
- case .permissionDenied: .permissionDenied
- case .userCancelled: .operationCancelled
- case .transientFailure: .tryAgain
- case .permanentFailure: .addMediaUnavailable
- }
- }
-
- private static func backgroundTransferKey(
- _ error: RadrootsBackgroundTransferError
- ) -> RadrootsUserMessageKey {
- switch error {
- case .invalidRequest: .invalidRequest
- case .unavailable: .backgroundTransferUnavailable
- case .transferFailure: .backgroundTransferFailed
- case .persistenceFailure: .secureStateUnavailable
- }
- }
-
- private static func documentKey(
- _ error: RadrootsDocumentInterchangeError
- ) -> RadrootsUserMessageKey {
- switch error {
- case .invalidRequest: .invalidRequest
- case .notFound: .fileOperationFailed
- case .userCancelled: .operationCancelled
- case .permissionDenied: .permissionDenied
- case .transientFailure: .tryAgain
- case .permanentFailure: .fileOperationFailed
- }
- }
-
- private static func localStateResetKey(
- _ error: RadrootsAppLocalStateResetError
- ) -> RadrootsUserMessageKey {
- switch error {
- case .invalidRequest: .invalidRequest
- case .fileSystemFailure, .keychainFailure: .secureStateUnavailable
- }
- }
-
- private static func mediaPreparationKey(
- _ error: RadrootsAppleMediaPreparationError
- ) -> RadrootsUserMessageKey {
- switch error {
- case .invalidRequest: .invalidRequest
- case .unavailable: .addMediaUnavailable
- case .preparationFailure: .addMediaUnavailable
- }
- }
-
- private static func telemetryKey(_ error: RadrootsTelemetryError) -> RadrootsUserMessageKey {
- switch error {
- case .invalidRequest: .invalidRequest
- }
- }
-
- private static func externalActionKey(
- _ error: RadrootsExternalActionError
- ) -> RadrootsUserMessageKey {
- switch error {
- case .invalidRequest: .invalidRequest
- case .blockedByPolicy, .unavailable: .permissionDenied
- case .transientFailure: .tryAgain
- case .permanentFailure: .runtimeOperationFailed
- }
- }
-
- private static func fileKey(_ error: RadrootsAppleFileError) -> RadrootsUserMessageKey {
- switch error {
- case .invalidRequest: .invalidRequest
- case .notFound, .permanentFailure: .fileOperationFailed
- case .permissionDenied: .permissionDenied
- case .transientFailure: .tryAgain
- }
- }
-
- private static func locationKey(_ error: RadrootsLocationServicesError) -> RadrootsUserMessageKey {
- switch error {
- case .invalidRequest: .invalidRequest
- case .permissionDenied: .permissionDenied
- case .unavailable, .timeout, .cancelled, .transientFailure, .permanentFailure:
- .locationUnavailable
- }
- }
-
- private static func userPresenceKey(
- _ error: RadrootsUserPresenceError
- ) -> RadrootsUserMessageKey {
- switch error {
- case .invalidRequest: .invalidRequest
- case .userCancelled: .operationCancelled
- case .permissionDenied: .permissionDenied
- case .unavailable, .timeout, .transientFailure, .permanentFailure:
- .userPresenceUnavailable
- }
- }
-
- private static func backgroundTaskKey(
- _ error: RadrootsBackgroundTaskError
- ) -> RadrootsUserMessageKey {
- switch error {
- case .invalidRequest: .invalidRequest
- case .unavailable, .schedulerFailure: .runtimeUnavailable
- }
- }
-
- private static func securityKey(_ error: RadrootsAppleSecurityError) -> RadrootsUserMessageKey {
- switch error {
- case .invalidRequest: .invalidRequest
- case .permissionDenied: .permissionDenied
- case .userCancelled: .operationCancelled
- case .notFound, .transientFailure, .unavailable, .permanentFailure, .keychainFailure:
- .secureStateUnavailable
- }
- }
-
- private static func mobileStoreKey(
- _ error: RadrootsAppleMobileStoreError
- ) -> RadrootsUserMessageKey {
- switch error {
- case .invalidPublicKey: .invalidRequest
- case .protectedDataUnavailable, .invalidDirectoryLayout, .fileSystemFailure:
- .secureStateUnavailable
- }
- }
-
- private static func verifiedArtifactKey(
- _ error: RadrootsVerifiedArtifactAccessError
- ) -> RadrootsUserMessageKey {
- switch error {
- case .invalidDescriptor: .invalidRequest
- case .protectedDataUnavailable: .secureStateUnavailable
- case .artifactUnavailable, .artifactCorrupt, .fileSystemFailure: .fileOperationFailed
- }
+ RadrootsUserMessageClassifier.key(for: error, fallback: fallback)
}
private static var defaultBundle: Bundle {
diff --git a/scripts/maintainability_ratchet.py b/scripts/maintainability_ratchet.py
@@ -0,0 +1,340 @@
+#!/usr/bin/env python3
+"""Fail-closed Swift/Python size and Python complexity ratchet."""
+
+from __future__ import annotations
+
+import argparse
+import ast
+import json
+import sys
+from pathlib import Path
+from typing import Any
+
+MAX_BASELINE_BYTES = 256 * 1024
+BASELINE_PATH = Path("test-fixtures/maintainability-baseline.v1.json")
+SWIFT_ROOTS = (Path("Radroots"), Path("RadrootsTests"), Path("RadrootsUITests"))
+PYTHON_ROOT = Path("scripts")
+EXCLUDED_SWIFT_ROOT = Path("Radroots/Generated")
+
+
+class MaintainabilityError(Exception):
+ """Stable maintainability-policy rejection."""
+
+
+class _FunctionComplexity(ast.NodeVisitor):
+ def __init__(self) -> None:
+ self.value = 1
+
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
+ del node
+
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
+ del node
+
+ def visit_Lambda(self, node: ast.Lambda) -> None:
+ del node
+
+ def visit_If(self, node: ast.If) -> None:
+ self.value += 1
+ self.generic_visit(node)
+
+ def visit_IfExp(self, node: ast.IfExp) -> None:
+ self.value += 1
+ self.generic_visit(node)
+
+ def visit_For(self, node: ast.For) -> None:
+ self.value += 1
+ self.generic_visit(node)
+
+ def visit_AsyncFor(self, node: ast.AsyncFor) -> None:
+ self.value += 1
+ self.generic_visit(node)
+
+ def visit_While(self, node: ast.While) -> None:
+ self.value += 1
+ self.generic_visit(node)
+
+ def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
+ self.value += 1
+ self.generic_visit(node)
+
+ def visit_BoolOp(self, node: ast.BoolOp) -> None:
+ self.value += max(0, len(node.values) - 1)
+ self.generic_visit(node)
+
+ def visit_Match(self, node: ast.Match) -> None:
+ self.value += len(node.cases)
+ self.generic_visit(node)
+
+ def visit_comprehension(self, node: ast.comprehension) -> None:
+ self.value += 1 + len(node.ifs)
+ self.generic_visit(node)
+
+
+class _DefinitionCollector(ast.NodeVisitor):
+ def __init__(self, path: str) -> None:
+ self.path = path
+ self.scope: list[str] = []
+ self.values: dict[str, int] = {}
+
+ def visit_ClassDef(self, node: ast.ClassDef) -> None:
+ self.scope.append(node.name)
+ self.generic_visit(node)
+ self.scope.pop()
+
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
+ self._record(node)
+
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
+ self._record(node)
+
+ def _record(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
+ name = ".".join((*self.scope, node.name))
+ key = f"{self.path}:{name}"
+ if key in self.values:
+ raise MaintainabilityError("Python function identity is duplicated")
+ counter = _FunctionComplexity()
+ for statement in node.body:
+ counter.visit(statement)
+ self.values[key] = counter.value
+ self.scope.append(node.name)
+ for statement in node.body:
+ self.visit(statement)
+ self.scope.pop()
+
+
+def _read_regular(path: Path, maximum: int = MAX_BASELINE_BYTES) -> bytes:
+ try:
+ if path.is_symlink() or not path.is_file():
+ raise MaintainabilityError("maintainability input is not a regular file")
+ size = path.stat().st_size
+ if size < 1 or size > maximum:
+ raise MaintainabilityError("maintainability input exceeds its byte bound")
+ value = path.read_bytes()
+ except OSError as error:
+ raise MaintainabilityError("maintainability input cannot be read") from error
+ if len(value) != size:
+ raise MaintainabilityError("maintainability input changed while reading")
+ return value
+
+
+def _load_baseline(repo_root: Path) -> dict[str, Any]:
+ try:
+ value = json.loads(_read_regular(repo_root / BASELINE_PATH))
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
+ raise MaintainabilityError("maintainability baseline is malformed") from error
+ expected = {
+ "schema",
+ "schema_version",
+ "source_revision",
+ "thresholds",
+ "swift_file_exception",
+ "python_file_exception",
+ "python_complexity_exception",
+ "bounded_module",
+ }
+ if not isinstance(value, dict) or set(value) != expected:
+ raise MaintainabilityError("maintainability baseline fields differ")
+ if (
+ value["schema"] != "radroots.ios.maintainability-baseline.v1"
+ or value["schema_version"] != 1
+ or value["source_revision"] != "c63002bcc4d3f6656e93aabe4fca6bd771376629"
+ ):
+ raise MaintainabilityError("maintainability baseline identity differs")
+ return value
+
+
+def _source_files(repo_root: Path) -> tuple[list[Path], list[Path]]:
+ swift = sorted(
+ path
+ for root in SWIFT_ROOTS
+ for path in (repo_root / root).rglob("*.swift")
+ if not path.is_symlink()
+ and not path.relative_to(repo_root).is_relative_to(EXCLUDED_SWIFT_ROOT)
+ )
+ python = sorted(
+ path for path in (repo_root / PYTHON_ROOT).glob("*.py") if not path.is_symlink()
+ )
+ if not swift or not python:
+ raise MaintainabilityError("maintainability source inventory is empty")
+ return swift, python
+
+
+def _line_inventory(repo_root: Path, paths: list[Path]) -> dict[str, int]:
+ values: dict[str, int] = {}
+ for path in paths:
+ raw = _read_regular(path, 4 * 1024 * 1024)
+ try:
+ text = raw.decode("utf-8")
+ except UnicodeDecodeError as error:
+ raise MaintainabilityError("maintainability source is not UTF-8") from error
+ relative = path.relative_to(repo_root).as_posix()
+ values[relative] = len(text.splitlines())
+ return values
+
+
+def _python_complexity(repo_root: Path, paths: list[Path]) -> dict[str, int]:
+ values: dict[str, int] = {}
+ for path in paths:
+ relative = path.relative_to(repo_root).as_posix()
+ try:
+ tree = ast.parse(_read_regular(path, 4 * 1024 * 1024), filename=relative)
+ except (SyntaxError, ValueError) as error:
+ raise MaintainabilityError("Python source cannot be parsed") from error
+ collector = _DefinitionCollector(relative)
+ collector.visit(tree)
+ overlap = set(values) & set(collector.values)
+ if overlap:
+ raise MaintainabilityError("Python function identity is duplicated")
+ values.update(collector.values)
+ return values
+
+
+def snapshot(repo_root: Path) -> dict[str, dict[str, int]]:
+ swift, python = _source_files(repo_root)
+ return {
+ "swift_lines": _line_inventory(repo_root, swift),
+ "python_lines": _line_inventory(repo_root, python),
+ "python_complexity": _python_complexity(repo_root, python),
+ }
+
+
+def _closed_exception_map(
+ value: object,
+ *,
+ key_name: str,
+ ceiling_name: str,
+) -> dict[str, int]:
+ if not isinstance(value, list):
+ raise MaintainabilityError("maintainability exception inventory is invalid")
+ result: dict[str, int] = {}
+ for row in value:
+ if (
+ not isinstance(row, dict)
+ or set(row) != {key_name, ceiling_name}
+ or not isinstance(row[key_name], str)
+ or not isinstance(row[ceiling_name], int)
+ or row[ceiling_name] < 1
+ or row[key_name] in result
+ ):
+ raise MaintainabilityError("maintainability exception row is invalid")
+ result[row[key_name]] = row[ceiling_name]
+ if list(result) != sorted(result):
+ raise MaintainabilityError("maintainability exceptions are not ordered")
+ return result
+
+
+def _verify_metric(
+ observed: dict[str, int],
+ exceptions: dict[str, int],
+ threshold: int,
+ label: str,
+) -> None:
+ expected_exceptions = {key for key, value in observed.items() if value > threshold}
+ if set(exceptions) != expected_exceptions:
+ raise MaintainabilityError(f"{label} exception inventory differs")
+ for key, ceiling in exceptions.items():
+ if ceiling <= threshold or observed[key] > ceiling:
+ raise MaintainabilityError(f"{label} metric regressed")
+
+
+def _thresholds(baseline: dict[str, Any]) -> dict[str, int]:
+ value = baseline["thresholds"]
+ if (
+ not isinstance(value, dict)
+ or set(value)
+ != {"swift_file_lines", "python_file_lines", "python_function_complexity"}
+ or value["swift_file_lines"] != 600
+ or value["python_file_lines"] != 800
+ or value["python_function_complexity"] != 10
+ ):
+ raise MaintainabilityError("maintainability thresholds differ")
+ return value
+
+
+def _verify_observed_metrics(
+ baseline: dict[str, Any],
+ observed: dict[str, dict[str, int]],
+ thresholds: dict[str, int],
+) -> None:
+ specifications = (
+ (
+ "swift_file_exception",
+ "path",
+ "maximum_lines",
+ "swift_lines",
+ "swift_file_lines",
+ "Swift file",
+ ),
+ (
+ "python_file_exception",
+ "path",
+ "maximum_lines",
+ "python_lines",
+ "python_file_lines",
+ "Python file",
+ ),
+ (
+ "python_complexity_exception",
+ "function",
+ "maximum_complexity",
+ "python_complexity",
+ "python_function_complexity",
+ "Python complexity",
+ ),
+ )
+ for baseline_key, identity, ceiling, metric, threshold, label in specifications:
+ exceptions = _closed_exception_map(
+ baseline[baseline_key], key_name=identity, ceiling_name=ceiling
+ )
+ _verify_metric(observed[metric], exceptions, thresholds[threshold], label)
+
+
+def _verify_bounded_modules(
+ baseline: dict[str, Any],
+ observed: dict[str, dict[str, int]],
+ thresholds: dict[str, int],
+) -> None:
+ modules = baseline["bounded_module"]
+ if not isinstance(modules, list) or modules != sorted(set(modules)):
+ raise MaintainabilityError("bounded module inventory differs")
+ all_lines = observed["swift_lines"] | observed["python_lines"]
+ for path in modules:
+ if path not in all_lines:
+ raise MaintainabilityError("bounded module is absent")
+ limit = thresholds[
+ "swift_file_lines" if path.endswith(".swift") else "python_file_lines"
+ ]
+ if all_lines[path] > limit:
+ raise MaintainabilityError("bounded module exceeds its threshold")
+
+
+def verify(repo_root: Path) -> None:
+ baseline = _load_baseline(repo_root)
+ thresholds = _thresholds(baseline)
+ observed = snapshot(repo_root)
+ _verify_observed_metrics(baseline, observed, thresholds)
+ _verify_bounded_modules(baseline, observed, thresholds)
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("command", choices=("verify", "snapshot"))
+ parser.add_argument(
+ "--repo-root", type=Path, default=Path(__file__).resolve().parent.parent
+ )
+ arguments = parser.parse_args()
+ try:
+ if arguments.command == "snapshot":
+ print(json.dumps(snapshot(arguments.repo_root.resolve()), sort_keys=True))
+ else:
+ verify(arguments.repo_root.resolve())
+ print("maintainability ratchet verified")
+ except MaintainabilityError as error:
+ print(f"maintainability: {error}", file=sys.stderr)
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/package_contract.py b/scripts/package_contract.py
@@ -129,33 +129,43 @@ def parse_xcconfig_assignments(text: str) -> dict[str, str]:
def parse_project_package(text: str, package_name: str) -> dict[str, str]:
lines = text.splitlines()
- packages_line = next(
- (index for index, line in enumerate(lines) if line == "packages:"),
- None,
+ packages_line, packages_end = _project_package_bounds(lines)
+ start = _project_package_start(lines, packages_line, packages_end, package_name)
+ return _project_package_fields(lines[start + 1 :])
+
+
+def _project_package_bounds(lines: list[str]) -> tuple[int, int]:
+ start = next(
+ (index for index, line in enumerate(lines) if line == "packages:"), None
)
- if packages_line is None:
+ if start is None:
raise PackageContractError("project package inventory is absent")
- expected_header = f" {package_name}:"
- packages_end = next(
+ end = next(
(
index
- for index in range(packages_line + 1, len(lines))
+ for index in range(start + 1, len(lines))
if lines[index] and not lines[index].startswith((" ", "#"))
),
len(lines),
)
- start = next(
- (
- index
- for index in range(packages_line + 1, packages_end)
- if lines[index] == expected_header
- ),
- None,
+ return start, end
+
+
+def _project_package_start(
+ lines: list[str], start: int, end: int, package_name: str
+) -> int:
+ expected = f" {package_name}:"
+ result = next(
+ (index for index in range(start + 1, end) if lines[index] == expected), None
)
- if start is None:
+ if result is None:
raise PackageContractError("project package entry is absent")
+ return result
+
+
+def _project_package_fields(lines: list[str]) -> dict[str, str]:
values: dict[str, str] = {}
- for line in lines[start + 1 :]:
+ for line in lines:
if line and not line.startswith(" "):
break
match = re.fullmatch(r" ([a-z_]+): (\S+)", line)
@@ -224,7 +234,9 @@ def _swift_package(repo_root: Path) -> dict[str, Any]:
try:
value = json.loads(output)
except (UnicodeDecodeError, json.JSONDecodeError) as error:
- raise PackageContractError("Swift package manifest output is malformed") from error
+ raise PackageContractError(
+ "Swift package manifest output is malformed"
+ ) from error
if not isinstance(value, dict):
raise PackageContractError("Swift package manifest output is not an object")
return value
@@ -236,22 +248,9 @@ def _apple_revision(package: dict[str, Any]) -> str:
raise PackageContractError("Swift package dependencies are absent")
matches: list[str] = []
for dependency in dependencies:
- item = _mapping(dependency, "Swift package dependency")
- source = item.get("sourceControl")
- if not isinstance(source, list) or len(source) != 1:
- continue
- identity = _mapping(source[0], "Swift package source")
- remote = identity.get("location")
- revision = identity.get("requirement")
- remote_values = remote.get("remote") if isinstance(remote, dict) else None
- if (
- isinstance(remote_values, list)
- and remote_values == [{"urlString": APPLE_KIT_REMOTE}]
- and isinstance(revision, dict)
- and isinstance(revision.get("revision"), list)
- and len(revision["revision"]) == 1
- ):
- matches.append(revision["revision"][0])
+ candidate = _apple_dependency_revision(dependency)
+ if candidate is not None:
+ matches.append(candidate)
if (
len(matches) != 1
or not isinstance(matches[0], str)
@@ -261,6 +260,25 @@ def _apple_revision(package: dict[str, Any]) -> str:
return matches[0]
+def _apple_dependency_revision(dependency: object) -> object | None:
+ item = _mapping(dependency, "Swift package dependency")
+ source = item.get("sourceControl")
+ if not isinstance(source, list) or len(source) != 1:
+ return None
+ identity = _mapping(source[0], "Swift package source")
+ remote = identity.get("location")
+ requirement = identity.get("requirement")
+ remote_values = remote.get("remote") if isinstance(remote, dict) else None
+ if remote_values != [{"urlString": APPLE_KIT_REMOTE}]:
+ return None
+ if not isinstance(requirement, dict):
+ return None
+ revisions = requirement.get("revision")
+ if not isinstance(revisions, list) or len(revisions) != 1:
+ return None
+ return revisions[0]
+
+
def _validate_privacy(document: dict[str, Any]) -> None:
_exact(document.get("NSPrivacyTracking"), False, "privacy tracking")
_exact(document.get("NSPrivacyTrackingDomains"), [], "privacy tracking domains")
@@ -310,13 +328,14 @@ def _validate_ui_test_plist(document: dict[str, Any]) -> None:
raise PackageContractError("UI test plist inventory is incomplete")
-def verify(repo_root: Path) -> tuple[str, str]:
- root = repo_root.resolve()
+def _verify_repository_layout(root: Path) -> None:
for forbidden in ("docs", ".github", ".act"):
path = root / forbidden
if path.exists() or path.is_symlink():
raise PackageContractError("forbidden public repository root exists")
+
+def _verify_cargo_and_source(root: Path) -> tuple[str, str]:
cargo = _read_toml(root / "Cargo.toml")
workspace = _mapping(cargo.get("workspace"), "Cargo workspace")
workspace_package = _mapping(workspace.get("package"), "Cargo workspace package")
@@ -370,7 +389,10 @@ def verify(repo_root: Path) -> tuple[str, str]:
_exact(consumer.get("repository"), LIB_REMOTE, "consumer Lib remote")
_exact(consumer.get("revision"), lib_revision, "consumer Lib revision")
_exact(consumer.get("version"), release_version, "consumer Lib version")
+ return release_version, lib_revision
+
+def _verify_apple_dependencies(root: Path) -> str:
package = _swift_package(root)
_exact(package.get("name"), "radroots_ios_app", "Swift package name")
_exact(package.get("defaultLocalization"), "en", "Swift localization")
@@ -380,13 +402,20 @@ def verify(repo_root: Path) -> tuple[str, str]:
raise PackageContractError("project AppleKit field inventory differs")
_exact(project.get("url"), APPLE_KIT_REMOTE, "project AppleKit remote")
_exact(project.get("revision"), apple_revision, "project AppleKit revision")
+ return apple_revision
+
+def _verify_apple_configuration(root: Path) -> None:
_validate_privacy(_read_plist(root / "Radroots/Resources/PrivacyInfo.xcprivacy"))
_validate_app_plist(_read_plist(root / "Radroots/Info.plist"))
_validate_ui_test_plist(_read_plist(root / "RadrootsUITests/Info.plist"))
- base = parse_xcconfig_assignments(_read_text(root / "Radroots/Config/Base.xcconfig"))
- debug = parse_xcconfig_assignments(_read_text(root / "Radroots/Config/Debug.xcconfig"))
+ base = parse_xcconfig_assignments(
+ _read_text(root / "Radroots/Config/Base.xcconfig")
+ )
+ debug = parse_xcconfig_assignments(
+ _read_text(root / "Radroots/Config/Debug.xcconfig")
+ )
if set(base) != {
"RADROOTS_FIELD_IOS_RUNTIME_MODE",
"RADROOTS_FIELD_IOS_NOSTR_RELAY_URLS",
@@ -423,9 +452,12 @@ def verify(repo_root: Path) -> tuple[str, str]:
"debug Blossom origin",
)
+
+def _verify_package_locks(root: Path, apple_revision: str) -> None:
resolved_paths = (
root / "Package.resolved",
- root / "Radroots.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved",
+ root
+ / "Radroots.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved",
)
resolved = [_read_json(path) for path in resolved_paths]
for document in resolved:
@@ -433,6 +465,8 @@ def verify(repo_root: Path) -> tuple[str, str]:
if resolved[0].get("pins") != resolved[1].get("pins"):
raise PackageContractError("Swift and Xcode package locks disagree")
+
+def _verify_persona_toolchain(root: Path) -> None:
verifier_project = _read_toml(root / "scripts/persona-verifier/pyproject.toml")
verifier_lock = _read_toml(root / "scripts/persona-verifier/uv.lock")
verifier_metadata = _mapping(verifier_project.get("project"), "verifier project")
@@ -446,6 +480,10 @@ def verify(repo_root: Path) -> tuple[str, str]:
["jsonschema==4.26.0"],
"verifier dependencies",
)
+ dependency_groups = _mapping(
+ verifier_project.get("dependency-groups"), "verifier dependency groups"
+ )
+ _exact(dependency_groups, {"dev": ["ruff==0.12.12"]}, "verifier dev tools")
_exact(verifier_lock.get("requires-python"), "==3.14.7", "verifier lock Python")
package_rows = verifier_lock.get("package")
if not isinstance(package_rows, list):
@@ -456,13 +494,19 @@ def verify(repo_root: Path) -> tuple[str, str]:
if isinstance(item, dict)
}
_exact(locked_packages.get("jsonschema"), "4.26.0", "verifier jsonschema lock")
+ _exact(locked_packages.get("ruff"), "0.12.12", "verifier ruff lock")
+
+def _verify_required_files(root: Path) -> None:
required_files = (
".swiftformat",
".swiftlint.yml",
+ "scripts/maintainability_ratchet.py",
"scripts/local-social-fixture.py",
"scripts/swift-quality.sh",
"scripts/linux-shared-rust.sh",
+ "test-fixtures/maintainability-baseline.v1.json",
+ "test-fixtures/swiftlint-maintainability-baseline.v1.json",
"test-fixtures/bud11-upload-authorization-mutations.v1.json",
"test-fixtures/bud11-upload-authorization-mutations.v1.schema.json",
"test-fixtures/local-social-personas.v1.json",
@@ -476,6 +520,17 @@ def verify(repo_root: Path) -> tuple[str, str]:
for relative in ("scripts/swift-quality.sh", "scripts/linux-shared-rust.sh"):
if not os.access(root / relative, os.X_OK):
raise PackageContractError("required package command is not executable")
+
+
+def verify(repo_root: Path) -> tuple[str, str]:
+ root = repo_root.resolve()
+ _verify_repository_layout(root)
+ release_version, _ = _verify_cargo_and_source(root)
+ apple_revision = _verify_apple_dependencies(root)
+ _verify_apple_configuration(root)
+ _verify_package_locks(root, apple_revision)
+ _verify_persona_toolchain(root)
+ _verify_required_files(root)
return release_version, apple_revision
diff --git a/scripts/persona-verifier/pyproject.toml b/scripts/persona-verifier/pyproject.toml
@@ -6,3 +6,8 @@ dependencies = ["jsonschema==4.26.0"]
[tool.uv]
package = false
+
+[dependency-groups]
+dev = [
+ "ruff==0.12.12",
+]
diff --git a/scripts/persona-verifier/uv.lock b/scripts/persona-verifier/uv.lock
@@ -46,9 +46,17 @@ dependencies = [
{ name = "jsonschema" },
]
+[package.dev-dependencies]
+dev = [
+ { name = "ruff" },
+]
+
[package.metadata]
requires-dist = [{ name = "jsonschema", specifier = "==4.26.0" }]
+[package.metadata.requires-dev]
+dev = [{ name = "ruff", specifier = "==0.12.12" }]
+
[[package]]
name = "referencing"
version = "0.37.0"
@@ -98,3 +106,29 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" },
{ url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" },
]
+
+[[package]]
+name = "ruff"
+version = "0.12.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a8/f0/e0965dd709b8cabe6356811c0ee8c096806bb57d20b5019eb4e48a117410/ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6", size = 5359915, upload-time = "2025-09-04T16:50:18.273Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/09/79/8d3d687224d88367b51c7974cec1040c4b015772bfbeffac95face14c04a/ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc", size = 12116602, upload-time = "2025-09-04T16:49:18.892Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/c3/6e599657fe192462f94861a09aae935b869aea8a1da07f47d6eae471397c/ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727", size = 12868393, upload-time = "2025-09-04T16:49:23.043Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/d2/9e3e40d399abc95336b1843f52fc0daaceb672d0e3c9290a28ff1a96f79d/ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb", size = 12036967, upload-time = "2025-09-04T16:49:26.04Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/03/6816b2ed08836be272e87107d905f0908be5b4a40c14bfc91043e76631b8/ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577", size = 12276038, upload-time = "2025-09-04T16:49:29.056Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/d5/707b92a61310edf358a389477eabd8af68f375c0ef858194be97ca5b6069/ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e", size = 11901110, upload-time = "2025-09-04T16:49:32.07Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/3d/f8b1038f4b9822e26ec3d5b49cf2bc313e3c1564cceb4c1a42820bf74853/ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e", size = 13668352, upload-time = "2025-09-04T16:49:35.148Z" },
+ { url = "https://files.pythonhosted.org/packages/98/0e/91421368ae6c4f3765dd41a150f760c5f725516028a6be30e58255e3c668/ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8", size = 14638365, upload-time = "2025-09-04T16:49:38.892Z" },
+ { url = "https://files.pythonhosted.org/packages/74/5d/88f3f06a142f58ecc8ecb0c2fe0b82343e2a2b04dcd098809f717cf74b6c/ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5", size = 14060812, upload-time = "2025-09-04T16:49:42.732Z" },
+ { url = "https://files.pythonhosted.org/packages/13/fc/8962e7ddd2e81863d5c92400820f650b86f97ff919c59836fbc4c1a6d84c/ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92", size = 13050208, upload-time = "2025-09-04T16:49:46.434Z" },
+ { url = "https://files.pythonhosted.org/packages/53/06/8deb52d48a9a624fd37390555d9589e719eac568c020b27e96eed671f25f/ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45", size = 13311444, upload-time = "2025-09-04T16:49:49.931Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/81/de5a29af7eb8f341f8140867ffb93f82e4fde7256dadee79016ac87c2716/ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5", size = 13279474, upload-time = "2025-09-04T16:49:53.465Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/14/d9577fdeaf791737ada1b4f5c6b59c21c3326f3f683229096cccd7674e0c/ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4", size = 12070204, upload-time = "2025-09-04T16:49:56.882Z" },
+ { url = "https://files.pythonhosted.org/packages/77/04/a910078284b47fad54506dc0af13839c418ff704e341c176f64e1127e461/ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23", size = 11880347, upload-time = "2025-09-04T16:49:59.729Z" },
+ { url = "https://files.pythonhosted.org/packages/df/58/30185fcb0e89f05e7ea82e5817b47798f7fa7179863f9d9ba6fd4fe1b098/ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489", size = 12891844, upload-time = "2025-09-04T16:50:02.591Z" },
+ { url = "https://files.pythonhosted.org/packages/21/9c/28a8dacce4855e6703dcb8cdf6c1705d0b23dd01d60150786cd55aa93b16/ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee", size = 13360687, upload-time = "2025-09-04T16:50:05.8Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/fa/05b6428a008e60f79546c943e54068316f32ec8ab5c4f73e4563934fbdc7/ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1", size = 12052870, upload-time = "2025-09-04T16:50:09.121Z" },
+ { url = "https://files.pythonhosted.org/packages/85/60/d1e335417804df452589271818749d061b22772b87efda88354cf35cdb7a/ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d", size = 13178016, upload-time = "2025-09-04T16:50:12.559Z" },
+ { url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload-time = "2025-09-04T16:50:15.737Z" },
+]
diff --git a/scripts/swift-quality.sh b/scripts/swift-quality.sh
@@ -11,6 +11,20 @@ readonly -a SOURCE_PATHS=(
RadrootsTests
RadrootsUITests
)
+readonly -a MAINTAINABILITY_RULES=(
+ cyclomatic_complexity
+ file_length
+ function_body_length
+ function_parameter_count
+ large_tuple
+ type_body_length
+)
+readonly -a PYTHON_QUALITY_PATHS=(
+ scripts/maintainability_ratchet.py
+ scripts/package_contract.py
+ scripts/test_maintainability_ratchet.py
+ scripts/test_package_contract.py
+)
command -v swiftformat >/dev/null || {
echo "swift-quality: swiftformat is unavailable" >&2
@@ -29,3 +43,21 @@ swiftlint lint \
--silence-deprecation-warnings \
--config .swiftlint.yml \
"${SOURCE_PATHS[@]}"
+
+metric_arguments=()
+for rule in "${MAINTAINABILITY_RULES[@]}"; do
+ metric_arguments+=(--only-rule "$rule")
+done
+swiftlint lint \
+ --strict \
+ --quiet \
+ --no-cache \
+ --silence-deprecation-warnings \
+ --config .swiftlint.yml \
+ --baseline test-fixtures/swiftlint-maintainability-baseline.v1.json \
+ "${metric_arguments[@]}" \
+ "${SOURCE_PATHS[@]}"
+
+uv run --offline --project scripts/persona-verifier ruff format --check "${PYTHON_QUALITY_PATHS[@]}"
+uv run --offline --project scripts/persona-verifier ruff check "${PYTHON_QUALITY_PATHS[@]}"
+uv run --offline --project scripts/persona-verifier python scripts/maintainability_ratchet.py verify
diff --git a/scripts/test_maintainability_ratchet.py b/scripts/test_maintainability_ratchet.py
@@ -0,0 +1,156 @@
+from __future__ import annotations
+
+import json
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+SCRIPTS = Path(__file__).resolve().parent
+if str(SCRIPTS) not in sys.path:
+ sys.path.insert(0, str(SCRIPTS))
+
+import maintainability_ratchet as ratchet # noqa: E402
+
+
+class MaintainabilityRatchetTests(unittest.TestCase):
+ def _repository(self) -> tuple[tempfile.TemporaryDirectory[str], Path]:
+ temporary = tempfile.TemporaryDirectory()
+ root = Path(temporary.name)
+ (root / "Radroots").mkdir()
+ (root / "RadrootsTests").mkdir()
+ (root / "RadrootsUITests").mkdir()
+ (root / "scripts").mkdir()
+ (root / "test-fixtures").mkdir()
+ (root / "Radroots/App.swift").write_text("struct App {}\n", encoding="utf-8")
+ (root / "scripts/check.py").write_text(
+ "def check():\n return True\n", encoding="utf-8"
+ )
+ self._write_baseline(root)
+ return temporary, root
+
+ def _write_baseline(self, root: Path, **overrides: object) -> None:
+ baseline: dict[str, object] = {
+ "schema": "radroots.ios.maintainability-baseline.v1",
+ "schema_version": 1,
+ "source_revision": "c63002bcc4d3f6656e93aabe4fca6bd771376629",
+ "thresholds": {
+ "swift_file_lines": 600,
+ "python_file_lines": 800,
+ "python_function_complexity": 10,
+ },
+ "swift_file_exception": [],
+ "python_file_exception": [],
+ "python_complexity_exception": [],
+ "bounded_module": ["Radroots/App.swift", "scripts/check.py"],
+ }
+ baseline.update(overrides)
+ (root / ratchet.BASELINE_PATH).write_text(
+ json.dumps(baseline, sort_keys=True), encoding="utf-8"
+ )
+
+ def test_current_repository_satisfies_ratchet(self) -> None:
+ ratchet.verify(SCRIPTS.parent)
+
+ def test_new_oversized_swift_file_is_rejected(self) -> None:
+ temporary, root = self._repository()
+ with temporary:
+ (root / "Radroots/Large.swift").write_text("x\n" * 601, encoding="utf-8")
+ with self.assertRaisesRegex(ratchet.MaintainabilityError, "inventory"):
+ ratchet.verify(root)
+
+ def test_exception_cannot_grow_or_become_stale(self) -> None:
+ temporary, root = self._repository()
+ with temporary:
+ path = root / "Radroots/App.swift"
+ path.write_text("x\n" * 602, encoding="utf-8")
+ exception = [{"path": "Radroots/App.swift", "maximum_lines": 601}]
+ self._write_baseline(root, swift_file_exception=exception)
+ with self.assertRaisesRegex(ratchet.MaintainabilityError, "regressed"):
+ ratchet.verify(root)
+ path.write_text("struct App {}\n", encoding="utf-8")
+ with self.assertRaisesRegex(ratchet.MaintainabilityError, "inventory"):
+ ratchet.verify(root)
+
+ def test_new_oversized_python_file_is_rejected(self) -> None:
+ temporary, root = self._repository()
+ with temporary:
+ (root / "scripts/large.py").write_text("x = 1\n" * 801, encoding="utf-8")
+ with self.assertRaisesRegex(ratchet.MaintainabilityError, "inventory"):
+ ratchet.verify(root)
+
+ def test_new_complex_python_function_is_rejected(self) -> None:
+ temporary, root = self._repository()
+ with temporary:
+ branches = "".join(
+ f" if value == {index}:\n return {index}\n"
+ for index in range(10)
+ )
+ (root / "scripts/complex.py").write_text(
+ "def complex(value):\n" + branches + " return -1\n",
+ encoding="utf-8",
+ )
+ with self.assertRaisesRegex(ratchet.MaintainabilityError, "inventory"):
+ ratchet.verify(root)
+
+ def test_baseline_identity_threshold_and_order_are_closed(self) -> None:
+ temporary, root = self._repository()
+ with temporary:
+ self._write_baseline(root, source_revision="f" * 40)
+ with self.assertRaisesRegex(ratchet.MaintainabilityError, "identity"):
+ ratchet.verify(root)
+ self._write_baseline(
+ root,
+ thresholds={
+ "swift_file_lines": 601,
+ "python_file_lines": 800,
+ "python_function_complexity": 10,
+ },
+ )
+ with self.assertRaisesRegex(ratchet.MaintainabilityError, "threshold"):
+ ratchet.verify(root)
+ self._write_baseline(
+ root,
+ bounded_module=["scripts/check.py", "Radroots/App.swift"],
+ )
+ with self.assertRaisesRegex(ratchet.MaintainabilityError, "inventory"):
+ ratchet.verify(root)
+
+ def test_bounded_module_must_exist_and_remain_small(self) -> None:
+ temporary, root = self._repository()
+ with temporary:
+ self._write_baseline(root, bounded_module=["Radroots/Missing.swift"])
+ with self.assertRaisesRegex(ratchet.MaintainabilityError, "absent"):
+ ratchet.verify(root)
+ (root / "Radroots/App.swift").write_text("x\n" * 601, encoding="utf-8")
+ self._write_baseline(
+ root,
+ swift_file_exception=[
+ {"path": "Radroots/App.swift", "maximum_lines": 601}
+ ],
+ )
+ with self.assertRaisesRegex(ratchet.MaintainabilityError, "bounded"):
+ ratchet.verify(root)
+
+ def test_exception_rows_are_exact_unique_and_ordered(self) -> None:
+ invalid = [
+ [
+ {"path": "b", "maximum_lines": 601},
+ {"path": "a", "maximum_lines": 601},
+ ],
+ [
+ {"path": "a", "maximum_lines": 601},
+ {"path": "a", "maximum_lines": 602},
+ ],
+ [{"path": "a", "maximum_lines": 601, "extra": True}],
+ ]
+ for rows in invalid:
+ with self.subTest(rows=rows):
+ with self.assertRaises(ratchet.MaintainabilityError):
+ ratchet._closed_exception_map(
+ rows, key_name="path", ceiling_name="maximum_lines"
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/scripts/test_package_contract.py b/scripts/test_package_contract.py
@@ -11,7 +11,7 @@ SCRIPTS = Path(__file__).resolve().parent
if str(SCRIPTS) not in sys.path:
sys.path.insert(0, str(SCRIPTS))
-import package_contract as contract
+import package_contract as contract # noqa: E402
class PackageContractTests(unittest.TestCase):
@@ -69,9 +69,7 @@ class PackageContractTests(unittest.TestCase):
with self.assertRaisesRegex(contract.PackageContractError, "unsupported"):
contract.parse_make_assignments(
"ifneq ($(UNREACHABLE),)\n"
- "override RADROOTS_FIELD_LIB_GIT_REV := "
- + "a" * 40
- + "\nendif\n"
+ "override RADROOTS_FIELD_LIB_GIT_REV := " + "a" * 40 + "\nendif\n"
)
def test_duplicate_xcconfig_assignment_is_rejected(self) -> None:
diff --git a/test-fixtures/maintainability-baseline.v1.json b/test-fixtures/maintainability-baseline.v1.json
@@ -0,0 +1,56 @@
+{
+ "schema": "radroots.ios.maintainability-baseline.v1",
+ "schema_version": 1,
+ "source_revision": "c63002bcc4d3f6656e93aabe4fca6bd771376629",
+ "thresholds": {
+ "swift_file_lines": 600,
+ "python_file_lines": 800,
+ "python_function_complexity": 10
+ },
+ "swift_file_exception": [
+ {"path": "Radroots/Runtime/RadrootsGeneratedRuntimeBackend.swift", "maximum_lines": 1696},
+ {"path": "Radroots/Runtime/RadrootsRuntimeClient.swift", "maximum_lines": 1371},
+ {"path": "Radroots/Runtime/RadrootsRuntimeModels.swift", "maximum_lines": 1244},
+ {"path": "Radroots/State/RadrootsAddStore.swift", "maximum_lines": 810},
+ {"path": "Radroots/State/RadrootsConfigurationStore.swift", "maximum_lines": 759},
+ {"path": "Radroots/Views/RadrootsSupportingViews.swift", "maximum_lines": 629},
+ {"path": "RadrootsTests/RadrootsAddStoreTests.swift", "maximum_lines": 1784},
+ {"path": "RadrootsTests/RadrootsStateMigrationTests.swift", "maximum_lines": 718},
+ {"path": "RadrootsUITests/RadrootsRemoteQualificationUITests.swift", "maximum_lines": 2212}
+ ],
+ "python_file_exception": [
+ {"path": "scripts/local-social-fixture.py", "maximum_lines": 2626},
+ {"path": "scripts/test_local_social_fixture.py", "maximum_lines": 1188}
+ ],
+ "python_complexity_exception": [
+ {"function": "scripts/local-social-fixture.py:BlossomHandler.do_PUT", "maximum_complexity": 11},
+ {"function": "scripts/local-social-fixture.py:FixtureState._publish_persona_event", "maximum_complexity": 13},
+ {"function": "scripts/local-social-fixture.py:FixtureState.upload", "maximum_complexity": 14},
+ {"function": "scripts/local-social-fixture.py:RelayHandler.handle", "maximum_complexity": 26},
+ {"function": "scripts/local-social-fixture.py:bounded_directory_inventory", "maximum_complexity": 12},
+ {"function": "scripts/local-social-fixture.py:directory_digest", "maximum_complexity": 16},
+ {"function": "scripts/local-social-fixture.py:exact_persona_test_node", "maximum_complexity": 11},
+ {"function": "scripts/local-social-fixture.py:load_exported_persona_attachments", "maximum_complexity": 31},
+ {"function": "scripts/local-social-fixture.py:matches", "maximum_complexity": 22},
+ {"function": "scripts/local-social-fixture.py:photo_attempt_matches", "maximum_complexity": 12},
+ {"function": "scripts/local-social-fixture.py:reconstruct_persona_result_v2", "maximum_complexity": 66},
+ {"function": "scripts/local-social-fixture.py:simulator_metadata", "maximum_complexity": 23},
+ {"function": "scripts/local-social-fixture.py:valid_blossom_authorization", "maximum_complexity": 29},
+ {"function": "scripts/local-social-fixture.py:valid_bud11_server_domain", "maximum_complexity": 12},
+ {"function": "scripts/local-social-fixture.py:valid_nostr_event", "maximum_complexity": 16},
+ {"function": "scripts/local-social-fixture.py:validate_persona_attempt_evidence", "maximum_complexity": 59},
+ {"function": "scripts/local-social-fixture.py:validate_persona_evidence", "maximum_complexity": 31},
+ {"function": "scripts/local-social-fixture.py:validate_persona_result", "maximum_complexity": 43},
+ {"function": "scripts/local-social-fixture.py:validate_persona_suite", "maximum_complexity": 28},
+ {"function": "scripts/local-social-fixture.py:verify_bip340", "maximum_complexity": 11},
+ {"function": "scripts/local-social-fixture.py:verify_persona", "maximum_complexity": 18},
+ {"function": "scripts/test_local_social_fixture.py:mutate_bud11_event", "maximum_complexity": 29}
+ ],
+ "bounded_module": [
+ "Radroots/Runtime/RadrootsUserMessageClassifier.swift",
+ "Radroots/Runtime/RadrootsUserMessages.swift",
+ "scripts/maintainability_ratchet.py",
+ "scripts/package_contract.py",
+ "scripts/test_package_contract.py"
+ ]
+}
diff --git a/test-fixtures/swiftlint-maintainability-baseline.v1.json b/test-fixtures/swiftlint-maintainability-baseline.v1.json
@@ -0,0 +1 @@
+[{"text":"final class RadrootsAppModel: ObservableObject {","violation":{"location":{"character":7,"line":55,"file":"Radroots\/App\/RadrootsAppModel.swift"},"severity":"warning","ruleName":"Type Body Length","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 255 lines"}},{"text":" init(","violation":{"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","location":{"file":"Radroots\/App\/RadrootsAppModel.swift","line":71,"character":3},"ruleDescription":"Function bodies should not span too many lines","severity":"warning","reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines"}},{"text":"actor RadrootsAddMediaCoordinator: RadrootsAddMediaHandling {","violation":{"severity":"error","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 427 lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"file":"Radroots\/Runtime\/RadrootsAddMediaCoordinator.swift","line":73,"character":1},"ruleDescription":"Type bodies should not span too many lines"}},{"text":" func uploadInBackground(","violation":{"location":{"character":3,"file":"Radroots\/Runtime\/RadrootsAddMediaCoordinator.swift","line":192},"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines","severity":"warning","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 55 lines"}},{"text":"#endif","violation":{"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 580","severity":"warning","location":{"file":"Radroots\/Runtime\/RadrootsAddMediaCoordinator.swift","character":1,"line":580}}},{"text":"private final class RadrootsGeneratedRuntimeBackend: RadrootsRuntimeBackend, @unchecked Sendable {","violation":{"ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 545 lines","severity":"error","ruleName":"Type Body Length","ruleIdentifier":"type_body_length","location":{"character":15,"line":134,"file":"Radroots\/Runtime\/RadrootsGeneratedRuntimeBackend.swift"}}},{"text":"}","violation":{"reason":"File should contain 1000 lines or less: currently contains 1696","ruleDescription":"Files should not span too many lines.","severity":"error","ruleName":"File Length","ruleIdentifier":"file_length","location":{"file":"Radroots\/Runtime\/RadrootsGeneratedRuntimeBackend.swift","character":1,"line":1696}}},{"text":"}","violation":{"ruleIdentifier":"file_length","location":{"character":1,"file":"Radroots\/Runtime\/RadrootsLifecycleCoordinator.swift","line":434},"ruleDescription":"Files should not span too many lines.","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 434","severity":"warning"}},{"text":"actor RadrootsRuntimeClient {","violation":{"ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","severity":"error","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 847 lines","location":{"file":"Radroots\/Runtime\/RadrootsRuntimeClient.swift","character":1,"line":441},"ruleIdentifier":"type_body_length"}},{"text":" private func start(","violation":{"ruleDescription":"Function bodies should not span too many lines","location":{"line":503,"file":"Radroots\/Runtime\/RadrootsRuntimeClient.swift","character":11},"ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 60 lines","ruleIdentifier":"function_body_length","severity":"warning"}},{"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<RadrootsRuntimeChange> {","violation":{"severity":"warning","location":{"file":"Radroots\/Runtime\/RadrootsRuntimeClient.swift","line":881,"character":3},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 88 lines","ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length"}},{"text":" private func beginShutdown() -> ShutdownOperation {","violation":{"ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines","severity":"warning","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleIdentifier":"function_body_length","location":{"file":"Radroots\/Runtime\/RadrootsRuntimeClient.swift","line":1061,"character":11}}},{"text":"}","violation":{"severity":"error","reason":"File should contain 1000 lines or less: currently contains 1371","location":{"file":"Radroots\/Runtime\/RadrootsRuntimeClient.swift","character":1,"line":1371},"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length"}},{"text":"}","violation":{"ruleIdentifier":"file_length","ruleName":"File Length","severity":"error","location":{"character":1,"line":1244,"file":"Radroots\/Runtime\/RadrootsRuntimeModels.swift"},"ruleDescription":"Files should not span too many lines.","reason":"File should contain 1000 lines or less: currently contains 1244"}},{"text":"final class RadrootsAddStore: ObservableObject {","violation":{"severity":"error","location":{"character":7,"file":"Radroots\/State\/RadrootsAddStore.swift","line":11},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 741 lines","ruleName":"Type Body Length"}},{"text":" func submit() async {","violation":{"ruleName":"Function Body Length","severity":"warning","location":{"character":3,"line":280,"file":"Radroots\/State\/RadrootsAddStore.swift"},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines"}},{"text":" func retract(_ card: RadrootsTodayCard) async {","violation":{"location":{"line":438,"character":3,"file":"Radroots\/State\/RadrootsAddStore.swift"},"severity":"warning","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"}},{"text":" private func uploadPendingMedia(_ initial: RadrootsDraftStatus) async throws","violation":{"location":{"line":524,"file":"Radroots\/State\/RadrootsAddStore.swift","character":11},"ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 60 lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"warning"}},{"text":"}","violation":{"reason":"File should contain 400 lines or less: currently contains 810","location":{"character":1,"line":810,"file":"Radroots\/State\/RadrootsAddStore.swift"},"ruleIdentifier":"file_length","severity":"warning","ruleDescription":"Files should not span too many lines.","ruleName":"File Length"}},{"text":"actor RadrootsConfigurationStore {","violation":{"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","severity":"error","ruleIdentifier":"type_body_length","location":{"file":"Radroots\/State\/RadrootsConfigurationStore.swift","line":78,"character":1}}},{"text":" func load() throws -> RadrootsAppConfiguration {","violation":{"reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","severity":"error","ruleIdentifier":"function_body_length","location":{"file":"Radroots\/State\/RadrootsConfigurationStore.swift","line":148,"character":5}}},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleName":"File Length","ruleIdentifier":"file_length","location":{"file":"Radroots\/State\/RadrootsConfigurationStore.swift","line":759,"character":1},"severity":"warning","reason":"File should contain 400 lines or less: currently contains 759"},"text":"}"},{"violation":{"ruleName":"Type Body Length","location":{"line":40,"file":"Radroots\/State\/RadrootsSessionStore.swift","character":1},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 431 lines","severity":"error","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"actor RadrootsSessionStore {"},{"violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","ruleIdentifier":"function_body_length","severity":"warning","location":{"file":"Radroots\/State\/RadrootsSessionStore.swift","character":10,"line":70},"ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines"},"text":" static func production("},{"violation":{"ruleName":"Cyclomatic Complexity","reason":"Function should have complexity 10 or less; currently complexity is 14","severity":"warning","ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited.","location":{"character":11,"line":182,"file":"Radroots\/State\/RadrootsSessionStore.swift"}},"text":" private func start(acceptingReconfiguration: Bool) async -> RadrootsSessionPhase {"},{"violation":{"location":{"character":11,"line":182,"file":"Radroots\/State\/RadrootsSessionStore.swift"},"ruleName":"Function Body Length","severity":"warning","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 64 lines","ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length"},"text":" private func start(acceptingReconfiguration: Bool) async -> RadrootsSessionPhase {"},{"violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleName":"Function Body Length","severity":"warning","ruleIdentifier":"function_body_length","location":{"character":11,"line":389,"file":"Radroots\/State\/RadrootsSessionStore.swift"},"ruleDescription":"Function bodies should not span too many lines"},"text":" private func reconcileIdentity(_ identity: RadrootsAppIdentity) async throws {"},{"violation":{"severity":"warning","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 493","ruleName":"File Length","ruleDescription":"Files should not span too many lines.","location":{"line":493,"file":"Radroots\/State\/RadrootsSessionStore.swift","character":1}},"text":"}"},{"violation":{"ruleIdentifier":"file_length","location":{"line":451,"file":"Radroots\/State\/RadrootsSupportingStores.swift","character":1},"reason":"File should contain 400 lines or less: currently contains 451","ruleName":"File Length","ruleDescription":"Files should not span too many lines.","severity":"warning"},"text":"}"},{"violation":{"severity":"warning","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 257 lines","location":{"line":13,"file":"Radroots\/State\/RadrootsTodayStore.swift","character":7}},"text":"final class RadrootsTodayStore: ObservableObject {"},{"violation":{"location":{"character":1,"file":"Radroots\/Views\/RadrootsAddView.swift","line":4},"reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines","severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","ruleIdentifier":"type_body_length"},"text":"struct RadrootsAddView: View {"},{"violation":{"ruleDescription":"Files should not span too many lines.","location":{"character":1,"line":560,"file":"Radroots\/Views\/RadrootsAddView.swift"},"ruleIdentifier":"file_length","ruleName":"File Length","severity":"warning","reason":"File should contain 400 lines or less: currently contains 560"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","location":{"character":1,"line":365,"file":"Radroots\/Views\/RadrootsSupportingViews.swift"},"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"warning","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines"},"text":"struct RadrootsSettingsView: View {"},{"text":"}","violation":{"ruleName":"File Length","severity":"warning","ruleIdentifier":"file_length","location":{"character":1,"file":"Radroots\/Views\/RadrootsSupportingViews.swift","line":629},"ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 629"}},{"text":"}","violation":{"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","severity":"warning","reason":"File should contain 400 lines or less: currently contains 493","location":{"character":1,"line":493,"file":"Radroots\/Views\/RadrootsTodayView.swift"},"ruleName":"File Length"}},{"text":"final class RadrootsAddStoreTests: XCTestCase {","violation":{"severity":"error","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"character":7,"line":6,"file":"RadrootsTests\/RadrootsAddStoreTests.swift"},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 665 lines"}},{"text":"private actor AddBackend: RadrootsRuntimeBackend {","violation":{"ruleIdentifier":"type_body_length","severity":"error","location":{"file":"RadrootsTests\/RadrootsAddStoreTests.swift","line":847,"character":9},"ruleDescription":"Type bodies should not span too many lines","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 579 lines","ruleName":"Type Body Length"}},{"text":" nonisolated static func schemas() -> [RadrootsAddSchema] {","violation":{"ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","severity":"warning","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 89 lines","location":{"file":"RadrootsTests\/RadrootsAddStoreTests.swift","line":934,"character":22}}},{"text":" private func makeStatus(","violation":{"ruleIdentifier":"function_parameter_count","ruleName":"Function Parameter Count","reason":"Function should have 5 parameters or less: it currently has 8","location":{"character":11,"file":"RadrootsTests\/RadrootsAddStoreTests.swift","line":1371},"severity":"warning","ruleDescription":"Number of function parameters should be low."}},{"text":" var counts: (enqueue: Int, retry: Int, cancel: Int, acceptedSettlement: Int) {","violation":{"ruleIdentifier":"large_tuple","reason":"Tuples should have at most 2 members","severity":"error","location":{"line":1635,"file":"RadrootsTests\/RadrootsAddStoreTests.swift","character":15},"ruleDescription":"Tuples shouldn't have too many members. Create a custom type instead.","ruleName":"Large Tuple"}},{"text":"}","violation":{"ruleName":"File Length","ruleDescription":"Files should not span too many lines.","severity":"error","location":{"file":"RadrootsTests\/RadrootsAddStoreTests.swift","line":1784,"character":1},"reason":"File should contain 1000 lines or less: currently contains 1784","ruleIdentifier":"file_length"}},{"text":"final class RadrootsRemoteQualificationTests: XCTestCase {","violation":{"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","severity":"warning","location":{"file":"RadrootsTests\/RadrootsRemoteQualificationTests.swift","line":7,"character":7}}},{"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {","violation":{"ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","ruleDescription":"Function bodies should not span too many lines","location":{"line":45,"character":3,"file":"RadrootsTests\/RadrootsRemoteQualificationTests.swift"},"ruleIdentifier":"function_body_length","severity":"warning"}},{"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {","violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleDescription":"Function bodies should not span too many lines","location":{"file":"RadrootsTests\/RadrootsRemoteQualificationTests.swift","character":3,"line":212},"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"warning"}},{"text":"final class RadrootsRuntimeClientTests: XCTestCase {","violation":{"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 334 lines","ruleDescription":"Type bodies should not span too many lines","location":{"file":"RadrootsTests\/RadrootsRuntimeClientTests.swift","character":7,"line":4},"ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"warning"}},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","location":{"line":598,"character":1,"file":"RadrootsTests\/RadrootsRuntimeClientTests.swift"},"ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 598","severity":"warning"},"text":"}"},{"violation":{"location":{"line":6,"character":7,"file":"RadrootsTests\/RadrootsStateMigrationTests.swift"},"severity":"error","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length"},"text":"final class RadrootsStateMigrationTests: XCTestCase {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","severity":"warning","location":{"file":"RadrootsTests\/RadrootsStateMigrationTests.swift","line":7,"character":5},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleIdentifier":"function_body_length"},"text":" func testRelayValidationMatchesRuntimeProfiles() throws {"},{"violation":{"location":{"character":1,"line":718,"file":"RadrootsTests\/RadrootsStateMigrationTests.swift"},"ruleName":"File Length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 718","severity":"warning","ruleIdentifier":"file_length"},"text":"}"},{"violation":{"location":{"line":207,"file":"RadrootsTests\/RadrootsSupportingStoreTests.swift","character":9},"reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 253 lines","severity":"warning","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length"},"text":"private actor SupportingBackend: RadrootsRuntimeBackend {"},{"violation":{"severity":"warning","reason":"File should contain 400 lines or less: currently contains 487","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","ruleIdentifier":"file_length","location":{"file":"RadrootsTests\/RadrootsSupportingStoreTests.swift","line":487,"character":1}},"text":"}"},{"violation":{"location":{"file":"RadrootsUITests\/RadrootsRemoteQualificationUITests.swift","line":4,"character":7},"severity":"error","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines","ruleName":"Type Body Length","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines"},"text":"final class RadrootsRemoteQualificationUITests: XCTestCase {"},{"violation":{"ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines","severity":"warning","ruleDescription":"Function bodies should not span too many lines","location":{"line":62,"file":"RadrootsUITests\/RadrootsRemoteQualificationUITests.swift","character":3},"ruleIdentifier":"function_body_length"},"text":" func testLocalSocialFiveFlowScenario() throws {"},{"violation":{"severity":"warning","ruleIdentifier":"function_body_length","location":{"character":3,"line":174,"file":"RadrootsUITests\/RadrootsRemoteQualificationUITests.swift"},"ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines"},"text":" func testLocalSocialDeterministicPersonas() throws {"},{"violation":{"ruleIdentifier":"function_body_length","severity":"warning","location":{"file":"RadrootsUITests\/RadrootsRemoteQualificationUITests.swift","character":3,"line":247},"ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func testRemoteBlossomUploadAndRecovery() throws {"},{"violation":{"location":{"character":11,"file":"RadrootsUITests\/RadrootsRemoteQualificationUITests.swift","line":573},"ruleName":"Cyclomatic Complexity","ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited.","severity":"warning","reason":"Function should have complexity 10 or less; currently complexity is 18"},"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {"},{"violation":{"location":{"character":11,"file":"RadrootsUITests\/RadrootsRemoteQualificationUITests.swift","line":767},"ruleName":"Cyclomatic Complexity","ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited.","severity":"warning","reason":"Function should have complexity 10 or less; currently complexity is 11"},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"warning","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleDescription":"Function bodies should not span too many lines","location":{"line":767,"character":11,"file":"RadrootsUITests\/RadrootsRemoteQualificationUITests.swift"}}},{"text":" static func environment(","violation":{"ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","location":{"character":10,"line":1736,"file":"RadrootsUITests\/RadrootsRemoteQualificationUITests.swift"},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","severity":"warning","ruleIdentifier":"function_body_length"}},{"text":"}","violation":{"severity":"error","location":{"line":2212,"character":1,"file":"RadrootsUITests\/RadrootsRemoteQualificationUITests.swift"},"reason":"File should contain 1000 lines or less: currently contains 2212","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","ruleName":"File Length"}}]
+\ No newline at end of file