Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.context/
DerivedData/

# Xcode user-specific files
*.xcuserstate
Expand Down
43 changes: 43 additions & 0 deletions .swiftlint-autofix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
excluded:
- DerivedData
- .context

only_rules:
- attribute_name_spacing
- closing_brace
- closure_end_indentation
- closure_spacing
- colon
- comma
- comma_inheritance
- comment_spacing
- control_statement
- duplicate_imports
- empty_enum_arguments
- empty_parameters
- empty_parentheses_with_trailing_closure
- function_name_whitespace
- implicit_optional_initialization
- leading_whitespace
- literal_expression_end_indentation
- modifier_order
- no_space_in_method_call
- opening_brace
- operator_usage_whitespace
- period_spacing
- protocol_property_accessors_order
- redundant_discardable_let
- redundant_void_return
- return_arrow_whitespace
- sorted_imports
- statement_position
- trailing_comma
- trailing_newline
- trailing_semicolon
- trailing_whitespace
- unneeded_parentheses_in_closure_argument
- vertical_whitespace
- vertical_whitespace_between_cases
- vertical_whitespace_closing_braces
- vertical_whitespace_opening_braces
- void_return
1 change: 1 addition & 0 deletions .swiftlint-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parent_config: .swiftlint.yml
61 changes: 61 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
reporter: xcode
strict: true

included:
- solipsistweets
- orion

excluded:
- DerivedData
- .context

disabled_rules:
# Broad shape limits are intentionally disabled so safety checks stay useful.
- cyclomatic_complexity
- file_length
- function_body_length
- function_parameter_count
- identifier_name
- large_tuple
- line_length
- nesting
- type_body_length

# Keep style-only noise out of strict build gates unless existing code is
# baselined or fixed separately.
- colon
- comma
- function_name_whitespace
- no_space_in_method_call
- opening_brace
- statement_position
- switch_case_alignment
- trailing_comma
- trailing_newline
- trailing_whitespace
- vertical_whitespace

opt_in_rules:
- contains_over_filter_count
- contains_over_filter_is_empty
- discarded_notification_center_observer
- empty_count
- empty_string
- fallthrough
- fatal_error_message
- force_unwrapping
- implicitly_unwrapped_optional
- identical_operands
- no_empty_block
- private_action
- private_outlet
- private_subject
- private_swiftui_state
- redundant_nil_coalescing
- toggle_bool
- unhandled_throwing_task
- unneeded_escaping
- unneeded_throws_rethrows
- unowned_variable_capture
- weak_delegate
- yoda_condition
65 changes: 65 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# AGENTS.md

## Build And Verification

Use the fastest verification script that covers the files you touched.

Use Verify Fast (`scripts/verify-fast.sh`) for changes isolated to one app target. Set `SCHEME=orion` when the change only needs the Orion target; otherwise the script defaults to `solipsistweets`.

Use Verify Full (`scripts/verify-full.sh`) when a change touches shared Swift files under `solipsistweets/`, project settings, build scripts, assets used by both apps, or anything that should compile for both app schemes. It builds the shared `Verify Full` scheme, which compiles both app targets in one Xcode invocation. This repository has two app schemes, `solipsistweets` and `orion`, and no watch target.

For production-scheme validation such as app packaging, project wiring, signing-sensitive behavior, platform support, or release-only settings, run the affected shared Xcode scheme directly with the needed destination/configuration. There is no separate watch/full verification script in this repo.

All verification scripts use repo-local `DerivedData/`, pass `-disableAutomaticPackageResolution`, disable the compiler index store with `COMPILER_INDEX_STORE_ENABLE=NO`, disable debug dylib generation with `ENABLE_DEBUG_DYLIB=NO`, disable testability with `ENABLE_TESTABILITY=NO`, disable previews/string-symbol/localized-string generation, and use simulator-only no-signing settings `CODE_SIGNING_ALLOWED=NO` plus `CODE_SIGN_STYLE=Manual`. This repo currently has no SwiftPM package dependencies; do not add package-resolution/cache infrastructure unless dependencies are introduced.

For simulator builds, the app-only and fast verification scripts constrain `ARCHS` to the host architecture by default to avoid building unused simulator slices. Override with `BUILD_ARCHS=...` only when broader simulator architecture coverage is intentional.

For build timing, use `scripts/time-build.sh`. It keeps timing build products under `.context/build-timing/` and enables Xcode's build timing summary. By default it times a clean `build` of the `Verify Full` scheme; set `SCHEME=orion`, `BUILD_ACTION=build-for-testing`, or `CLEAN=0` when needed.

## Tests

This repo does not maintain unit-test or UI-test targets, and tests are not part of routine development here. Do not add test targets, test schemes, test plans, XCTest/Testing dependencies, or test source folders as part of normal changes.

New code still needs verification. Use the fastest verification script that covers the files touched, and broaden to Verify Full (`scripts/verify-full.sh`) when the change affects shared code, project settings, or both app targets.

If a future change truly requires executable tests, first document why build, lint, and manual verification are insufficient and why the behavior can be tested with lower risk than leaving it untested. Keep that testing infrastructure scoped to the need and update this guidance in the same change.

## Project Settings

Keep app targets on Swift 6 language mode with warnings as errors, complete strict concurrency, strict memory safety, and explicit `any` existential checking. Do not weaken these settings to make a change compile; fix the source issue or document why a temporary exception is required.

Keep fast-build overhead low without bypassing lint: Xcode target lint phases run `scripts/swiftlint.sh build` with explicit config/script/source-directory inputs and stamp outputs so Xcode controls dependency analysis. Verification scripts apply build-test overrides to disable previews, testability, generated string catalog symbols, and localized string emission; real app schemes keep the Moods-style real-build settings.

Keep launch schemes useful for debugging. Shared launch schemes should reduce system log noise and make invalid geometry reports actionable when possible.

Release app products should validate products during build. Keep `VALIDATE_PRODUCT = YES` for production release targets.

Avoid broad platform expansion unless the product intentionally supports it. This repo supports iOS/iPadOS only; do not add Mac Catalyst, Designed for iPhone/iPad on Mac, visionOS, or XR support by accident while editing target settings.

Keep `ENABLE_USER_SCRIPT_SANDBOXING = NO`; project build phases may need access that Xcode's user script sandbox blocks.

## DerivedData

Keep DerivedData local to the worktree at `DerivedData/`. It is ignored by Git.

For a new worktree, `scripts/seed-derived-data.sh` can copy a warm sibling `DerivedData/` using APFS clone-copy semantics when available, then removes path-sensitive build state. Install the optional best-effort hooks with `scripts/install-git-hooks.sh`; the post-checkout hook seeds DerivedData if absent without blocking checkout on failure.

## Lint

SwiftLint is configured with focused safety/correctness rules in `.swiftlint.yml` and runs in strict mode. Broad size/name/shape rules and current style-only noise are disabled so formatting preferences do not drown out safety checks or block routine builds.

Run build-time lint with `scripts/swiftlint.sh build`; Xcode target phases run the same command during verification builds. Missing SwiftLint is a local warning but a CI error. Run the base config directly with `scripts/swiftlint.sh lint`. Run autofix-only style cleanup with `scripts/swiftlint.sh fix`.

Install the optional pre-commit hook with `scripts/install-git-hooks.sh`; it sets `core.hooksPath` to `scripts/git-hooks`, runs the separate `.swiftlint-autofix.yml` path with SwiftLint `--fix --format` on staged Swift files, re-stages fixes, and aborts if a staged Swift file also has unstaged edits. Keep broad style gates out of strict lint unless existing code is baselined or fixed separately.

## Source Practices

Prefer typed boundary models over raw dictionaries or half-decoded payloads. Use `String?` only when absence has meaning, keep one source of truth for mutable state, and prefer structs/enums unless identity, shared mutable state, UIKit inheritance, or Objective-C interop requires a class.

Use weak captures in long-lived closures, Combine sinks, timers, animation completions, and tasks that capture UI or store objects. Keep UI mutation on the main actor with `await MainActor.run { ... }` or `Task { @MainActor [weak self] in ... }`.

Treat app-owned static assets and parser setup as invariants. Fail fast when required named assets or static regular expressions are missing or invalid; handle user and server data as recoverable input.

For manual UIKit layout, do size-dependent work in `layoutSubviews()` or after `viewDidLayoutSubviews()`, ask subviews for size with `sizeThatFits(_:)`, prefer `bounds.size` plus `center`, and centralize padding and spacing constants.

Treat stale documentation as a correctness bug. When a change alters architecture, ownership, build behavior, invariants, or platform constraints, update the relevant repo documentation in the same change.
2 changes: 1 addition & 1 deletion orion/OrionApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ struct OrionApp: App {
@Environment(\.scenePhase) private var scenePhase
@State private var requestedURL: URL = RedditSiteProfile().startURL
@StateObject private var screenTimeTracker = OnScreenTimeTracker()
private let profile: SiteProfile = RedditSiteProfile()
private let profile: any SiteProfile = RedditSiteProfile()

var body: some Scene {
WindowGroup {
Expand Down
13 changes: 13 additions & 0 deletions scripts/git-hooks/post-checkout
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail

repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [[ -z "$repo_root" ]]; then
exit 0
fi

cd "$repo_root"

if [[ ! -d "$repo_root/DerivedData" && -x "$repo_root/scripts/seed-derived-data.sh" ]]; then
"$repo_root/scripts/seed-derived-data.sh" >/dev/null 2>&1 || true
fi
36 changes: 36 additions & 0 deletions scripts/git-hooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail

repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"

staged_swift_files=()
while IFS= read -r -d '' staged; do
staged_swift_files+=("$staged")
done < <(git diff --cached --name-only -z --diff-filter=ACMR -- '*.swift')

if [[ "${#staged_swift_files[@]}" -eq 0 ]]; then
exit 0
fi

unstaged_swift_files=()
while IFS= read -r -d '' unstaged; do
unstaged_swift_files+=("$unstaged")
done < <(git diff --name-only -z -- '*.swift')

for ((staged_index = 0; staged_index < ${#staged_swift_files[@]}; staged_index++)); do
staged="${staged_swift_files[$staged_index]}"
for ((unstaged_index = 0; unstaged_index < ${#unstaged_swift_files[@]}; unstaged_index++)); do
unstaged="${unstaged_swift_files[$unstaged_index]}"
if [[ "$staged" == "$unstaged" ]]; then
echo "error: $staged has both staged and unstaged edits; stage or stash one side before committing." >&2
exit 1
fi
done
done

"$repo_root/scripts/swiftlint.sh" autofix "${staged_swift_files[@]}"
git add -- "${staged_swift_files[@]}"

"$repo_root/scripts/swiftlint.sh" lint-autofix "${staged_swift_files[@]}"
"$repo_root/scripts/swiftlint.sh" build "${staged_swift_files[@]}"
16 changes: 16 additions & 0 deletions scripts/install-git-hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

cd "$repo_root"

chmod +x "$repo_root/scripts/git-hooks/post-checkout" "$repo_root/scripts/git-hooks/pre-commit"

if [[ "$(git config --get extensions.worktreeConfig || true)" == "true" ]]; then
git config --worktree core.hooksPath scripts/git-hooks
else
git config core.hooksPath scripts/git-hooks
fi

echo "Installed repo-local git hooks via core.hooksPath=scripts/git-hooks."
34 changes: 34 additions & 0 deletions scripts/seed-derived-data.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
target_dd="${DERIVED_DATA_PATH:-$repo_root/DerivedData}"
warm_dd="${1:-}"

if [[ -d "$target_dd" ]]; then
echo "DerivedData already exists: $target_dd"
exit 0
fi

if [[ -z "$warm_dd" ]]; then
sibling_root="$(dirname "$repo_root")"
warm_dd="$(find "$sibling_root" -maxdepth 3 -type d -name DerivedData -not -path "$target_dd" -print -quit 2>/dev/null || true)"
fi

if [[ -z "$warm_dd" || ! -d "$warm_dd" ]]; then
echo "No warm DerivedData found. Run a build to create $target_dd."
exit 0
fi

mkdir -p "$(dirname "$target_dd")"
if ! cp -cR "$warm_dd" "$target_dd" 2>/dev/null; then
cp -R "$warm_dd" "$target_dd"
fi

rm -rf \
"$target_dd/Build" \
"$target_dd/Index.noindex/Build" \
"$target_dd"/*/Build \
"$target_dd"/*/Index.noindex/Build

echo "Seeded DerivedData from $warm_dd"
71 changes: 71 additions & 0 deletions scripts/swiftlint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"

MODE="${1:-build}"
if [[ $# -gt 0 ]]; then
shift
fi

if [[ $# -eq 0 ]]; then
set -- solipsistweets orion
fi

if ! command -v swiftlint >/dev/null 2>&1; then
message="SwiftLint is not installed; install it with 'brew install swiftlint'."
if [[ "$MODE" == "build" && -z "${CI:-}" ]]; then
echo "warning: $message" >&2
exit 0
fi
if [[ "${CI:-}" == "true" || "${CI:-}" == "1" ]]; then
echo "error: $message" >&2
exit 1
fi
echo "error: $message" >&2
exit 127
fi

case "$MODE" in
build)
swiftlint lint \
--config "$ROOT_DIR/.swiftlint-build.yml" \
--working-directory "$ROOT_DIR" \
--strict \
--quiet \
--reporter xcode \
"$@"
;;
lint)
swiftlint lint \
--config "$ROOT_DIR/.swiftlint.yml" \
--working-directory "$ROOT_DIR" \
--strict \
--quiet \
--reporter xcode \
"$@"
;;
fix|autofix)
swiftlint lint \
--fix \
--format \
--config "$ROOT_DIR/.swiftlint-autofix.yml" \
--working-directory "$ROOT_DIR" \
--quiet \
"$@"
;;
lint-autofix)
swiftlint lint \
--config "$ROOT_DIR/.swiftlint-autofix.yml" \
--working-directory "$ROOT_DIR" \
--strict \
--quiet \
--reporter xcode \
"$@"
;;
*)
echo "usage: $0 [build|lint|fix|autofix|lint-autofix] [paths...]" >&2
exit 64
;;
esac
Loading