Skip to content

Releases: GRimAce11/Keel

v1.4.0 — two bugs in the gate, and somewhere to put the findings

Choose a tag to compare

@GRimAce11 GRimAce11 released this 21 Sep 07:41

Two bugs in the gate, and somewhere to put the findings.

keel diff shipped in 1.3.0 as the answer to "check reports forty findings
on an inherited codebase, so nobody turns it on". A review of it found that the
command miscounted the one thing it exists to catch, and that check had been
quietly making its own reports unreadable at exactly the size they matter.

A new cycle counted twice

keel diff reported one introduced loop as two regressions:

Regressions (2)
✗ New cycle at feature scope        Articles → Settings → Articles
✗ New feature-dependency-cycle      Articles → Settings → Articles is a
                                    dependency cycle.
  2 regressions, 9 changes, 0 fixed.

Both entries are the same loop. The delta compares cycles against the
dependency graph directly — at every structural scope — and then ran the rule
checker as well, whose feature-dependency-cycle rule found it a second time.

It was inconsistent as well as wrong. A module-scope cycle counted once,
because no rule covers that scope; a feature-scope one counted twice. The
number depended on where the loop happened to be, which is not a distinction
anybody could have guessed from the output.

The rule is now left out of the finding comparison. Nothing is lost: the direct
comparison covers every scope the rule does and three it does not, and it is
the one with the stable key, so A → B → A and B → A → B stay one cycle.

The rule did carry the lines the loop was read from, and a regression with
nowhere to look is one nobody can act on — so the cycle entry now collects the
first reference behind each hop:

Regressions (1)
✗ New cycle at feature scope
  Articles → Settings → Articles
    Features/Articles/Presentation/ArticleListViewModel.swift:11
    Features/Settings/Presentation/SettingsViewModel.swift:11

A rationale printed once per finding

check prints a short explanation under each finding. Four screens holding a
networking client produced four findings of one rule — and four copies of the
same three-line paragraph. Five findings ran to 59 lines.

It lands hardest on the case the last release was built for. Forty findings on
an inherited codebase means the same handful of paragraphs repeated until
nobody reads any of them.

Findings are already ordered by rule, so the paragraph now sits at the head of
each run and the [rule-id] tag on every finding says which run it belongs to.
A detail written for one finding — "Add import SwiftData to this file" —
names that file and is still printed every time.

Findings that reach the pull request

keel check --github
keel diff origin/main..HEAD --github

Both commands could fail a pull request and then say why in a build log. They
now emit GitHub Actions annotations, so each finding lands on the line it is
about:

::error file=Shared/UI/ArticleBadge.swift,line=4,title=shared-code-depends-on-feature::…

It invents nothing — every finding already carried file:line. Severity
carries across unchanged, because an annotation that softened an error would
disagree with the exit code, and the exit code is what gates the build. On
diff, a regression is an error whatever severity the rule behind it carries,
a change is a warning, and a fix is not annotated: there is no line to put it
on.

One line the CLI printed unwrapped

1.3.0 wrapped what the CLI prints and missed detail, which is the call every
command reaches for most. At 80 columns keel document printed its
architecture summary as a single 143-character line and keel new its
component list as 157.

It now wraps only when wrapping helps. A line that fits is printed exactly as
given, so the columns that commands line up by padding survive; a line built
around one word longer than the terminal — cd and a long path — is left
whole, because breaking it strands cd away from what it applies to and the
path can no longer be copied.

check also drops a blank line before its summary, where two in a row had read
as a gap rather than a separator.

Upgrading

brew update && brew upgrade keel

Nothing is removed and no exit code changes. keel diff will report fewer
regressions than 1.3.x did for the same branch — that is the fix, not a
loosening: the extra ones were the same cycle counted twice, and a branch that
failed for a real reason still fails.

549 tests, 60 suites. All 13 generated component combinations build.

1.3.1 — a diagram that should never have been drawn

Choose a tag to compare

@GRimAce11 GRimAce11 released this 16 Sep 07:35

A diagram that should never have been drawn.

What was wrong

keel document drew the dependency graph as a mermaid flowchart, and on a
cycle-heavy project the result was a hairball rather than a picture. A real
PROJECT.md had seven features with five of them in one knot, and a layer
diagram with six nodes, sixteen arrows and five of the six pointing both in and
out.

The guard that exists for this measured the wrong thing. It refuses to draw
past 24 nodes, which is right for a wall of boxes and useless here: these had
six and seven.

flowchart TD places nodes in ranks, top to bottom, which needs a direction of
flow to exist. A node with arrows both in and out cannot be ranked, and once
several of them are mutually reachable the layout has nothing to sort by.

Density alone was not the problem. Four modules with five arrows between them
sit at the same density and draw perfectly well, because nothing points
backwards.

What changed

The measure is now the size of the largest knot — the biggest set of nodes that
all reach one another, found by merging cycles that share a node.

Merging is the part that matters. Two separate two-node loops are two small
knots and draw fine; counting every looping node together would call that a
four-node tangle and refuse a diagram worth having.

Three nodes in a loop still draw, being a triangle. Four is where it stops
being a picture, and Keel now says it instead:

### Feature dependencies

4 of 4 features depend on one another in both directions. A flowchart of that
is a tangle rather than a picture, so Keel does not draw one.
`keel inspect --graph` lists the edges and `keel check` reports the cycles.

The sentence is the finding. A project where five of six layers depend on each
other in both directions is worth knowing about, and saying it plainly beats
drawing a knot the reader has to decode before reaching the same conclusion.

A diagram beside it at a scope with no knot is unaffected and still draws, and
so is the two-feature cycle that shipped in 1.3.0.

Upgrading

brew update && brew upgrade keel

No flags, commands or exit codes changed. The only difference is what
keel document writes for a project whose dependencies loop back on
themselves.

1.3.0 — keel diff, and a way to adopt keel check

Choose a tag to compare

@GRimAce11 GRimAce11 released this 16 Sep 06:33

Two commands' worth of new ground: what a branch changed, and a way to switch
check on before everything is fixed.

keel diff

keel diff                     # working tree against HEAD
keel diff origin/main..HEAD   # what this branch did

keel check answers "what is wrong with this codebase". On a project you
inherited that is forty findings and no action, which is why nobody turns it on
in CI. keel diff answers "what did this branch make worse".

Regressions (2)
✗ New cycle at module scope
  Features → Shared → Features
✗ New shared-code-depends-on-feature
  Shared depends on the Articles feature.
    Demo/Shared/UI/ArticleBadge.swift:4

Changes (2)
! Added module dependency Shared → Features
! Dependency flow changed from Features → shared to Shared → features

Only regressions exit non-zero. Adding a feature is a change; adding a cycle is
a regression.

The split invents no new opinion. Keel's position has always been that the only
directions it will call wrong are the ones the project itself establishes —
shared code depended on by a feature, a layer pointing back outwards — plus
cycles, which need no rule to be wrong. Regressions are exactly that set, read
from the rules check already enforces, so the two commands cannot reach
different conclusions about the same code. New warnings are changes, not
regressions, because in check a convention never gets to fail a build.

Your working tree is never touched. Each revision is read in a temporary
git worktree and removed afterwards — never checkout, never stash.

Three failures get a real answer rather than a git error: a shallow clone is
told fetch-depth: 0 (CI checkouts default to depth 1, so this will be the
common one), a directory outside a repository is told keel inspect works
without history, and a revision from before the project existed is "nothing to
compare".

keel check --write-baseline

keel check --write-baseline   # accept what is there now
keel check --strict           # from now on, only new findings fail

.keel/baseline.json is found, not configured. Accepted findings are counted
and reported, never silently dropped. A baseline entry that no longer matches
is reported as fixed and does not fail — punishing somebody for fixing
something is how a tool gets turned off.

Findings are matched on rule and file, never on the line, so an unrelated edit
above a finding does not invalidate the file. The recorded count still matters:
a sixteenth finding in a file that had fifteen is a new one.

PROJECT.md draws the dependency graph

keel document now renders mermaid flowcharts, which GitHub displays: one for
how the project is partitioned (feature scope when features couple, module
scope otherwise) and one for how a request moves through the layers. Nodes on a
cycle are outlined. Past 24 nodes it says how many there are instead of drawing
them — a truncated dependency graph does not look truncated, and a missing edge
reads as an absent dependency.

No flag. It is simply in the document now.

document --check grew with it: module and layer edges are fingerprinted, so a
diagram cannot go stale while the file is reported current.

Output

Console gained terminal-width awareness, word wrapping with hanging indents,
a shared table primitive and a separator rule. keel check was emitting a
300-character rationale as one line and keel inspect a 277-character one.

At 80 columns, the longest non-path line across inspect, check --explain,
doctor and the graph reports is now 79 characters. Paths are never broken —
a path split across two lines cannot be copied.

Prose is capped at 100 columns even on a wide terminal; tables are not, because
a column of paths is scanned down rather than read across. A table too wide for
the terminal stacks rather than overflowing.

No new flags. NO_COLOR and the TTY check are unchanged.

--ai no longer repeats the document

The prompt now tells the agent what Keel already wrote — conventions,
architecture rules, prohibitions and current check findings — as exclusions.
It was asking for conventions and risks while withholding both, so the
reader met one finding twice: once under "Known concerns" as measured, once
under "Risks" as a suggestion, with nothing to say which to trust.

Documentation

The README is 131 lines with no collapsed blocks. Install is at line 18 and a
working command at line 24. Everything else moved to REFERENCE.md, lifted
whole rather than rewritten.

Upgrading

brew upgrade keel

Nothing was removed and no flag changed meaning. keel diff is new, and
keel check gained --write-baseline and --no-baseline.

Keel 1.2.2 — correct file names under symlinked paths

Choose a tag to compare

@GRimAce11 GRimAce11 released this 15 Sep 13:01

A generation bug that mangled file names under some install paths.

What was wrong

keel new could produce privateMyApp.xcodeproj, privateREADME.md and
private.gitignore — every leading path component fused to the word
"private" — whenever Keel's bundled templates resolved through a symlinked
directory.

Two mistakes compounding, both of which read as correct:

  • Stripping the template root was written as a substring replacement rather
    than a prefix strip, so it could match in the middle of a path.
  • FileManager.enumerator resolves symlinks in the URLs it yields, while a
    URL built by hand does not. A root spelled /tmp/x therefore enumerates as
    /private/tmp/x.

The substring then matched eight characters in, leaving /private glued to
whatever followed — and the renderer turned that into a file name.

Who was affected

Anyone whose Keel binary resolved through a symlinked path. A normal Homebrew
install is not one: /opt/homebrew is a real path, and both sides agreed, so
this never appeared there. It showed up running a Homebrew bottle out of an
extracted tarball, which lands under /private.

What changed

One helper now resolves both sides and strips a prefix, returning the whole
path on a miss rather than a hybrid of the two. The feature generator and the
source analyzer had the same symlink weakness with a safer fallback, and use it
too.

The regression test was checked against the bug rather than only against the
fix: putting the old line back makes it fail with exactly
["privateMyApp", "privateMyApp/MyAppApp.swift"]. An earlier version of the
test passed either way and would have been worthless.

Upgrading

brew upgrade keel

No behaviour, output or flags changed.

Keel 1.2.1 — installable on Sonoma again

Choose a tag to compare

@GRimAce11 GRimAce11 released this 15 Sep 12:17

A fix for people who could not install Keel at all.

What was wrong

Package.swift asked for swift-tools-version: 6.1, which requires Xcode
16.3 — and Xcode 16.3 cannot be installed before macOS 15. The Homebrew formula
meanwhile advertised support back to Ventura.

So on macOS 13 or 14, brew install GRimAce11/tap/keel ended in:

keel: A full installation of Xcode.app 16.3 is required to compile
Xcode 16.3 cannot be installed on macOS 14.

rather than a working binary. That has been true since v0.2.0.

What changed

The manifest now asks for Swift 6.0 tools, which ships with Xcode 16.0.
Nothing in it needed anything 6.1 added — the 6.1 came from the original
project scaffolding. Swift 6.0 tools still default to the Swift 6 language
mode
, so strict concurrency is unchanged and nothing was given up for it.

The tap's formula has been corrected to match reality rather than aspiration.

Upgrading

brew upgrade keel

Nothing else changed — no behaviour, no output, no flags. If you are on
macOS 15 and Keel already installed for you, this release does nothing you
will notice.

How it was found

A bottle build on a macos-14 runner, which is a fair imitation of the user
who had been hitting this. Worth saying plainly: no amount of checking the
formula's inputs would have caught it, because the URL, the digest and the
syntax were all correct the whole time.

Keel 1.2.0 — validates the architecture it reads

Choose a tag to compare

@GRimAce11 GRimAce11 released this 15 Sep 10:39

Keel now validates the architecture it reads, and can walk you through it.

keel explore

An interactive way into a codebase you did not write.

Probe
  Swift files   43        Features   2
  Types         62        Targets    2

    1. Architecture      5. Types by role
    2. Features          6. Architecture warnings
    3. Dependencies      7. Search
    4. Data flow         8. Exit

Pick a dependency and it shows the evidence path down to the line. Pick a
warning and it shows the rule, the severity, the source, and why that rule
exists. Pick a type and it shows what it refers to and what refers back.

Where the graph cannot establish something it says "Undetermined from static
analysis"
rather than offering the likeliest shape. It never invokes an
agent, and it works on a project that does not compile.

keel check validates real boundaries

It reads the same relationship graphs inspect does, so it can now report a
screen holding a networking client, a cycle between features, shared code
depending on a feature, or a layer pointing back outwards — each with the lines
it was read from.

Severity is derived from the evidence, not fixed per rule. An error needs
the relationship and both of its ends to be established by the code: a
SwiftUI View holding an @Model type is certain and fails; the same view
holding an ArticleRepository rests on a suffix and warns. A naming convention
never gets to fail a build.

--explain says why each rule exists and why the finding carries its severity.
--interactive walks the findings one at a time.

AI interprets rather than paraphrases

The optional agent now receives the relationship facts — what depends on what,
any cycles, the roles types fill, and the evidence behind each finding — and
cannot pass its own work off as measurement. Its reading is labelled
inferred, its advice suggested, and there is no field an agent can fill
that comes out labelled observed. A claim naming a type the project does not
contain is dropped.

Upgrading

brew upgrade keel

keel check may report findings it previously could not see, including
errors. Run it locally before wiring this version into CI.

keel document output changed shape, so re-run it to regenerate PROJECT.md.

Unchanged

Nothing here touches the network, needs an account, or invokes an agent.
new, inspect, document, check, explore and doctor all work offline
on a project that does not currently compile.

Keel 1.1.0 — reads what a codebase is made of

Choose a tag to compare

@GRimAce11 GRimAce11 released this 15 Sep 06:16

Keel now reads what a codebase is made of, not only what it declares.

What's new

keel inspect --relationships — how the project's own types refer to each
other. A superclass told apart from a protocol conformance, an initializer
parameter from any other parameter, a declared property from a mention in a
method body. Every edge carries the line that wrote it.

keel inspect --graph — imports and type references joined into one
answer
to "what depends on what", readable at target, module, feature and layer
scope.
It finds cycles between features, which imports structurally cannot show: in
a single-target app both features compile into the same module, so no import
ever crosses between them.

Architecture detection is no longer inference from names. Evidence now
says
which of three kinds it is — a relationship, a declaration, or a naming
convention — and the rule is mechanical: a conclusion resting only on names
can
never be reported as coming from the code. A composition root is a type that
builds the app's services, not a type called *Container.

Seven new dimensions, all read from relationships: what screens actually hold,
whether features depend on each other, whether layer boundaries hold, which
way
dependencies run between shared code and features, how far up the stack
persistence and networking are reached for, and where SwiftUI and UIKit meet.

Upgrading

keel document output changed shape, so a PROJECT.md generated by 1.0.2
will
not match one generated by this. Re-run keel document to regenerate it.

brew upgrade keel

Unchanged                                                         

Nothing here touches the network, needs an account, or invokes an agent.
new, inspect, document, check and doctor all still work offline on a
project that does not currently compile.

Keel 1.0.2 — one broken project no longer stops the scan

Choose a tag to compare

@GRimAce11 GRimAce11 released this 11 Sep 11:08

Keel 1.0.2 — one broken project no longer stops the scan

A patch release. Two fixes, both found by using Keel rather than by testing it.

Fixed

A leftover .xcodeproj aborted the whole scan. A directory named
Something.xcodeproj with no project.pbxproj inside it — a half-written or
abandoned project directory — made keel inspect, keel document and
keel check fail outright, even when a perfectly readable project sat beside it.
Unreadable projects are now skipped, and only a scan where nothing could be read
fails. A tool whose promise is working on projects that do not build should not
fall over at the first broken thing it finds.

The error said nothing useful. "Could not read Foo.xcodeproj" restated the
problem rather than diagnosing it. It now names the file that is missing and says
what its absence usually means.

If you installed 1.0.1 from the tap

Reinstall. The formula shipped the binary without the resource bundle holding
Keel's templates, so keel --version worked and keel new died with a fatal
error. That was a packaging fault rather than a Keel one, and it is fixed in the
tap:

brew update && brew upgrade GRimAce11/tap/keel

Install

Keel ships from its own Homebrew tap, not homebrew-core, so the formula name is
fully qualified:

brew install GRimAce11/tap/keel

GRimAce11/tap is the tap, keel is the formula. No separate
brew tap step is needed, and plain brew install keel will not
find it.

Verified

282 tests. Installed from the tap and exercised end to end on a clean install:
new, add feature, inspect, document, check, doctor, ai all run from
the installed binary, and the formula's own test block passes.

Nothing in 1.0 changed. See the
1.0 release for what Keel
does.

Keel 1.0 — Create, understand, and maintain iOS projects from the terminal

Choose a tag to compare

@GRimAce11 GRimAce11 released this 11 Sep 10:25

Keel 1.0 — Create, understand, and maintain iOS projects from the terminal

Keel generates iOS projects, reads ones it did not generate, explains their
architecture with the evidence behind every claim, and validates them against
the rules they already follow. AI is optional and never automatic.

Install

Keel ships from its own Homebrew tap, not homebrew-core, so the formula name is
fully qualified:

brew install GRimAce11/tap/keel

GRimAce11/tap is the tap, keel is the formula. No separate
brew tap step is needed, and plain brew install keel will not
find it.

Commands

keel new Generate a project containing only the components you asked for
keel add feature Add a feature to an existing project, in that project's own shape
keel inspect Targets, structure, dependencies, and the architecture they imply
keel document Write PROJECT.md from static analysis, with --check to catch drift
keel check Validate a project against the rules it already follows
keel doctor Diagnose whether this machine can build what Keel generates
keel ai Detect, select and verify a local agent — or use none

What Keel will not do

It will not guess. Every architecture finding carries the counts behind it and
says whether the evidence was the code itself or a naming convention. Where the
facts settle nothing, the answer is Undetermined rather than the likeliest
guess.

It will not fail your build over a convention. keel check reserves errors for
things that are structurally certain — an attribute used without its framework, a
scheme CI cannot see. A view model missing @MainActor is a warning that says
Keel may be wrong, because isolation can arrive from somewhere syntax cannot
follow.

It will not write a rule nobody follows. PROJECT.md states "view models are
@MainActor" only when they all are.

It will not invent documentation. Everything in PROJECT.md is derived from
the project, and re-running on an unchanged project produces an empty diff.

On the AI layer

Every core command works offline with no agent, no account and no key.

Agents are detected by reading PATH and are never executed — not even for a
version string. Selection is explicit, a project may lower the permission level
but never raise it, and --no-ai overrides everything.

What an agent receives is the analysis, not your source: the same derived facts
PROJECT.md already prints. The agent returns structured fields and Keel writes
the Markdown, so an agent that ignores instructions costs its own section and
nothing else. keel document --show-prompt shows you exactly what would be sent,
without sending it.

Verified

280 tests. All thirteen component combinations build against a simulator. A
generated feature compiles and its tests pass.

Exercised against seven iOS projects Keel did not generate — 45,000 lines of mixed
SwiftUI and UIKit, Combine, third-party packages — where it reported zero false
errors.

claude -p and codex exec were run against the real CLIs and answered.
gemini -p is confirmed as the correct flag. ollama run is the one shipped
invocation nobody has exercised yet.

Since 1.0.0

Fixed analysis scope in monorepos. Running keel inspect at the root of a
repository containing ios/, backend/ and web/ counted a Swift backend's
files as part of the app and fed its types to architecture detection. Scanning
now hangs off the directory containing the project file. For an ordinary project
nothing changes.

Requirements

macOS 13 or newer. Building from source needs Xcode 16.3 or newer, for Swift 6.1.

v0.1.0 — Project generation

Choose a tag to compare

@GRimAce11 GRimAce11 released this 10 Sep 17:12

First usable release. keel new MyApp generates a Swift 6 / SwiftUI project
that opens in Xcode, builds, and passes its tests.

Highlights

Nine components, each independently selectable. Answer the prompts, or pass
--no-networking and friends. Declining a component means its files are never
written — there is no APIClient.swift left behind to delete.

No dependencies. Not in Keel, and not in what it generates. URLSession,
async/await, Observation and Swift Testing cover it.

Works offline. Templates are compiled into the binary rather than fetched,
so generation is reproducible for a given Keel version and cannot fail on a
network hiccup.

What you get

  • Xcode project using synchronized folder groups — adding a source file
    needs no project file edit, and two people adding files on the same day get
    no .pbxproj merge conflict
  • MVVM with @Observable @MainActor ViewModels
  • One ViewState per screen instead of parallel data / isLoading /
    errorMessage properties, so a spinner cannot render on top of an error
  • Networking — APIClient with retry, Retry-After handling, typed
    APIError, envelope unwrapping
  • Dependency injection — AppContainer composition root
  • Persistence — SwiftData container and store protocol
  • Authentication — token storage, refresh coalescing, sign-out on 401
  • Keychain, Localization (String Catalog), Design system tokens
  • Example feature — a working list + detail screen with a DTO/domain split
  • Tests — Swift Testing, with stubs so ViewModel tests never touch a network

Not implemented yet

keel document, inspect, check, doctor and ai are declared and exit
non-zero with a message explaining what they will do. They land in later
phases — see the roadmap.

Install

git clone https://github.com/GRimAce11/Keel.git
cd Keel
swift build -c release
cp .build/release/keel /usr/local/bin/

Requires macOS 13+ to run, and Xcode 16+ to open generated projects. Homebrew
distribution is planned.

Verified

85 Keel tests, 20 generated-project tests, and 13 component combinations built
against the iOS simulator — including every module in isolation.