Mirror the shared Swift rules canon (rules-swift, commit 4ebb892) into docs/rules-swift/ as a read-only reference alongside the repo's tailored docs/rules/. PROVENANCE.md records the source, commit, and what was included or excluded; the upstream repo's own VCS, CI, and lint tooling is omitted as machinery rather than rule content. Exclude the vendored tree from the style gate (scripts/check-style.sh) and the pre-commit hook: the canon's enforcement docs quote the forbidden attribution phrases to define them, the same reason the repo's own enforcement files are already excluded.
6.1 KiB
Tiledown Conventions
The Swift coding conventions for Tiledown. They keep the codebase consistent, testable, and portable. Read this before opening a PR. When in doubt, read the surrounding files and match what is already there: consistency with existing code outranks personal preference.
These are the Swift domain conventions: the language- and platform-specific
craft. They sit on top of a vendored core spine of cross-cutting engineering
discipline (the optimal-path ethic, proof and verification, testing discipline,
systematic debugging, commits, and git process). That spine lives in
core/; read it first, the Swift rules build on it and do not
repeat it.
This page is the overview. The full per-area Swift rules live in
docs/rules/ (index at docs/rules/README.md), and
the cross-cutting rules in docs/rules/core/.
Language
Swift for everything. The single exception is JavaScript, and only where it is intrinsic to the output: client-side tiles (Mermaid, charts, forms, polls) emit HTML and JS that run in the visitor's browser. JS is never used for build logic or tooling.
Engineering principles
- Optimal over fast. Respect existing code and idioms. Clarify ambiguity before coding rather than assuming requirements. When a real trade-off exists, surface two or three options instead of guessing.
- Progressive architecture. Start with the simplest thing that works. Add a protocol only when a second concrete consumer exists. Generalise only when a pattern has actually emerged. Do not pre-abstract.
- Make impossible states unrepresentable. Use exhaustive enums with
associated values. Never force-unwrap (
!,try!) in shipping code. Errors carry both a human-readable reason and an actionable recovery path. - Testable by design. Inject every collaborator through the initialiser. Test behaviour through the public API, not implementation details.
- Profile, then optimise. Value semantics by default. Pick the right data structure first. Optimise only with a profile in hand.
Dependency injection
- No singletons. No
static let shared, no process-wide config reached through static accessors. Every dependency appears at theinitsite so coupling is visible, testable, and removable. - Every external collaborator goes through
init. Not method parameters at the call site, not static fallbacks, not environment lookup. Pure free functions that compute from their arguments are fine. - Cross-module seams are protocols, not concrete imports. A library module does not reach into another module's concrete types. The executable (the composition root) is the only place that wires concretes together.
- No closure typealiases for named cross-module contracts. Use a named
protocol. Closures as ordinary method parameters (
onProgress:) are fine.
Namespacing and file layout
- Every public type lives under an
enum/structnamespace that mirrors its folder. No public type at file scope. ReadingModule.Sub.Leafshould tell you where the type lives and what it does. - Namespace anchors are caseless
enum. Usestructonly when the type is also a value. Neverclassfor a namespace. Shared mutable state is anactoror an injected value, never aclass. - Drop redundant context. Under
Availability, the error isAvailability.Error, notAvailabilityError. - Concrete types are declared via extensions on the leaf namespace. Conformances may be separate extensions on the qualified path.
- One non-private type per file. Private helper types may co-locate when they exist only to support the main type.
- File naming. A file declaring
extension Foo.Bar { public struct X }is namedFoo.Bar.X.swift(dots, matching the qualified name). Anchor files contain only the namespace declaration, no implementation.
Concurrency
- Swift 6 strict concurrency is on. Types crossing concurrency boundaries are
Sendable; prove it, do not silence it with@uncheckedunless you have a documented reason. - Shared mutable state goes in an
actor. UI-affine state is@MainActor. - Use structured concurrency. No arbitrary
Task.sleepto paper over ordering.
Cross-platform
Tiledown builds and runs on Apple platforms and Linux, with Windows where explicitly noted. The TileKit engine itself targets macOS and Linux; the broader cross-platform discipline (and the CI matrix in cross-platform.md) extends to Windows for code that opts into it.
- Guard platform-divergent code and abstract platform-specific dependencies behind a protocol seam: one implementation per platform, wired by the composition root. Subprocess use is allowed.
- Prefer pure-Swift implementations over Foundation-only conveniences in the core.
- On the server side, prefer cross-platform libraries (for example AsyncHTTPClient over URLSession, swift-log over os.log).
Testing
- Use the Swift Testing framework:
@Test,@Suite,#expect. Not XCTest. - One behaviour per test. Descriptive names. Deterministic data. No live network or filesystem dependence; inject test doubles.
- One test target per source target, named
<SourceTarget>Tests. - Use parameterised tests (
@Test(arguments:)) for families of similar cases.
Verification before "done"
The discipline lives in core/verification.md: no
claim of done, fixed, or passing without fresh command output in the same response.
For Swift, match the command to the claim:
| Claim | Command |
|---|---|
| Build succeeds | swift build |
| Tests pass | swift test (cite the count, e.g. 42 / 42) |
| Bug fixed | the test that reproduced it, now passing |
"Looks good" and "should pass" are not evidence.
Commits, branches, PRs
See core/commits.md and
core/git-discipline.md, and the project's
CONTRIBUTING.md. In short: Conventional Commits, one focused
change per PR, a CHANGELOG.md entry for any change touching shipping source, and
no AI attribution or em dashes in any committed text.