field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

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:
M.swiftlint.yml | 4++--
MAGENTS.md | 9++++++++-
MMakefile | 6+++++-
MPackage.swift | 1+
MREADME.md | 11+++++++++++
ARadroots/Runtime/RadrootsUserMessageClassifier.swift | 264+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MRadroots/Runtime/RadrootsUserMessages.swift | 224+------------------------------------------------------------------------------
Ascripts/maintainability_ratchet.py | 340+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/package_contract.py | 131++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mscripts/persona-verifier/pyproject.toml | 5+++++
Mscripts/persona-verifier/uv.lock | 34++++++++++++++++++++++++++++++++++
Mscripts/swift-quality.sh | 32++++++++++++++++++++++++++++++++
Ascripts/test_maintainability_ratchet.py | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/test_package_contract.py | 6++----
Atest-fixtures/maintainability-baseline.v1.json | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest-fixtures/swiftlint-maintainability-baseline.v1.json | 2++
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