Skip to content

Lint with oida, at zero baseline - #31

Merged
3lvis merged 10 commits into
mainfrom
ci/oida
Sep 17, 2026
Merged

3lvis merged 10 commits into
mainfrom
ci/oida

Conversation

@3lvis

@3lvis 3lvis commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Why

Pinwheel had no linter, no formatter and no pre-merge gate of any kind — Actions has been paused to manual dispatch since GitHub's macOS runners started flaking on the hostless capture tests, so every push since has landed on a promise. This adopts oida, the linter Networking and SwiftSync already run, at zero baseline, and puts a gate back on every push by running it where the flake has never reached: Linux.

1380 first-party violations at the start, 0 now, with 36 in-place disables that each carry their reason. (Corrected after merge: the body first said 21, which undercounted the directives.)

The approach

bin/lint fetches the pinned release, checksums it against .oida-checksum-<platform>, asserts the version after unzipping, and runs it. .oida.yml carries the rules, the commit hook calls bin/lint --staged, and .github/workflows/lint.yml calls bin/lint --check on ubuntu-latest in swift:6.3.2-noble. It is its own workflow rather than a job in ci.yml, because ci.yml is the paused one and lint has no reason to wait on it.

the --fix reformat and its corrections 1380 → 507
document rules scoped to what VOICE.md governs → 454
force casts, the force try, names, list and condition shapes → 427
the LEARNINGS pass (below) → 145
the prose that was left → 54
single-use void functions, folded or justified → 0

The LEARNINGS directory was the graduated pass's leftovers

136 notes, and 120 of them carry the same timestamp — one bulk split of an existing document rather than 136 sessions writing 136 lessons. Reading them against AGENTS.md, that document already carries what they found: "Containment is a UIKit job", "behaviour is a value with no views in it", "Theme is law", "Colours are trait-reactive, fonts are not", "One implementation per component", "A warning only re-emits on a build that recompiles", "Dump the runtime rather than stopping at a search result" — each is a note, already promoted. LEARNINGS.md's own step 2 says to "delete the file once the rule carries everything it said", and that half never ran.

So the notes go, and git log --diff-filter=D -- LEARNINGS/ reads them. Four rules AGENTS.md was missing graduated first: the zero-cooperation capture contract and the hosted-world routing behind it, that an OS bump silently empties SwiftUI's private shape storage, that rows get a tray and a screen gets a sheet, and that a hosting controller reports the size its old content had.

Two of them were stale in a way a symptom search would have found: one note says a captured surface must never be a List, and a later one says a raw List captures fully and "this is why the demo stays a raw List".

The prose

91 sentences across AGENTS.md, README.md, MIGRATION.md and LEARNINGS.md, 73 in AGENTS.md alone. VOICE.md asks for the positive core rather than the negative foil, and it reads better nearly every time: "never editing source and test in the same step" became "with the test landing in a step of its own ahead of any source edit".

The 21 that stay

Seven are recursive tree walkers with no call site to move into. The rest are seams where the rule's repair would be worse than the violation: the tray parts' show/detach vocabulary, whose inlining would have a parent reach past a part into what it hosts; PinwheelRecorder's statics, which forward to a private session a caller cannot name; and PinTrayCardPlacement, which owns the card's constraints. Each says so above the disable.

UITableViewCell.setDefaultSelectedBackgound() is gone — public, unused, and misspelled at every call site. MIGRATION.md carries the three lines that replace it.

Testing

Both tiers, local, green at every commit: unit 213/213 and hosted 16/16. The Demo app builds warning-clean against a throwaway derived-data path, which is the only way the warnings gate answers honestly.

Learnings

Three oida issues came out of this, which is the point of adopting it here:

  • #44 — document findings are dropped from the report the moment swift-format has an unformatted file to name. It fails closed, but a repository adopting oida starts unformatted, which is exactly when the document count reads zero: bin/lint --check reported 0 document violations against this tree where plain oida lint reported 424.
  • #46--fix rewrites .map(\.value) to a trailing closure inside a for-in header, which Swift warns about. On a repository that fails CI on any first-party warning, the corrector wrote the build CI rejects.
  • Orphaned doc comments and disables cannot coexist. // oida:disable:next between a /// and its declaration trips orphaned_doc_comment; above the doc it disables the wrong line. The shape that works is the reason above the doc and // oida:disable:this trailing the declaration.

Earned its keep: AGENTS.md § Built to be tested — "Almost every bug here has been the same shape: a second copy of the state". Folding selectedItem looked safe until the build showed it was passed to PinwheelIndexView as a closure, and folding wasPulled took a test off the seam whose contract it exists to hold. Both got reverted to the seam.

@3lvis
3lvis marked this pull request as ready for review September 17, 2026 04:52
@3lvis
3lvis merged commit 03efc93 into main Sep 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant