Development¶
loom is built and checked with Nix flakes. The repository's flake.nix is the
source of truth for the package, development shell, and checks.
Development shell¶
Enter the shell before running local Common Lisp commands:
The shell also provides test and coverage as interactive aliases. Use the
loom-* commands for scripts and CI because they are executable commands and
work with nix develop -c.
The shell provides SBCL, ASDF dependencies, the test runner, and the MkDocs Material toolchain. The source tree is organized by shared editor layers and feature packages; see the architecture page for the composition boundaries.
Nix pins the library revisions in flake.lock, while loom.asd declares the
package version consumed by ASDF and the executable. When upgrading a library,
inspect its ASDF :depends-on list and
keep the corresponding sibling inputs explicit in flake.nix; this keeps the
development shell, package build, and test system on the same dependency
graph.
Only release-tagged sibling packages are admitted to the production dependency
graph. A newer repository is evaluated separately, but remains out of flake.nix
until it publishes a release tag and its API is covered by a focused integration
test; this keeps upgrades reproducible and prevents an unstable package from
entering the build through a transitive dependency.
It also provides cl-weave for the test DSL and paredit for structural
Common Lisp inspection and editing. The flake's paredit-lint check parses the
Lisp source set before packaging, so the same structural syntax gate is
available locally and in CI.
The test system declares every package used directly by its tests, including
the cl-weave generators and package-specific integration helpers. Keep these
ASDF edges explicit when rearranging implementation dependencies; tests should
not become loadable only because an unrelated production dependency currently
re-exports them.
Common Lisp authoring policy¶
Use defmacro for repetitive declarations whose variation is known at
compile time: command families, command-spec catalogs, codecs, and other
small declarative tables are generated from one readable source form. Keep
runtime state transitions and domain calculations as ordinary functions so
they remain directly callable, testable, and independent of macro expansion.
Do not introduce an adapter merely to rename or forward an operation already
provided by a nerima-lisp package; depend on that package at the owning
boundary instead.
Property and fuzz tests use cl-weave generators when an invariant spans a
large input space. Example-based it-each cases remain appropriate for
protocol tables and boundary contracts, while it-property and it-fuzz
should exercise pure data transformations and CPS callbacks. A green
coverage report does not justify adding tests for unreachable macroexpansion
forms or defstruct default initializers: report those SB-COVER limitations
separately and maximize executable expression and branch coverage.
Worktree workflow¶
The main branch is the integration point for completed work. Before
integrating a worktree, inspect its staged, unstaged, and untracked files and
compare its commits with main. Integrate only a complete work unit, then
remove the worktree after confirming that main contains it. Empty or
duplicate worktrees should be removed without creating an additional branch.
For a detached worktree containing unstaged changes, first run the focused or
full test command, review the diff, and commit the complete work unit in that
worktree. Then fast-forward main to that commit, verify the commit and clean
status from a main checkout, and remove the worktree with git worktree
remove. Do not delete a worktree until its commit is reachable from main.
When a named branch already contains the complete work unit, keep its existing
commits and commit any remaining unstaged changes separately. Verify that the
current remote main is an ancestor of the branch tip, then push the branch
tip to main as a fast-forward. If the branch tip is already in main, do not
create a duplicate commit. After confirming the remote tip, remove the worktree
and delete both the finished local branch and its remote branch. Worktrees
outside the repository's active worktree root may be inspected but must be
removed by the process that owns that location. Treat a finished branch that
is still checked out there as an owner-managed cleanup item and report it
without deleting the worktree or branch from this workflow.
Verification¶
Run the focused commands directly when iterating:
nix build
nix develop -c sbcl --script run-tests.lisp
nix fmt -- --ci
paredit inspect workspace --output json .
For a complete source lint, pass Lisp files in bounded batches (or one file at a time) when the shell would otherwise exceed the operating system's argument limit:
loom/test loads the Lisp unit and integration tiers declared in loom.asd in
serial order. t/unit/ verifies pure domain and application behavior, while
t/integration/ verifies package boundaries and external-system seams. The
executable tier in t/e2e/ is a separate Unix PTY suite run against the built
binary. It is a flake app rather than a checks.* derivation because the Nix
build sandbox has no pseudo-terminal, so nix flake check does not run it:
nix run .#e2e # every scenario
nix run .#e2e -- --list # scenario names
nix run .#e2e -- --only project # scenarios whose name contains "project"
The CI workflow runs nix flake check and the PTY app as separate jobs. The
PTY job builds .#default before running nix run .#e2e on ubuntu-latest,
where a real pseudo-terminal is available outside the Nix build sandbox.
The app builds packages.default and passes its bin/loom as LOOM_BINARY.
To drive a different binary, run the entry point directly inside the
development shell, which carries Python with pyte:
Each scenario runs the editor under a fresh scratch HOME and a temporary
directory, reconstructs the 80x24 screen from the PTY byte stream with
pyte, and waits on screen state rather than fixed delays. The runner prints
how many registered command-spec names the scenarios exercise; pass
--require-full-coverage to fail when that count is short.
The ordinary test process has a 1,800-second outer timeout and cl-weave's test runner gives each example a 40-second timeout. Coverage has a 1,800-second outer timeout. The PTY runner applies a 10-second deadline to each complete scenario, reserving five seconds for cleanup. Real cleanup must be verified against that total budget; the current failures are recorded in the verification ledger. These limits are part of the development contract and should be changed only with a measured reason.
Feature code that invokes an external command uses run-shell-command, which
has a 30-second default timeout. Callers can pass :timeout-seconds when a
command needs a different bounded duration; a timeout is returned as a
structured result with exit code 124.
Git commands use cl-vcs-kit directly with an argv list and structured
process results; cl-resilience-kit adds a cooperative deadline around each
operation. Git paths are not assembled into shell command strings.
Run the full CI-equivalent check, including the test suite, package build, formatter, paredit structural syntax check, coverage, and the strict MkDocs build, with:
To build the documentation independently, pass the repository's MkDocs configuration explicitly:
checks.default and checks.coverage both build inside the same PTY-less Nix
sandbox, so both set LOOM_SANDBOXED_CHECK; tests that spawn a real child
process over a PTY or a pipe skip visibly (in cl-weave's "N skipped" count)
rather than hanging inside that sandbox. apps.test and the development
shell's test alias do not set it, so the same tests run for real outside Nix
builds.
Coverage¶
Coverage is generated outside the checkout:
The flake's coverage output is a report directory rather than a runnable
program. Use nix build .#coverage to materialize it in the Nix store, or use
the command above to choose a local output directory.
The cl-weave runner covers Loom's src/ and packages/ trees and writes
coverage.data plus the HTML report under the selected coverage directory.
It rejects a no-test run and an empty source-expression selection. Record
SB-COVER's expression and branch totals separately; branch coverage does not
establish full expression coverage. SB-COVER is process-local, so top-level
declarations and the child-process-only loom:main path can remain unexecuted
in the report. Those forms are reported rather than hidden.
Set LOOM_COVERAGE_MIN_EXPRESSIONS and/or LOOM_COVERAGE_MIN_BRANCHES to make
the corresponding percentage a failing quality gate. Values must be between 0
and 100; raise them incrementally as executable coverage improves toward 100%.
Coverage debt triage¶
Treat an uncovered executable expression or branch as coverage debt. First map
the uncovered location back to its source form, then add a higher-level test
that exercises the user-visible behavior. Do not add tests solely to evaluate
defstruct slot declarations, type declarations, default forms, or other
top-level forms that SB-COVER instruments but that do not represent a runtime
decision in Loom. Do not lower a threshold or alter instrumentation to make a
report green; document a genuine harness limitation and keep the expression
and branch totals visible instead.
REPL workflow¶
Inside the development shell, load the system into a REPL:
The test system can also be loaded from a REPL with
(asdf:load-system "loom/test"); its public runner is
loom/test:run-tests.
Concurrency benchmark¶
The benchmark compares synchronous directory listing with the bounded file-tree prefetch and render-lane drain path:
It reports timings, accepted tasks, and derived speedup. The benchmark is observational and does not define a performance target.