Rinter is a semantic policy CLI for Swift projects.
It reads Rinfile.swift, evaluates changed Swift code with deterministic SwiftSyntax AST checks, and fail-closes on runtime errors.
- Missed required calls (authorization, logging, transaction cleanup) are hard to catch in review.
- Rinter enforces these rules with static AST checks and fails CI when violations are found.
- macOS 13 or later (Apple Silicon and Intel)
brew tap novr/taps
brew trust --formula novr/taps/rinter
brew install novr/taps/rinterUniversal binary (Apple Silicon and Intel):
curl -LO https://github.com/novr/Rin/releases/download/v<version>/rinter_<version>_darwin.tar.gz
curl -LO https://github.com/novr/Rin/releases/download/v<version>/checksums.txt
shasum -a 256 -c checksums.txt
tar -xzf rinter_<version>_darwin.tar.gz
./rinter --help- Build from source: Swift 6.2 or later
swift build -c release
./.build/release/rinter --help- Create
Rinfile.swift:
let policy = Rin.Policy {
Target(
include: ["App/**/*.swift", "Features/**/*.swift"],
exclude: ["**/Generated/**", "**/*Mock*.swift"]
)
Rules {
Rule(id: "viewmodel_load_tracks_screen") {
MustCall(
receiver: .symbol("Analytics"),
method: "sendScreen",
onPath: .namedFunctions("load", ifEmpty: .skip)
)
}
.scope(include: ["**/*ViewModel.swift"])
.message("ViewModel.load must send screen analytics.")
.severity(.error)
}
}- Run:
rinter- Example output:
❌ Semantic policy violation(s) found.
Features/Home/HomeViewModel.swift:42:13: [viewmodel_load_tracks_screen]
Required call `[symbol(Analytics), sendScreen]` was not found.
❌ Semantic policy violations detected.
Same as Quick Start. Add exclude on Rule.scope to skip mocks and generated code:
.scope(
include: ["**/*ViewModel.swift"],
exclude: ["**/*Mock*.swift", "**/Generated/**"]
)Narrow files with Rule.scope and functions with onPath on each predicate.
Changed Swift files matched by Target(include:) are analyzed.
Use Target(exclude:) to skip paths.
Target(
include: ["app/**/*.swift", "src/**/*.swift", "Modules/**/*.swift"],
exclude: ["vendor/**", "tests/**"]
)Rule.scope limits which files a rule applies to (separate from onPath, which limits functions or catch units inside those files).
.scope(
include: ["**/*ViewModel.swift"],
exclude: ["**/*Mock*.swift"]
)Rule(id: "authorization_single") {
MustCall(
receiver: .symbol("Authorizer"),
method: "authorize",
onPath: .namedFunctions("execute", ifEmpty: .skip)
)
}
Rule(id: "authorization_any") {
MustCallAnyOf([
RuleCall(receiver: .symbol("Authorizer"), method: "authorize"),
RuleCall(receiver: .symbol("Authorizer"), method: "can")
], onPath: .namedFunctions("execute", ifEmpty: .skip))
}MustCall and MustCallAnyOf default to everyFunction(ifEmpty: .violate) when onPath is omitted — prefer namedFunctions or matchingFunctions with ifEmpty: .skip for named entry points.
Rule(id: "store_catch_cancellation") {
MustHandleError(
target: .case("cancelled"),
as: .through,
onPath: .everyCatch(ifEmpty: .violate)
)
}
.scope(
include: ["**/Domain/Store/*Store.swift"]
)
.message("Store catch blocks must ignore cancelled via control-flow exit.")
.severity(.error).through requires the target case branch to exit via return, break, or continue (not guard case … else { return } on the non-target side).
Literal typed-throws type name only (e.g. throws(AppError) in source). MustThrow evaluates only functions with a literal typed-throws annotation in the signature:
Rule(id: "api_run_throws_app_error") {
MustThrow(type: "AppError", onPath: .namedFunctions("run", ifEmpty: .skip))
}
.scope(include: ["**/API/**/*.swift"])
.message("API run() must declare throws(AppError).")
.severity(.error)Functions with plain throws, no throws clause, throws(any Error), or typealias-dependent types are skipped (not violations). A violation is reported only when a literal typed throw is present and its type name does not match.
Rule(id: "transaction_pair") {
WhenCalls(receiver: .symbol("DB"), method: "beginTransaction")
.mustAlsoCallAnyOf([
RuleCall(receiver: .symbol("DB"), method: "commit"),
RuleCall(receiver: .symbol("DB"), method: "rollback"),
])
}
.scope(include: ["**/Database*.swift"])
.message("Transactions must end with commit() or rollback().")
.severity(.error)By default, WhenCalls checks follow-ups in the same function as the trigger.
Install both skills (Cursor, global). Run one command per skill (do not combine multiple -s flags):
npx skills add novr/Rin -s rin-dsl-rinfile -g -a cursor -y
npx skills add novr/Rin -s rin-dsl-rule-extraction -g -a cursor -yrin-dsl-rinfile: applied automatically when writing or editingRinfile.swift- Invoke rule extraction:
/rin-dsl-rule-extraction
Skill references:
- skills/rin-dsl-rinfile/SKILL.md — DSL semantics and Rinfile authoring
- skills/rin-dsl-rule-extraction/SKILL.md — project rule extraction
rinter [--config <path>] [--rule <id>] [-a|--all-files] [--fail-on-empty] [--format text|json] [--check-config] [--verbose]-c,--config: path toRinfile.swift(default:Rinfile.swift)-r,--rule: evaluate only the specified rule id-a,--all-files: evaluate all Swift files under project root (not just git diff)--fail-on-empty: exit1when no Swift files are in scope after include/exclude filters (default: exit0with an info message, or[]in JSON mode)--format: output format —text(default) prints human-readable messages;jsonprints a violations array to stdout ([]on pass)--check-config: validate Rinfile syntax, unique rule IDs, non-empty rule bodies, and rule clauses without evaluating source files; configuration failures exit2-v,--verbose: verbose logs during evaluation--version: show rinter version-h,--help: show help
Alternative execution:
swift package rinter --help
mint run novr/rin@<tag> rinter --help
swift run rinter --helpname: Rinter
on: [push, pull_request]
jobs:
check:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Download release binary
env:
RINTER_VERSION: "v0.0.9"
run: |
curl -LO "https://github.com/novr/Rin/releases/download/${RINTER_VERSION}/rinter_${RINTER_VERSION#v}_darwin.tar.gz"
curl -LO "https://github.com/novr/Rin/releases/download/${RINTER_VERSION}/checksums.txt"
shasum -a 256 -c checksums.txt
tar -xzf "rinter_${RINTER_VERSION#v}_darwin.tar.gz"
chmod +x ./rinter
- run: ./rinter --config Rinfile.swift --fail-on-empty- Parse
Rinfile.swiftwith SwiftSyntax. - Collect changed Swift files from git diff (or all files with
--all-files), then applyTargetinclude/exclude filters. When no files remain in scope, exit0by default; use--fail-on-emptyto exit1instead. - Evaluate each rule per unit (function,
catch, or trigger call) usingonPathscopes. - Predicates:
MustCall,MustCallAnyOf,MustHandleError,WhenCalls,MustDeclare,MustThrow,WhenCalls(name:). - Exit non-zero on violations or runtime errors (
0pass,1violations,2errors).
- File-local AST evaluation only
- Receiver matching is syntax-based (
.symbol("name")matches base identifier exactly) - Co-occurrence checks do not guarantee call order or 1:1 pairing
- Helper delegation (calls moved to private helpers) is not detected
deinitandsubscriptare not attached to function scope metadata
Details: skills/rin-dsl-rinfile/SKILL.md
Special thanks to fuwasegu/guardrail.