Sigil 0.21.0

release

A test-reporting honesty release, and the first one that can turn a previously green suite red. sigil test could report success while running fewer tests than it appeared to, or none at all, and nothing in its output said so. Both cases now fail the run.

Download 0.21.0Get started

It also adds (sigil grant), a standard library type for capability grants and the algebra for narrowing them. Nothing enforces a grant yet; this is the shared vocabulary, not a sandbox.

This is a breaking change. If your test/ directory holds a .sgl file that registers tests and is not named test-*.sgl or *-test.sgl, sigil test now exits 1 and names it. That file has never run. See "If this release turns your build red" below.

It reaches you without any change on your part. The sigil: field is a floor with an implicit 1.0.0 ceiling, so a project declaring 0.19, 0.20, 0.20.3 or ^0.20 admits 0.21.0 and picks it up on its next sigil deps install. There is no pre-1.0 version you can be pinned to that holds this back. The minor bump is a signal, not a barrier.

sigil test always reports how many files it ran

The summary's Files: line used to print only when discovery selected at least one file. A package whose tests are a native binary could lose its entire Sigil suite to a rename and still print Tests: 0 passed (0 total) under a green All tests passed!, with no Files: line at all, and exit 0. Zero tests run and every test passing looked identical. The line now always prints, including Files: 0 of 0 run.

A run that selects no Sigil test files fails

If discovery selects nothing while a .sgl file sits in test/ or tests/, sigil test exits 1 and names the files it skipped. A package with genuinely no Sigil tests has no .sgl under test/ and is unaffected.

Strict discovery is on by default

Discovery selects test-*.sgl and *-test.sgl only, so a file named anything else sits beside the suite and never runs. The summary cannot show this: a file that never matched is upstream of every count it reports, which is how a repository can publish Files: 5 of 5 run with a sixth file of real tests unrun beside the five.

sigil test now fails when a .sgl file that is a direct child of a test directory imports (sigil test), registers at least one test, and was not selected. It reads the file rather than running it, so a file that starts a server or calls exit is examined safely, and a file that mentions (sigil test) only in its comments is not flagged.

Files in subdirectories of test/ are never considered, because that is where fixtures, integration drivers and helper modules live. Across 175 checkouts holding 5193 test .sgl files, 1850 are not selected by discovery and 1647 of those are nested. The rule flags 28 files in four projects, and every one is a real test file.

If this release turns your build red

The message names the file:

✗ STRICT: 1 .sgl file(s) in a test directory register tests and were
  not selected by discovery. They have never run.
    - ./test/parser-regression.sgl

That file has never run, so the question the red is asking you is whether it should have. Both answers are a one line change.

If it should be running, rename it to test-parser-regression.sgl (or parser-regression-test.sgl). It joins the suite on the next run, and you get back however many tests it holds.

If it should not, because it is a live gate or a driver invoked by its own script, add it to .testignore in your project root. Globs work, so one line covers a family:

test/parser-regression.sgl
packages/myapp/test/*-live.sgl

.testignore is the same file discovery already honours, not a new mechanism.

If you cannot deal with it now, sigil test --no-strict restores the previous behaviour and you can come back to it. It is worth knowing that this is the larger of the changes available to you: it means editing your CI config, where the two fixes above are a rename or a single line, and it leaves the file unrun in the meantime.

(sigil grant): a shared vocabulary for capability grants

A new standard library module holding an immutable capability grant and the algebra for comparing and narrowing one. A grant is a set of facets, each naming a kind (fs, exec, net, import, eval, ffi, pty), a mode, and a resource pattern. A facet is a permission, and absence is denial:

(import (sigil grant))

(define g (grant fs: '(rw "/work") exec: '(allowlist "git" "sigil")))
(grant-covers? g (grant-from-specs '("fs:ro:/work/src")))   ; => #t
(grant-covers? g (grant-from-specs '("fs:rw:/etc")))        ; => #f

grant-covers? is the partial order, "has at least as much authority as". attenuate narrows. empty-grant permits nothing and grant-allow-all permits everything. grant-facets returns the facets for a consent prompt or an audit line, and grant=? compares authority rather than spelling, so two grants written differently that permit the same things compare equal.

Several tools in the Sigil ecosystem had each grown a grant table of their own, with their own idea of what a filesystem root or an allowed command meant. This is the type they can share, so a narrowing computed in one place means the same thing in another.

Narrowing composes, and cannot widen

(grant-covers? g (attenuate g narrowing)) holds for every pair of grants, so attenuating can only ever remove authority. attenuate also returns the greatest grant covered by both of its inputs rather than merely some grant covered by both, which is what makes narrowings safe to compose: applying two restrictions in either order gives the same authority, and a grant narrowed twice is exactly the intersection of the two, not something smaller that happens to be under both.

That distinction is easy to state and easy to get wrong, so the module ships 90 tests that check these as properties over a generated corpus of grants rather than as examples, along with the component boundary that keeps /a/bc from counting as under /a/b, and the argv[0] match that lets an allowlist entry git permit git and refuse /tmp/evil/git.

Grants serialize without changing what they permit

grant->specs renders a grant to strings like fs:rw:/work and exec:allowlist:git,sigil; grant-from-specs parses them back. Every spec string (sigil system grant) accepts today parses to the same authority here.

A grant round-trips through that surface preserving what it permits, and that holds by construction rather than by testing: a resource the grammar cannot carry unambiguously is refused when the grant is built, so the value that would survive the round trip as something broader cannot be created in the first place. The cost is worth knowing up front. An exec command cannot contain a comma, because the comma is what separates commands in exec:allowlist:. Filesystem paths may contain commas and colons, and both round-trip.

What this does not do

This release ships the value and the algebra, and nothing else. Nothing in Sigil consults a grant yet: there is no enforcement, no sandbox, and no VM involvement. Importing (sigil grant) does not restrict what your code can do.

It is useful now to any tool that already decides what it will allow and wants to express that decision in a form another tool can read, compare and narrow. The checking layer is later work.