This guide describes a seed development machine setup for working on Consent Scheme. The repository is still early, so the required toolchain is intentionally small and will grow as implementation tickets land.
- Git
- GitHub CLI,
gh - Emacs, preferably a current stable release
- GNU Make
- ripgrep,
rg, for fast repository searches
Optional but useful:
- Chibi Scheme,
chibi-scheme, for optional portable R7RS Chibi checks and the reference implementation oracle runner - Gauche,
gosh, for additional reference implementation oracle coverage - Guile,
guile, and Sagittarius,sagittarius, for broader optional oracle comparison coverage - Racket,
racket, plus itsr7rspackage for developer oracle comparisons; install the package withraco pkg install --auto r7rs - CHICKEN Scheme,
csi, plus itsr7rsegg for developer oracle comparisons; install the egg withchicken-install r7rs - Gambit Scheme,
gsiandgsc, for developer oracle comparisons and host-compiled portable executable checks; Homebrew packages it asgambit-scheme - ShellCheck or other local lint tools for future scripts
Authenticate the GitHub CLI before working with issues or pull requests:
gh auth statusIf authentication is missing:
gh auth loginConfirm the repository remote:
git remote -vClone the repository and create a topic branch for each issue:
git clone git@github.com:tahoma/consent.git
cd consent
git switch -c author-name/issue-N/short-nameUse a short contributing author name as the branch prefix no matter which tools you use.
Before implementing a ticket, read:
- Repository agent instructions, for agentic or tool-assisted work
- Architecture and threat model
- Multi-host adapter and bootstrap strategy
- Secrets, local-only context, and redaction
- Naming convention
- Scheme style guidelines
- Scheme references
- R7RS-small report reference
- Roadmap note
- Contributing
- Project skill bundle, for task-specific workflow reminders
- The GitHub issue you are working on
The GitHub roadmap issue is the source of truth for dependency ordering. Start with dependency-free or explicitly unblocked issues.
- Keep canonical runtime concepts represented as Scheme-readable data.
- Treat the portable R7RS implementation as a first-class peer of the Emacs Lisp bootstrap, and as the long-term path toward self-hosted or native reader, evaluator, emitter, and REPL work.
- Treat portable R7RS Scheme as the default home for host-neutral behavior: semantic helpers, protocol datums, codecs, deterministic parsers, library surfaces, fixtures, and tests.
- Keep the dual-core implementation surface as small as possible. Preserve architectural parity between Emacs Lisp and portable Scheme modules only for the irreducible reader, evaluator, macro, primitive, and host-effect adapter slices that cannot yet be single-sourced. If a slice lands on only one side, record the remaining parity work and why it is not yet portable Scheme before calling the issue complete.
- Keep Emacs-specific behavior behind host adapter modules.
- Avoid project history or personal machine details in public docs, tests, and examples.
- Follow Scheme Style Guidelines for portable Scheme definition shape, docstrings, and rich metadata layout.
- Follow the commit-message rules in Contributing.
The portable character implementation consumes the pinned Unicode 17.0.0 UCD
inputs under vendor/unicode/17.0.0/. Regenerate the checked-in Scheme table
library after changing those inputs, their hashes, or the generator:
make update-unicode-data
make check-unicode-dataThe update target uses the required Emacs runtime, verifies all input SHA-256
hashes, and writes scheme/consent/unicode-data.sld. The check target performs
the same transformation in memory and fails if the checked-in output differs.
Build and test targets never download Unicode data; updating the pinned release
is an explicit reviewed repository change. Follow
Portable Character Model and Unicode Profile for the UCD
source files, fallback policy, default non-Turkic casing choice, license, and
upgrade procedure.
Run the representation-independent Unicode oracle after changing generated
tables, their lookup representation, or (scheme char) behavior:
make check-unicode-semantics
CONSENT_UNICODE_SEMANTIC_HOST=gambit make check-unicode-semanticsThe portable program observes the exported (consent unicode-data) query API
consumed by (scheme char) for all 1,112,064 Unicode scalar values in
ascending order. It covers the four owned classification properties, decimal
value, simple upper/lower/fold mappings, and full string upper/lower/fold
mappings. Surrogate code points are excluded because they are not Unicode
scalar values. Querying the owned data API directly prevents a test host's own
(scheme char) implementation and Unicode release from entering the oracle.
The ordinary character and conformance suites separately cover the public
wrapper's ASCII fast paths, character/string conversions, and full-map glue.
The data queries include the corresponding ASCII results, so the digest also
fixes the semantic values those fast paths must preserve.
The program writes a canonical binary stream, and the wrapper compares its
byte count and SHA-256 digest with the checked Unicode 17.0.0 expectation. The
stream contains no generated-table representation details, so representation
changes pass when exported semantics remain identical. The target has no
wall-clock assertion. It is deliberately outside the default make test loop
and joins the opt-in make test-full and exhaustive CI lanes instead.
Schema 1 begins with the ASCII bytes for Consent Unicode semantics, followed
by NUL, byte 1, and NUL. Each ascending scalar record contains:
- the scalar as three big-endian bytes;
- one flag byte whose low four bits are Alphabetic, Uppercase, Lowercase, and White_Space in that order;
- one decimal byte, using
255when there is no decimal value; - the simple uppercase, lowercase, and foldcase scalars as three big-endian bytes each; and
- each full mapping as a one-byte scalar count followed by three big-endian bytes per mapped scalar.
The scalar itself is encoded in every record, so omission, duplication, or ordering drift changes the digest as well as a property or mapping change. For the same Unicode release, treat a digest change as a semantic change: validate it against an independent representation or UCD-derived oracle before updating the checked expectation. A Unicode upgrade updates the pinned inputs, version metadata, semantic expectation, and review evidence together.
Use the opt-in same-process benchmark to compare Unicode import and lookup costs across revisions on the same machine under equivalent checkout conditions:
make benchmark-unicode
CONSENT_UNICODE_BENCHMARK_ITERATIONS=500 \
CONSENT_UNICODE_BENCHMARK_IMPORT_ITERATIONS=5 make benchmark-unicodeThe command emits one Scheme-readable consent-benchmark record per metric. It
measures the first (scheme char) import, warm imports into fresh contexts, and
persistent ASCII and BMP classification. Simple case-mapping measurements cover
a BMP hit, an occupied-region identity miss, an empty BMP-region identity miss,
and a supplementary-plane hit; the final metric exercises full string
upcasing. Each record includes stable metric and schema names, elapsed and
per-iteration seconds, and garbage-collection counts. The benchmark has no
pass/fail wall-time threshold and is not part of make test or CI; compare
equivalent runs instead of treating timings from unlike machines as
regressions.
Checked-in Emacs Lisp implementation docstrings under lisp/ must fit within
the byte-compiler docstring width limit. make lint-elisp-docstrings enforces
this source-level rule directly, and make lint-elisp runs it before the
warnings-as-errors byte-compile gate. The source-level check deliberately avoids
generated byte-compiler docstrings, such as cl-defstruct constructor
signatures whose width can vary across Emacs releases.
When fixing a warning, put point inside the docstring and use M-q
(fill-paragraph) in emacs-lisp-mode. Emacs fills docstrings with
emacs-lisp-docstring-fill-column; setting it to 79 keeps source docstrings
comfortably under the project limit while preserving a single string literal.
Portable Scheme comments carry the API and invariant documentation that future
contributors need while editing .sld and .scm files. Runtime-visible
documentation belongs to the body literal convention in
Docstring Metadata Convention when a binding needs
metadata that standard readers, reflection, reference tools, or compiled
runtimes can preserve. Comments remain source-only and are not visible through
ordinary R7RS reading. Broader Scheme formatting and definition-shape rules live
in Scheme Style Guidelines.
- Start each portable Scheme file with a
;;;header that names the library or source file responsibility and the host/core boundary it belongs to. - Put a leading
;;comment before top-level Schemedefine-record-type,define-syntax, and plain datadefineforms. For procedure definitions, a simple string docstring supersedes a leading summary comment. Add a separate source comment only when it describes an invariant, policy, pass boundary, or portability concern that does not belong in the runtime docstring. Section comments may supplement per-binding comments, but they do not replace needed per-binding documentation. - For record types, document ownership of the record shape, any mutable fields, and whether the record is part of the public Consent Scheme datum surface or an internal implementation record.
- For macros, document hygiene assumptions, literal identifiers, private marker syntax, and the target form or pass that receives the expansion.
- Comment primitive/kernel boundaries, policy or capability assumptions, compiler/backend assumptions, include/load paths, and other places where a small local change would affect runtime authority or portability.
- Keep comments concise for tiny R7RS helpers whose names and surrounding source fully state most of the contract. Do not add line-by-line narration for simple selectors, wrappers, or local loops.
- Keep Scheme comments public-repo safe: avoid project history, personal machine paths, secrets, transcripts, and non-project branding.
These comment rules are enforced by
consent-scheme-documentation-test-source-comments (in the test-emacs-tools
shard), which fails any top-level (define ...) in scheme/, tests/scheme/,
or fixtures/r7rs/ .scm/.sld files that lacks a leading ;; comment or a
procedure docstring -- including plain value bindings such as (define program-input-stream-pulls 0), not only procedures. Only the Emacs ERT
doc-lint enforces this; running the file directly under a host Scheme (Chibi,
Guile) passes it, so a missing comment surfaces only in the Emacs-tools shard.
Runtime-visible documentation for public procedures belongs in a simple string
docstring in the procedure body, using the convention in
Docstring Metadata Convention. Add docstrings to new
exported public procedures in checked-in Scheme libraries when the procedure
body form supports them. Do not keep a leading ;; comment that only restates
the docstring. Simple string docstrings do not document macros, record fields,
library forms, renamed exports, or plain data bindings; those surfaces need
future metadata records rather than a placeholder procedure docstring.
Primitive bindings do not have reader-visible bodies, so public primitive
documentation belongs in their manifest metadata. New public primitive
manifest entries should include concise user-facing documentation and rely on
implementation procedure docstrings only as fallback for internal or generated
hooks; fallback reflection marks the origin as (implementation-procedure string).
Every exported procedure in the runtime scheme/ tree must carry the rich
property record described in
Docstring Metadata Convention: a
#((parameters ...) (returns ...) (effects ...)) vector following the simple
string docstring. Public parameter and return descriptors must either use the
dotted string/string-list shorthand for intentional any or include both
explicit (type ...) metadata and a non-empty (description ...) in expanded
descriptors. Missing type metadata still normalizes to any at read time, but
the project lint treats expanded public API descriptors without a type or
description as incomplete documentation. The lint also rejects shorthand or
expanded (type any) when descriptor prose names an obvious primitive type;
use explicit types in those cases. Custom type names should normally follow
local predicates by dropping the trailing ?, while tagged datums without
predicates should use structural types such as list.
The effects field is also required and must contain at least one symbol. Use
pure when the procedure has no observable effect; otherwise name allocation,
state access, callback invocation, errors, or host effects explicitly.
Expanded descriptors should keep (type ...) on the same line as the
parameter or returns head when the line fits within the soft line limit, with
the description on the following line. Keep longer type forms on their own line.
The consent-scheme-documentation-test-public-rich-docstrings gate enforces
this fail-closed -- it runs over every runtime scheme/ source file by default
rather than an opt-in allowlist, so a newly added file is covered automatically
and any exported procedure missing the record fails the test. There is no
rich-docstring exemption path; a file with no exported procedures already
passes, and exported runtime procedures are part of the introspectable public
surface.
Project tests live under tests/ and run through the repository Makefile.
Emacs Lisp bootstrap tests use ERT and follow these conventions:
- test files are named
tests/consent-*-test.el - module tests mirror implementation modules, such as
tests/consent-reader-test.elforlisp/consent-reader.el - shared ERT helpers should live in
tests/consent-test-helper.eland provideconsent-test-helper - the batch runner is
tests/consent-test-runner.el
The runner starts Emacs with -Q --batch, adds project-local lisp/ and
tests/ directories to load-path, loads test files in deterministic order,
and does not load user Emacs configuration.
Future R7RS conformance fixtures should plug into make test through the same
test command instead of adding a second top-level verification path.
Portable R7RS tests live under tests/scheme/ and are launched directly by
tools/run-portable-tests.sh; ERT is not part of their execution path. The
aggregate local targets run the full suite under Gambit, Racket with its r7rs
package, Guile, Gauche, and Chibi. CI schedules Chibi as a required aggregate
host and the longest direct and compiled hosts as first-class semantic plan
shards (runtime, evaluator, integration, agent, library, random,
and property, or their compiled counterparts), so failures and timings
compare the same behavior surfaces instead of one host-sized total. The local
make test-portable-chibi target uses chibi-scheme on PATH, or the command
named by CONSENT_CHIBI, and skips when Chibi is unavailable.
Portable test bodies use SRFI 64 through (stdlib testing) as their result
engine, with SRFI 252 property tests and SRFI 78/SRFI 42 table checks where
they improve coverage. (testing harness) supplies only the
suite lifecycle, batch failure, Scheme-readable summary, and SRFI 78 adapter.
The scheme/testing/ namespace contains reusable portable testing libraries
and is part of the runtime manifest index, so downstream users can
import the harness. Executable Consent test suites and cases remain ordinary
programs under tests/scheme/, outside the manifest. (stdlib ...) remains
reserved for standards-derived libraries. Additional test libraries should be
named for the missing facility they provide under (testing ...).
(testing plan) owns validated, Scheme-readable multi-program test plans.
The project plan in tests/scheme/test-plan.scm records program paths, semantic
and scheduling tags, and named shards as composable selectors. The plan remains
project test data outside the runtime manifest; only the reusable plan facility
is manifested. tools/run-portable-tests.sh asks (testing runner) to resolve
that plan and then supplies the irreducible host-specific process invocation.
R7RS provides (scheme load), but separate processes remain a deliberate test
isolation policy rather than a language limitation.
tools/run-portable-test-set.sh is the local aggregate adapter: it launches the
same named plan shards concurrently and retains one log per semantic group.
CI invokes make test-portable-shard once per group for the historically long
Guile, Gauche, Gambit-compiled, and Racket-compiled lanes. Compiled build jobs
publish a freshly linked bin/consent artifact first; the downstream group
matrix downloads that exact product instead of recompiling it in every test
job. Scheduled noncanonical source-metadata/docstring combinations retain the
full cross through aggregate shard-set jobs.
Every ordinary full program is also classified as either compiled or
self-host-gap, never both. The compiled selector is evidence-based: a program
joins it only after passing under both the Gambit-compiled and Racket-compiled
self-host runners. A gap is not an accepted alternate tier; it is a living
runtime-conformance defect that must name an implementation issue and leave the
plan when that issue ships. testing-plan-test.scm enforces the exhaustive
partition and guards the compiled program count against silent shrinkage.
(testing registry) supplies ERT-style named case registration,
tags, composable selectors, failed-case reruns, explicit source locations,
per-case timing through an injectable clock, and Scheme-readable inspection
records. Its diagnostic hook lets an Emacs, CLI, or other host attach native
backtraces without putting non-R7RS stack APIs in portable test code.
(testing runner) is the developer-facing batch layer. Registered suite
programs call testing-runner-main with (command-line) and consequently
support --list, --select SELECTOR, --verbose, --report FILE, and
--rerun-failed FILE. Selectors are Scheme data: (all), (name NAME),
(tag TAG), and recursively composed (and ...), (or ...), and (not ...)
forms. A normal run prints a concise summary, preserves assertion-level SRFI 64
result properties in its report, records per-case timing and diagnostics, and
exits zero for success, one for test failure, or two for runner/configuration
failure. For example:
guile --r7rs -L scheme tests/scheme/consent-context-test.scm \
--select '(tag property)' --report context-test-report.scm
guile --r7rs -L scheme tests/scheme/consent-context-test.scm \
--rerun-failed context-test-report.scmHosts that do not expose trailing program arguments through R7RS
command-line can provide the same string list as a Scheme datum in
TESTING_RUNNER_ARGUMENTS; this is the portable host-adapter fallback.
testing-runner-plan-main is the corresponding multi-program entry point used
by the thin launcher; it reads a (testing plan) datum and emits the selected
program paths for one named shard. Host-specific execution semantics remain
explicit in plan data: the live model checks use live-direct for R7RS hosts
that enter the Consent evaluator and live-compiled for self-hosted binaries
that already execute inside a Consent interaction context.
Host-neutral semantics must have canonical portable Scheme tests unless a
documented host boundary makes that impossible. Core runtime, reader,
evaluator, macro, library, and standard-library changes should therefore add
or update tests/scheme/ coverage first. The Scheme runner and thin host
launcher own those files; ERT continues to own Emacs adapter behavior such as
buffers, windows, commands, and prompts. Every ERT file must have an ownership
entry in tests/scheme/ert-portable-parity-map.scm. Mixed source-backed ERT
cases must name a portable case/check marker or a concrete Emacs-only boundary.
consent-test-case-parity-audit-test.el rejects unmapped files, incomplete
mixed-case partitions, stale portable markers, and portable programs absent
from the Scheme-native test plan.
The current placement audit and justified ERT-only categories are recorded in
docs/portable-test-audit.md.
The multi-host bootstrap strategy in
docs/multi-host-bootstrap.md defines what belongs
in portable Scheme modules versus host adapter modules. New host-neutral runtime
or library behavior should gain portable fixtures where practical before a host
adapter exposes it through editor, process, filesystem, model, or persistence
capabilities.
The local R7RS-small report reference lives in
docs/r7rs-small-report.md. The active R7RS-small
conformance matrix lives in docs/r7rs-conformance.md.
The canonical shared fixture corpus lives in
fixtures/r7rs/conformance-cases.scm as an consent-fixture-suite.
Fixture records carry id, kind, phase, category, section, status,
oracle, options, source, expect, and description fields so the Emacs
Lisp harness, portable Scheme harness, and conformance runner select from the
same indexed cases. Fixtures marked pending, policy-gated, or unavailable
are loaded and validated by ERT without being executed. Fixtures marked
implemented must run through make test.
Fixtures may also carry optional oracle-eligibility and oracle-reason
fields when a reference implementation should not run the case. The current
eligibility values are policy-gated and not-oracle-eligible. Reasons include
host-policy, agent-specific, resource-limit, agent-result-record,
implementation-dependent, and unspecified.
A second consent-fixture-suite corpus, fixtures/repl/parity-cases.scm (kind
repl-parity), is the host-neutral conformance corpus for the cross-host REPL
interaction contract (docs/repl-interaction-contract.md).
Its cases carry session, options, input, and an expect record sequence
rather than the reader/evaluator fields above. Two parallel runners drive the
same cases against both REPL hosts: tests/scheme/consent-repl-parity-test.scm
(portable terminal REPL, in the shared host-suite file list, so it runs on
every host shard including Chibi) and
tests/consent-repl-parity-test.el (Emacs incremental REPL). Because both read
one corpus, a host that drifts from the contract fails its runner.
Pure shared R7RS conformance fixtures can also be compared with external Scheme implementations through the oracle runner:
make conformance-oracleThe default reference adapters are Chibi Scheme and Sagittarius. Gauche, Guile,
Racket, CHICKEN, and Gambit remain opt-in comparison adapters so contributors
can inspect a wider implementation matrix before changing defaults. The runner
uses CONSENT_CHIBI, CONSENT_GAUCHE, CONSENT_GUILE,
CONSENT_SAGITTARIUS, CONSENT_RACKET, CONSENT_CHICKEN,
and CONSENT_GAMBIT when set, otherwise it searches for chibi-scheme,
gosh, guile, sagittarius, racket, csi, and gsi on PATH. The
Racket adapter requires Racket's separate r7rs package and wraps generated
fixture programs with #lang r7rs. The CHICKEN adapter requires the r7rs
egg and invokes csi with -q -R r7rs -s. The Gambit adapter invokes gsi
with -:r7rs,search=$REPO/scheme, where $REPO/scheme is the repository's
portable R7RS library directory. Each adapter writes eligible fixtures to a
temporary R7RS program and invokes the reference implementation with that file
as the command-line program argument. Missing reference implementations are
reported as unsupported-reference in Scheme-readable oracle reports and do
not affect the default make test command.
| Adapter | Role | Environment override | Discovered command | Notes |
|---|---|---|---|---|
| Chibi Scheme | default | CONSENT_CHIBI |
chibi-scheme |
Required CI host. |
| Sagittarius | default | CONSENT_SAGITTARIUS |
sagittarius |
Runs with -r 7 for R7RS mode. |
| Gauche | opt-in comparison | CONSENT_GAUCHE |
gosh |
Useful for library and writer behavior comparisons. |
| Guile | opt-in comparison | CONSENT_GUILE |
guile |
Runs with --no-auto-compile --r7rs. |
| Racket | developer-only comparison | CONSENT_RACKET |
racket |
Requires the Racket r7rs package; generated programs are wrapped with #lang r7rs. |
| CHICKEN Scheme | developer-only comparison | CONSENT_CHICKEN |
csi |
Requires the r7rs egg; runs with -q -R r7rs -s. |
| Gambit Scheme | opt-in comparison and compile host | CONSENT_GAMBIT |
gsi |
Homebrew formula gambit-scheme; gsc builds the standalone binary. |
The default host-compiled portable runtime uses the same R7RS mode and library
search stance as the Gambit interpreter shard. Set
CONSENT_GAMBIT_COMPILER to choose a specific gsc executable; otherwise
compile checks discover gsc on PATH. The oracle runner does not invoke
gsc, but documenting both tools keeps interpreter and compiler setup aligned.
Oracle reports identify each fixture by case id and classify the comparison as
portable-agree, implementation-variant, agent-mismatch,
unsupported-reference, policy-gated, or not-oracle-eligible. The runner
intentionally skips Consent Scheme-specific result fixtures, resource-limit
fixtures, and host-effecting R7RS libraries such as (scheme file),
(scheme load), (scheme process-context), (scheme repl), and
(scheme time). It also skips fixtures whose result depends on whether a
reference command reads a file as a strict R7RS program or as REPL input from a
file, since R7RS permits the latter mode to accept import declarations outside
the program prefix. The target is report-oriented; inspect agent-mismatch
reports as conformance investigation signals.
The oracle normalizes narrow reference writer spelling variation when the same R7RS value is otherwise clear, such as Chibi's doubled plus sign in complex NaN outputs. It does not collapse semantic distinctions such as exact versus inexact numbers.
implementation-variant reports are intentionally visible. Treat them as
portability notes rather than failures when Consent Scheme agrees with at least
one supported reference and the remaining references differ among themselves.
Current expected sources include exact versus inexact numeric results, special
NaN and infinity spellings, optional reader support for datum labels in program
source, bytevector port optional-argument behavior, reference-specific library
loading behavior, and case-folding quirks in developer-only references. Add
output normalization only for narrow writer aliases that preserve the same R7RS
datum. Add oracle-eligibility metadata only when the reference command cannot
exercise the same language mode as the fixture, not merely because
implementations disagree.
To focus the report stream, pass a comma-separated status filter:
CONSENT_ORACLE_STATUSES='agent-mismatch,implementation-variant' make conformance-oracleTo compare a chosen reference implementation set, pass a comma-separated reference filter:
CONSENT_ORACLE_REFERENCES='chibi,gauche,guile,sagittarius,racket,chicken,gambit' make conformance-oracleTo print a compact status count before the report stream:
CONSENT_ORACLE_SUMMARY=1 make conformance-oraclemake compile builds executable artifacts from the portable R7RS runtime by
using external Scheme host compiler toolchains. This path packages the current
portable implementation through mature host compilers; it is not the future
Consent Scheme LLIR/native compiler backend tracked by #115 through #121.
The default compile host is Gambit, whose gsc -exe -nopreload produces a
standalone native executable with no runtime dependency — the artifact suitable
for make install:
make compileSelect a host explicitly with CONSENT_COMPILE_HOST:
CONSENT_COMPILE_HOST=gambit make compile
CONSENT_COMPILE_HOST=racket make compileThe Racket binary (raco exe --cs) is relocatable as a file but loads boot files
from the installed Racket, so it runs only where Racket is present (see the
Installing section's caveat).
The Racket path requires both racket and raco; override discovery with:
CONSENT_RACKET=/path/to/racket CONSENT_RACO=/path/to/raco make compileThe Gambit path requires both gsi and gsc; override discovery with:
CONSENT_GAMBIT=gsi CONSENT_GAMBIT_COMPILER=gsc CONSENT_COMPILE_HOST=gambit make compileGenerated outputs stay under build/compile/<host>/ by default:
bin/consent: the host-compiled executable artifactsrc/: generated host wrapper sources and, for Gambit, the mirrored portable.sldsources plus generated C files used for linkingcollections/: generated host dependency wrappers when a host needs them, currently the Racket pathmanifest.scm: Scheme-readable artifact manifestlogs/: compiler, compile-timing, and smoke-test logs
CI caches only the non-runnable intermediate subdirectories that speed up a
rebuild (src/, incremental/, and Racket collections/ as applicable). It
does not cache bin/consent: a dedicated build job relinks and smoke-tests the
product binary from the current checkout, then publishes that run-scoped binary
as an artifact consumed by the compiled Scheme-native shard matrix.
Use CONSENT_COMPILE_BUILD_DIR to place those generated files elsewhere:
CONSENT_COMPILE_BUILD_DIR=/tmp/consent-compile make compileThe build runs version and scalar-evaluation smoke commands against the executable before reporting success:
build/compile/<host>/bin/consent --version
build/compile/<host>/bin/consent --eval '(+ 1 2)'The build then writes a temporary pure script containing a function definition
and assertion and runs it with --script. Separate temporary probes verify
that --eval, --script, and bare-path execution deny an ungranted file write.
The smoke also runs a script by bare path (consent FILE, equivalent to
consent --script FILE) and as an executable /bin/sh polyglot, exercising
the shebang-handling boundary end to end. See
executable-scripts.md for how to write and run an
executable Consent Scheme script.
Remove generated compile artifacts with:
make clean-compileArchitecturally, this pipeline is a compiler front-end with a borrowed code-generation backend, and the embedded source store is a capability-addressable virtual filesystem underlay; see host-compiled-staging.md.
make install puts the host-compiled binary on PATH. It installs the binary
built by make compile for the current CONSENT_COMPILE_HOST; it does not
build it for you, because install is commonly run under sudo and a build
step would leave root-owned artifacts in the source tree. The supported flow is:
make compile
sudo make installinstall follows the GNU installation variables:
sudo make install PREFIX=/usr/local # default; binary -> /usr/local/bin/consent
make install DESTDIR=/tmp/stage PREFIX=/usr/local # stage under a packaging root
make install bindir=$HOME/.local/bin # user-local, no sudoDESTDIR is prepended to every install path (the packaging/staging convention);
PREFIX (default /usr/local), bindir (default $(PREFIX)/bin), mandir
(default $(PREFIX)/share/man), and datadir (default $(PREFIX)/share) select
the final locations. After staging the binary, install runs the staged
consent --version and fails unless it matches version.sld, catching a
non-executable staging path or a Racket binary installed where Racket is absent.
install also lays the runtime-provided Consent Scheme library tree (the base
prelude, syntax prelude, and source-libraries) under the versioned
$(datadir)/consent/$(version). The binary carries an embedded copy of these
as a zero-dependency floor, so it runs standalone even when relocated; the
installed tree lets you inspect or override them. At startup the binary resolves
runtime libraries in order: CONSENT_LIBRARY_PATH (colon-separated, an explicit
override), then the install datadir baked in at compile time, then the source
tree (in a checkout), then the embedded floor. So an installed or overridden tree
wins over the embedded copy; a bare copied binary still works via the floor.
(Keep PREFIX consistent between make compile and make install so the baked
datadir matches the install location, or point CONSENT_LIBRARY_PATH at the
installed tree.)
consent --script FILE (and a #!/usr/bin/env consent shebang) runs the file
through the Consent interpreter under the non-interactive fail-closed posture —
not the host Scheme — at parity with the Emacs consent-script-run-file; see
executable-scripts.md.
If no binary has been built, install exits with guidance instead of silently
doing nothing:
make clean-compile && make install # exits 2: "run `make compile` first"The host is chosen with CONSENT_COMPILE_HOST, which defaults to Gambit. The
Gambit binary (gsc -exe -nopreload) is a standalone native executable:
relocatable, with no runtime dependency, so the default install flow produces an
artifact that works on a machine without a Scheme toolchain. The build uses
Gambit's built-in eq?-hash behind Consent's identity adapter, while table
storage and policy remain portable Scheme. The hot graph registry therefore
has no external module dependency. Its startup smoke hides the build host's
Gambit module tree, so any accidentally unlinked dependency fails before the
standalone artifact is published:
make compile # CONSENT_COMPILE_HOST defaults to gambit
sudo make installThe Racket binary (raco exe --cs) is relocatable as a file but still loads
boot files from the installed Racket: it runs only while Racket remains
installed on the same machine. (Bundling a full Racket runtime tree with
raco distribute is out of scope.) The post-install --version check is what
turns a Racket-without-Racket install into a hard failure rather than a runtime
surprise.
A consent(1) man page is installed to $(mandir)/man1/consent.1 only when one
exists at docs/consent.1 (CONSENT_MANPAGE). That page is generated by the
documentation pipeline (#458); until it lands, install prints a notice and
skips the man page. Override the source path with CONSENT_MANPAGE=....
Remove an installed binary (and man page) with the matching variables:
sudo make uninstall # removes /usr/local/bin/consent (+ man page)
make uninstall DESTDIR=/tmp/stage PREFIX=/usr/localuninstall removes exactly the paths install writes — the binary, the man page,
and the versioned $(datadir)/consent/$(version) library tree — and is
idempotent; per GNU convention it does not remove shared directories such as
bindir.
make dist packages the compiled binary into a versioned tarball under
$(CONSENT_DIST_DIR) (default build/compile/dist/), named from version.sld
and the compile host (for example consent-<version>-gambit.tar.gz). The
tarball contains the binary, its manifest.scm, README.md, LICENSE, the
runtime library tree under share/consent/<version>/, and the man page when
present:
CONSENT_COMPILE_HOST=gambit make compile
CONSENT_COMPILE_HOST=gambit make dist
ls build/compile/dist/Released binaries are published by the tag-triggered
release workflow: pushing a v* tag builds
the standalone Gambit binary, runs the smoke and native test gates, verifies a
staged install, and uploads the make dist tarball as a GitHub Release asset.
Only the standalone Gambit binary is published, for the Racket-needs-Racket
reason above.
The default local verification command is:
make testmake test runs a trimmed default shard set for a fast local loop: one
representative portable host (test-portable-racket) plus its reflection
contract and catalog stress companions (test-portable-racket-reflect and
test-portable-racket-reflect-stress,
CONSENT_DEFAULT_PORTABLE_TEST_SHARD_TARGETS) and the Emacs-hosted shard set in
CONSENT_EMACS_TEST_SHARD_TARGETS. The portable reader/writer/docstring
machinery that the source-metadata and docstring-retention modes exercise is
host-independent, so one portable host is enough for the default loop.
The Emacs-hosted surface is split across multiple shards (#556) so the
parallelizer can overlap them on hosts with more cores than there were once
shards: test-emacs-tools keeps the tools and docs cluster (CI, compile,
diagnostics, doc-pass tests, ...), while test-emacs-integration carries the
heavier integration surface (REPL, VCS, native-CLI daemon) that used to dominate
test-emacs-tools's wall time. Reflection runs through its own contract shard
(test-emacs-reflect) plus catalog, documentation/apropos, binding crosswalk,
and dynamic manifest stress shards so manifest-backed discovery does not
serialize the REPL/integration lane. The aggregate test-emacs-reflect-stress
target remains available for ad hoc local runs. The four full host-compile +
install/dist tests in consent-compile-portable-test.el are stranded into the
opt-in test-emacs-native-build shard, which make test skips and
make test-full runs. The native build path is already exercised separately by
test-portable-gambit-native and test-portable-compiled; the
test-emacs-native-build shard additionally covers the install/dist packaging
surface against a single shared host build per host (built once per Emacs
process, reused across the runner-smoke and install/dist tests).
CONSENT_EMACS_TEST_SHARD_TARGETS and the Emacs-hosted CI matrix list the
longest expected wall-time shards first. That ordering is a scheduling hint so
constrained runner pools start tail rows early; it does not change the behavior
surface each shard covers.
The former library/conformance aggregate is likewise only a compatibility target for ad hoc local runs. Default and CI execution split its exact 212-test union into fixture/conformance, library runtime, standard-library behavior, random/property libraries, and standard-library manifest/vendor shards. These names intentionally align with the portable plan's behavior surfaces wherever the two implementations exercise comparable semantics.
The official stdlib reference corpus has canonical portable coverage in
tests/scheme/stdlib-json-reference-test.scm, including the FoundationDB JSON
sample, invalid inputs and their explicit exclusions, implementation-defined
fixture classifications, JSON Lines, and JSON Text Sequences. The complete
corpus runs through the Gambit- and Racket-compiled self-hosted lanes as the
gold-standard proof that the shipped evaluator can load and exercise its own
JSON library. SRFI 180 reference semantics no longer run through ERT; the old
eight-minute Emacs stress shard and its smaller companion shard were removed.
Portable host failures are reported by the named
test-portable-* shard and test file; Emacs adapter/bootstrap failures remain
reported by the named test-emacs-* shard.
Portable reflection tests follow the same behavior-surface split. Quick
manifest-input and helper contracts run in dedicated
test-portable-*-reflect shards, while full catalog traversal and dynamic
manifest add/remove stress coverage run in dedicated
test-portable-*-reflect-stress shards. That keeps Guile and Gauche
catalog-rebuild costs visible without folding them into the ordinary host timing
row.
make test defaults to -j16 so up to 16 shard processes can run in parallel
on a many-core host. Override with CONSENT_TEST_JOBS=N make test on narrower
hardware.
The default set also runs test-parity, the cross-implementation parity gate
(#374). It runs the shared fixture corpus through both in-repo cores — the
Emacs-hosted implementation and the portable R7RS implementation — and fails on
any result divergence, turning the docs/architecture.md "First-Class Portable
Scheme" parity rule into an executable gate. Scope is the irreducible dual core
(reader, evaluator, macro, runtime); cases whose source imports a library that
is single-sourced from a portable .sld and loaded by both bootstraps fall out
of scope automatically, because such a library cannot diverge from itself. The
Emacs bridge spawns the portable emitter
(tests/scheme/consent-parity-emit.scm) under the host named by
CONSENT_PARITY_HOST (auto-discovered from chibi-scheme, gosh, or guile
when unset) and skips when no portable host is available, so the gate is a
no-op rather than a failure on a host-free machine.
The default set also runs lint-elisp, the Emacs byte-compile lint gate (#415).
It byte-compiles every checked-in lisp/*.el source with
byte-compile-error-on-warn enabled, so any byte-compiler warning — unbound
variables, arity mismatches, unused lexicals, obsolete calls — fails the build.
This is the cheapest static analysis already available for the Emacs Lisp twin
and is the Emacs-side counterpart to the portable doc-lint coverage the suite
carries (#407/#412). The gate redirects its bytecode into a throwaway
CONSENT_LINT_BUILD_DIR (default build/lint) so it leaves no stale .elc
beside the sources and never races the parallel test shards that load the .el
files directly. Run it on its own with:
make lint-elispmake compile-elisp remains available as the non-gating target that produces
in-place bytecode without warnings-as-errors.
The gate disables the byte-compiler's docstring-width sub-check
(byte-compile-docstring-max-column is left unbounded) so it is deterministic
across Emacs versions. cl-defstruct auto-generates a constructor docstring
whose (fn &key SLOT...) calling-convention line scales with the slot count;
Emacs 30 excludes that machine-generated line from the width check while Emacs
29 does not, so a wide struct — notably the consent--eval-context god-object
tracked by #371 — would otherwise fail the gate on the Emacs the CI runners
install (29.x) but not on a newer local Emacs. Docstring style and width are
owned by the separate checkdoc slice, not this gate. That batch checkdoc
pass is intentionally not part of the gate yet; it surfaces a large backlog of
docstring-convention findings and is tracked as its own follow-up slice.
The default set also runs lint-portable, the portable twin of lint-elisp
(#421). It compiles the host-loadable portable Consent Scheme libraries under
Guile with the high-signal static warning classes — unbound variable, arity
mismatch, use-before-definition, unused lexical, and format/case-datum
mistakes — promoted to errors, so the portable peer gets the same cheap static
coverage the Emacs twin does. Run it on its own with:
make lint-portableGuile is the gate host because, among the portable hosts already wired into CI
(Gambit, Racket, Guile, Gauche), it is the only one with a usable ahead-of-time
warning facility — guild compile -W ..., the same -W baseline the linting
survey records for Chez/Guile. Gambit reports arity mismatches only at run time,
and Racket and Gauche expose no comparable unused/unbound static warning pass, so
they are intentionally out of scope for this gate (the issue bounds it to hosts
already in CI). The gate drives a generated driver that imports every
host-loadable library with a unique prefix; auto-compilation surfaces warnings
for each transitively compiled library, and a fresh compile is forced each run so
cached bytecode never masks a warning. The driver and bytecode cache live under a
throwaway CONSENT_PORTABLE_LINT_BUILD_DIR (default build/lint-portable) that
the gate removes when it finishes, so it leaves nothing beside the sources.
Two warning classes are deliberately not gated. unused-toplevel and
unused-module fire hundreds of false positives because the project registers
internal helpers and primitives through runtime dispatch tables that Guile's
per-module static analysis cannot see; they are too noisy to gate cleanly. Four
libraries are also out of the gate's reach because they are not host-loadable as
pure R7RS — (agent diagnostics), (agent diff), and (agent test) import the
runtime-virtual (agent io) module, and (consent capability) imports the
host-adapter (consent capability primitive) layer; these are exercised through
the runtime instead. The exclusion list in tools/lint-portable.sh is explicit
so that a newly added library reaching such a module fails the gate loudly rather
than being skipped silently. Like the portable host shards, the gate skips
(rather than fails) when Guile is unavailable, so it is a no-op on a Guile-free
machine; CI installs Guile so it always runs there.
The default set also runs lint-readability, the repository-wide narrow-width
gate. It applies an 80-column soft limit and a 100-column hard limit to
first-party Scheme, Emacs Lisp, tests, fixtures, shell tooling, Make recipes,
and workflows. Soft-limit exceptions require a local category and rationale;
hard-limit exceptions are not supported. Run it and its format-class self-test
with:
make lint-readability
tools/lint-readability.sh --self-testThe compatibility target lint-line-length delegates to the same gate. See
docs/readability.md for wrapping examples, exception
categories, generated-source policy, provenance exclusions, and migration
metrics.
The default set also runs lint-branding, the assistant/tool/vendor branding
gate. It enforces the AGENTS.md rule that no assistant, tool, vendor, or
workflow branding appears in branch names, pull request titles or bodies, commit
messages, generated artifacts, or ordinary documentation. The dedicated
project credits page is the exception for explicit tool-assisted development
credit. The gate machine-checks that boundary rather than leaving it to
contributor diligence, the same stance the two compiler lint gates take. Run it
on its own with:
make lint-brandingThe gate (tools/lint-branding.sh) uses two pattern tiers to keep false
positives near zero on this project's own domain language. Self-attribution
markers — a co-author trailer naming a model or tool, a "generated with/by
" line, a session link, the robot emoji — are scanned everywhere: tracked
file contents, commit messages, the PR title/body, and the branch name. Bare
vendor or tool slugs are scanned only in those metadata contexts (commit
messages, PR title/body, branch name), never in file contents, because the
repository legitimately writes some of those words in prose — CLAUDE.md is a
checked-in tool-config file, docs/references.md cites external papers by their
publishers, Project Credits gives explicit tool-assisted
development credit, and "cursor" is this project's own word for the shared
stdin cursor. The slug set is therefore deliberately narrow and excludes words
with a legitimate technical use here (notably "openai", as in the shipped
OpenAI-compatible transport). A third tier scans each commit's author and
committer identity (name <email>) — and only that — for the vendor-bot email
domains a real contributor never authors from. The author email is what GitHub
attributes a commit to, so a vendor "noreply" identity badges the commit as
machine-authored on the PR even when the branch name, every commit message, and
the PR body are clean. Locally the script scans the tree, the current branch,
the origin/main..HEAD range, and that range's commit identities.
CI enforces the gate two ways — a required primary and a fallback:
- Dedicated
Brandingworkflow (required, primary)..github/workflows/branding.ymlruns on pull-requestopened,synchronize,reopened, andeditedevents — theeditedtype matters because a branding trailer is usually appended to the PR title or body after creation, and without it that change would never be re-scanned. The job checks out full history (fetch-depth: 0) and injects the PR title and body from the event payload (CONSENT_PR_TITLE/CONSENT_PR_BODY) alongside the base and head SHAs and the branch (CONSENT_BRANDING_BASE/CONSENT_BRANDING_HEAD/CONSENT_BRANDING_BRANCH), so every dimension is scanned deterministically: tracked files, branch name, commit messages, commit author/committer identity, and the PR title/body. The job's check (Assistant/tool/vendor branding gate) is a required status check in the "Protect main" ruleset, so a failure blocks merge rather than only reporting red. Two caveats it does not cover: the required check assumes the PR branch carries this workflow file (a branch cut before it landed must rebase ontomainfor the check to report), and direct pushes tomainare not gated by it — though the ruleset makesmainpull-request-only, closing that path in practice. Because it edits a file under.github/workflows/, adding or changing it needs a workflow-scoped push. - Piggybacked on
lint-elisp(fallback, no workflow change).lint-brandingis also a prerequisite oflint-elisp, so the always-runlint-elispjob runs the gate even outside a pull request — for example on a push tomain. That job checks out shallow, so there it reliably scans tracked file contents (the checked-out tree) and the branch name (GITHUB_HEAD_REF); the commit-range scans (messages and identity) run only when a base ref is present, and the PR title/body are read best-effort from the public REST API unauthenticated, degrading to a loud notice rather than a failure when that read is rate-limited or blocked. This path rides on normal build code the job already executes, so it predates and backstops the dedicated workflow.
Run the exhaustive set — every portable host shard plus every Emacs shard — with the opt-in escape hatch:
make test-fullmake test-full runs CONSENT_FULL_TEST_SHARD_TARGETS. You can also override
the default set directly, for example to add one more host without running the
whole matrix:
CONSENT_TEST_SHARD_TARGETS='test-portable-guile test-emacs-core' make testRun make test-full (or the matching scheduled CI lane) before landing
axis-sensitive changes to the reader, writer, or docstring machinery, since
those are the paths the trimmed default no longer fans out across every host.
Set CONSENT_TEST_TARGET_ROOT to keep the current checkout's Makefile and
portable test launcher while pointing Scheme host commands at another checkout
or archive's scheme/ directory. This is useful for historical timing sweeps that
replay a newer harness against an older reader/evaluator implementation:
CONSENT_TEST_TARGET_ROOT=/tmp/consent-old make test-portable-chibiCI mirrors this trimmed default on the per-push lane (pull_request and push)
and keeps the exhaustive run on a separate opt-in lane. The
source_metadata × docstring_retention cross is a de-feature smoke test —
stripping syntax metadata and docstrings and confirming the runtime still passes.
Over the cross's first 29 PRs it never selected a different test list or caught a
failure the canonical combo missed, so the per-push lane carries only a fast
canary of it (#481):
- The canonical portable host (Gambit, the
test-portable-gambitjob) and the canonical Emacs shard (the core language/runtime shard, thetest-emacs-corejob) each run the canonicalon/fullcombo plus a single fully-strippedoff/nonede-feature smoke leg per push. The four intermediate combos (on/simple,on/none,off/full,off/simple) run only on the exhaustive lane. - The recompile-bound Gambit native shard runs
on/fullonly per push; its de-feature cross is exhaustive-lane only, since recompiling the native executable to re-run an identical ~2 s suite is the cross's largest per-push cost. - Every other host and Emacs shard runs only the canonical
on/fullcombo per push.
Every host and every Emacs shard is still represented at least once, so
cross-host parity coverage is preserved; only the redundant metadata/docstring
fan-out collapses. The exhaustive matrix — every host and shard across all six
combos, including the Gambit-compiled cross — runs nightly on the schedule lane
and on demand through workflow_dispatch. Trigger it before landing
axis-sensitive changes to the reader, writer, or docstring machinery: open the
Actions → Test workflow and use Run workflow (workflow_dispatch), or
wait for the nightly schedule run. The trimmed jobs
(test-portable-gambit, test-emacs-core, test-portable-extra-hosts,
test-portable-gauche-hosts, test-emacs-hosted, and the test-parity gate)
drive their source_metadata and docstring_retention matrix axes from a
github.event_name expression, so those events expand them back to the full
cross. The test-parity job (#374) runs the parity gate under Guile as a
required check on every lane. Guile, Racket, and the Racket-compiled runner use
the bare ubuntu-latest runner; Gauche is packaged only in the Ubuntu 26.04
container and runs in its own matrix job.
The lint-elisp job (#415) runs make lint-elisp on every lane as its own
lightweight required check, alongside the license-reuse REUSE/SPDX job. It
needs only Emacs, so it is a fast static gate that runs in parallel with the
test shards rather than fanning out across the matrix axes. The lint-portable
job (#421) is its portable twin: it installs Guile and runs make lint-portable
on every lane as an equally lightweight required check, so the portable libraries
are gated for compiler warnings on the same per-push cadence.
CI and the local aggregates share Scheme-defined behavior shards so timing and failures stay visible by architectural path. The aggregate targets remain the convenient local entry points:
CONSENT_GAMBIT=gsi make test-portable-gambit
CONSENT_GAMBIT=gsi make test-portable-gambit-reflect
CONSENT_GAMBIT=gsi make test-portable-gambit-reflect-stress
CONSENT_GAMBIT=gsi CONSENT_GAMBIT_COMPILER=gsc make test-portable-gambit-native
CONSENT_PORTABLE_HOST=racket make test-portable-owned-reader-no-host-identity
CONSENT_RACKET=racket make test-portable-racket
CONSENT_RACKET=racket make test-portable-racket-reflect
CONSENT_RACKET=racket make test-portable-racket-reflect-stress
make test-portable-compiled
CONSENT_GUILE=guile make test-portable-guile
CONSENT_GUILE=guile make test-portable-guile-reflect
CONSENT_GUILE=guile make test-portable-guile-reflect-stress
CONSENT_GAUCHE=gosh make test-portable-gauche
CONSENT_GAUCHE=gosh make test-portable-gauche-reflect
CONSENT_GAUCHE=gosh make test-portable-gauche-reflect-stress
make test-emacs-core
make test-emacs-conformance
make test-emacs-library-runtime
make test-emacs-library-stdlib-core
make test-emacs-library-stdlib-property
make test-emacs-library-stdlib-manifest
make test-emacs-agent-control
make test-emacs-agent-reliability
make test-emacs-capability-boundary
make test-emacs-agent-state
make test-emacs-tools
make test-emacs-reflect
make test-emacs-reflect-catalog-stress
make test-emacs-reflect-documentation-stress
make test-emacs-reflect-binding-crosswalk-stress
make test-emacs-reflect-dynamic-manifest-stress
make test-emacs-integration
CONSENT_PARITY_HOST=guile make test-parityThe owned-reader identity-map target runs the focused poisoned-backend
regression. Set CONSENT_PORTABLE_HOST to racket, gambit, or guile; the
default is Racket. The normal local aggregates include it once, and CI runs it
once per available host on the canonical metadata/docstring combination.
The opt-in test-emacs-native-build shard runs the four full host-compile +
install/dist tests when invoked directly (make test-emacs-native-build) or
through make test-full; the trimmed make test skips it.
make test runs those shard targets in parallel by default. make test-portable remains available as the local aggregate for the default
portable R7RS hosts. CI records one log and check per semantic group for the
historically longest Guile, Gauche, Gambit-compiled, and Racket-compiled suites;
the compiled group jobs consume product binaries built earlier in the workflow.
Gambit and Racket direct-host aggregates emit the same per-group logs, allowing
the timing summary to compare equivalent plan selectors even where the CI job
remains host-aggregated. The required Chibi CI host uses the same aggregate
target that remains available for local timing and compatibility checks:
CONSENT_CHIBI=chibi-scheme make test-portable-chibiThe full-suite host shards run the same portable Scheme test files so their
timing rows compare host behavior rather than different test scopes. Each host
aggregate resolves full-evaluator and full-support from the Scheme plan and
runs those measured subsets concurrently; the evaluator program was the common
critical path, while the aggregate log still reports one complete host suite.
The Racket bridge generates
temporary #lang r7rs collection wrappers for checked-in .sld libraries
because Racket's R7RS package resolves imports as Racket collection modules.
The compiled self-host plan is the gold-standard product corpus. Its selector
contains 45 programs: 44 of the 65 ordinary full programs plus the
compiled-only runtime manifest smoke program. Those 44 ordinary programs cover
Scheme-visible reader and numeric behavior, registered agent semantics, testing
infrastructure, models, data structures, random/property facilities,
generators, and the complete SRFI 180 reference corpus. In particular, the
compiled agent-reliability and native CLI daemon adapter programs read real
structured fixture files through (scheme read); focused Racket-compiled runs
exercise 20 and 238 assertions, respectively. The compiled-only manifest smoke
also reads a labelled cycle through (scheme read), checks its self-identity,
mutates it, and verifies write-shared output. Six direct private owner/adapter
suites remain outside this selector: consent-reader-test.scm,
consent-numeric-test.scm, consent-numeric-generated-test.scm,
consent-fixture-test.scm, consent-symbol-test.scm, and
consent-datum-test.scm. Their borrowed-host ABI belongs to #120. Compiled
random/property suites retain public numeric coverage without crossing that
private dispatcher boundary. The remaining 21 ordinary programs are
explicitly tagged self-host-gap and assigned to implementation issues #120,
#346 (symbol identity and macro-introduced identifiers), or #432
(compiled-rooted nested evaluation).
The target is to drive that set to zero; it is not a permanent reduced suite.
Local test-portable-compiled and test-portable-gambit-native targets compile
before invoking the matching *-run consumer through consent --host-run. CI
exposes the phases separately and prioritizes each compile step before direct
host work and the compiled test consumer.
Compiled-build CI caches are intentionally intermediate-only: fallback restores
may warm generated sources, compiled objects, incremental hashes, or Racket
bytecode, but they must not restore a runnable bin/consent for the shard to
test.
CI builds and caches Gambit 4.9.7 because Ubuntu 24.04's gambc package is
4.9.3 and does not accept the -:r7rs runtime option needed for the portable
library search path. The Racket, Racket-compiled, and Guile extra-host shards
run on the bare runner. The Gauche matrix runs inside an Ubuntu 26.04 container
because Ubuntu 24.04 does not ship the Gauche package used by that shard. That
container base image is pulled from the AWS ECR Public Ubuntu mirror
(public.ecr.aws/ubuntu/ubuntu:26.04) instead of Docker Hub, and the Gauche
matrix runs one shard at a time to avoid registry pull throttling during job
provisioning. These shards contribute required host timing data. The
Emacs-hosted shards split the non-portable ERT suite into core
language/runtime, library/conformance, agent/capability, tools/docs, and
integration groups.
make test-emacs-hosted
remains available as the local aggregate for all non-portable ERT tests with
(not "consent-scheme-.*").
When CONSENT_TEST_SELECTOR is set, make test uses a single ERT runner
with that selector instead of the local shard fan-out:
CONSENT_TEST_SELECTOR='consent-smoke-test-harness-runs' make testEach shard uploads a test-log-* artifact and writes a job summary. On pull
requests, the combined timing job also updates one PR comment with a compact
shard timing table and a collapsible detail section so reviewers can see timing
at a glance from the PR conversation. Portable Scheme runners may also emit
fine-grained CONSENT_CI_CHECK_SECONDS diagnostics for slow checks; the
combined summary keeps those details below the fold and treats shard wall time
as the primary signal. The above-fold section sorts all reported shards by
wall-clock time across hosts and uses actual CI shard names so full-suite,
reflection contract, and reflection stress rows can be compared directly across
hosts. The collapsible detail section keeps the stable diagnostic shard order.
Alongside that human-facing summary, the combined timing job emits one
machine-readable, append-only record per run for longitudinal analysis. The
record is JSON Lines tagged with a schema_version, uploaded as the
ci-run-record artifact. See CI run record for the schema,
the field set, the schema-version discipline, and the durable-sink plan.
Live local model tests require an OpenAI-compatible local model endpoint. The CI smoke target exercises ordinary completion plus forced tool-calling through the Emacs host, the portable Racket host, and the Racket-compiled Consent Scheme host. The two portable programs are selected and run directly by the Scheme test plan and portable launcher; ERT only runs the separate Emacs-host checks. Run the CI smoke selector with:
make test-live-model-ciRun only the Scheme-native direct and compiled live lanes with:
make test-live-model-portableRun one quick-start profile shard after pulling that profile's models:
make test-live-model-small
make test-live-model-recommended
make test-live-model-largeRun all opt-in live local model tests, including all three quick-start profile sets, with:
make test-live-modelAll live targets set CONSENT_LIVE_MODEL_TEST=1. The profile and all-live
targets also set CONSENT_LIVE_MODEL_MATRIX=1 and pass
CONSENT_LIVE_MODEL_MATRIX_CASES as comma-separated ROLE=MODEL cases. Use
CONSENT_LIVE_MODEL_ENDPOINT and CONSENT_LIVE_MODEL_ID to override the
default local endpoint and smoke model id.
The normal test-emacs-tools shard also runs a deterministic quick-start
contract test for docs/repl-agent-quickstart.md. That test extracts and
evaluates the tutorial's known-good Scheme differentiator and checks the
documented role/provider shape through a fake model transport. It deliberately
does not download or call real models; use the opt-in local live targets above
when you need to validate a local model set against the actual endpoint.
For documentation-only changes, also run:
git diff --check
rg -n "m[y]/consent|m[y]/mcp" README.md docsThe rg command should normally return no matches. Also search for any
project-history or private-machine references relevant to the change. The
pattern uses a character class so this guide does not carry the deprecated
spelling as plain text. If a match is intentional, explain why in the pull
request.
These are silent or CI-only traps when adding tests; nothing else in the build warns about them.
-
A new Emacs test file needs a matching shard selector. The ERT runner (
tests/consent-test-runner.el) auto-discovers everytests/consent-*-test.elby glob, butmake testdoes not run the whole glob -- it runs the four Emacs shard targets, each filtered by an ERT name-selector regexp in theMakefile(CONSENT_EMACS_CORE/LIBRARY/CAPABILITY/TOOLS_TEST_SELECTOR). A new file whoseert-deftestnames match none of those selectors loads but never runs undermake test(only under the unfilteredmake test-emacs-hostedor an explicitCONSENT_TEST_SELECTOR). When adding aconsent-*-test.el, add or broaden the matching shard selector. Host-spawning Scheme bridge tests (consent-scheme-*that start an external host) belong in the portable selectors; host-independent pure-elispconsent-scheme-*tests belong in an Emacs shard (for exampleCONSENT_EMACS_TOOLS_TEST_SELECTOR). Use a prefix clause only when the whole family is host-free; otherwise anchor exact names so host-spawning siblings stay on host shards. Theselector:strings in.github/workflows/test.ymlare cosmetic -- printed into the run summary only; CI selects tests by invokingmake <shard-target>, which reads theMakefileselector, so theMakefileedit is what changes CI coverage. Update the YAML literal only to keep the summary honest. -
Host-level
#u8(...)literals break CI's Racket. A#u8(...)bytevector literal read at the host level in atests/scheme/*.scmfile -- a real datum the host Scheme reads, not text inside a"..."Consent source string -- fails on CI's Racket withread-syntax: bad syntax #uand fails thetest-portable-racketshard. The other hosts accept it, and a newer local Racket also accepts it, somake test-portable-racketcan pass locally while CI breaks; do not trust a local Racket pass for#u8portability. Build host bytevector test data with(bytevector b ...), a plain(scheme base)call with no reader syntax that every host reader accepts.#u8(...)is only safe inside a Consent source string or an expected-value string, where the Consent reader (which supports#u8) reads it rather than the host reader. -
Self-hosted shards yield Consent number records, not host integers. Under
consent --host-run(thetest-portable-compiledandtest-portable-gambit-nativeshards) the test file is evaluated by the Consent interpreter, so a literal like'((length . 4))yields an alist whose4is a Consent number record, not a host integer. A.sldprocedure that compares or combines such a value with its own host-integer counters (>=,-, arithmetic,substring) works on every directly-run host but fails only on the self-hosted shards, surfacing as a generic(status error) (message "error") (host-condition error). Normalize at the boundary --(if (consent-number? v) (consent-number-value v) v)(Emacs:consent-number-p) -- leaving host integers and#funtouched. Reproduce with a freshly built binary run directly, for examplebuild/compile/gambit/bin/consent --host-run FILE. -
Exercise cyclic reader data in direct and self-hosted lanes. The portable owned compound heap preserves datum-label identity for multi-element pair and vector cycles such as
#0=(1 2 3 . #0#)and#0=#(1 #0#). Keep these cases in the portable reader suite, and keep representative cyclic mutation and writer cases in the shared fixture suite, so both compiled hosts and the Emacs bootstrap must preserve the same cycle behavior. -
Compiled-host
standard-source-library-*-filefailures are usually a stale install. If the compiled-host shards (test-portable-gambit-native,test-portable-compiled) fail only onstandard-source-library-case-lambda-file/standard-source-library-lazy-file-- expecting a relativescheme/consent/*.sldbut getting an absolute/usr/local/share/consent/<version>/consent/*.sld-- it is almost always a stale local install, not a regression. The compiled binary is built withCONSENT_INSTALL_DATADIR=/usr/local/share/consent/<version>and searches that datadir before the cwd-relative source tree. Tell-tale signs: the failing version's directory exists under/usr/local/share/consent/butmain's does not, and the interpreted hosts (run with-L <source>) all pass. Clear it withsudo rm -rf /usr/local/share/consent/<that-version>(orsudo make uninstall) and re-run. Do not weaken the test -- CI has no install and always resolves source-relative. -
Compare cross-host record streams by serializing, not
equal?. To assert two Consent contract-record streams are equal in a test that runs on every host, compare their serialized forms (consent-datum->externalin portable,consent-result->externalin Emacs), not rawequal?. Records embed canonical-number records, and on Gauche R7RSequal?on those is identity-based, so two value-equal streams built separately are notequal?there (Chibi/Guile/Racket happen to pass). Records reloaded from a captureddatumstream via the standard reader are plain Scheme data, whichconsent-datum->externalrefuses to write -- only live or replayed Consent records can be serialized.
The implementation module map is defined in the architecture document. Early work is expected to introduce directories such as:
lisp/
scheme/
tests/
fixtures/
Do not create broad placeholder trees without an issue that needs them. Let the first implementation tickets establish only the files they actually use.
Keep generated state, downloaded model weights, caches, transcripts, private memory, and machine-specific configuration out of git. Future tickets will define the exact ignored local-state paths.
Until those paths exist, avoid committing:
- secrets or provider tokens
- absolute paths from a developer machine
- generated logs or transcripts
- downloaded third-party assets
- local Emacs state
Before opening a PR:
- Confirm the branch only contains the intended issue work.
- Confirm
scheme/consent/version.sldmatches the issue's roadmap-derived version from #53. - Run the available verification commands.
- Check public docs for stale personal-config or historical-repo references.
- Use a Conventional Commits message for each commit.
- Reference the issue in commit footers and the PR body.