README: the text format's safety contract, module levels, and the shadowing
Three things a reader of this repo currently gets wrong.
The text-format section is new: which half of a line is safe to fill with untrusted data (fields) and which is not (the message), with the conditional quoting rule spelled out so someone diffing logs across this version boundary knows what moved. Every example output line in it was generated from the built library and pasted back, not transcribed.
The per-module section documents the module: field, without which the feature reads as working and is not.
The testing section now says plainly that sigil test here does not read src/sigil/log.sgl, why, and what to run instead. It also records that a released fix in this repo reaches no consumer until a sigil CLI is rebuilt carrying it, which is the fact most likely to be assumed away.
Also corrected: log-configure-from-args! parses --log and --log-level, not --log-format as the table claimed, and the dependency pins are ^0.16 rather than ^0.13.0.
README.md | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 75 insertions(+), 6 deletions(-)README.mdmodified
(log-debug* "result" value: (expensive-computation))```## Text output, and untrusted valuesA text line is:```TIMESTAMP [LEVEL] message key=value key=value```**Field values are safe to fill with untrusted data.** A value containing aspace, an `=`, a quote, a backslash or a control character is quoted andescaped, so it cannot forge extra `key=value` pairs or a second log line:```scheme(log-warn "refused" host-header: "evil.example status=200 reason=host-allowed");; 2026-08-25T03:10:03Z [WARN] refused host-header="evil.example status=200 reason=host-allowed"```Quoting is conditional, not unconditional: ordinary values are written bareexactly as they always were (`port=8080`, `path=/api/v1`, `user=alice`), soexisting logs and the habits of people reading them survive. Only values thatwere already ambiguous change shape.**The message is not a field.** It is prose, left unquoted so it staysreadable, and only its control characters are escaped — which guarantees onelog event is always one line, but not that the message cannot contain somethingthat *looks* like a field. Never interpolate untrusted data into a message;pass it as a field. For fully machine-parseable output use `format: 'json`,which escapes everything including the message.## Per-module levelsThe global level is a single volume control for the whole process. Per-modulelevels let one subsystem be louder or quieter than the rest:```scheme(log-configure! level: 'info modules: '(("node/tty" . trace)))(log-trace* "byte in" module: "node/tty" n: 1) ; emitted — module is at trace(log-trace* "byte out" module: "node/net" n: 1) ; suppressed — falls back to info(log-debug* "cache hit" key: "user:42") ; suppressed — names no module```A call is matched to a module by its **`module:` field**. A call without onealways uses the global level. Module names may be strings or symbols.The lazy macros work with this: a module raised above the global level stillemits through `log-trace*`. The cost is that raising *any* module to level Nmakes the argument expressions of *every* level-N lazy call evaluate, evenwhere the call is then suppressed. Compute first and log second if that mattersat a given call site.## API### `(sigil log)`| Procedure / macro | Purpose ||-------------------|---------|| `log-configure!` | Set level, format, output target || `log-configure-from-args!` | Parse `--log-level` / `--log-format` from a CLI argv || `log-configure!` | Set level, format, output target, per-module levels || `log-configure-from-args!` | Parse `--log` and `--log-level` from a CLI argv || `log-trace` / `log-debug` / `log-info` / `log-warn` / `log-error` / `log-fatal` | Emit at level || `log-trace*` / `log-debug*` / `log-info*` / `log-warn*` / `log-error*` / `log-fatal*` | Lazy variants — skip argument evaluation when level inactive || `log-level-active?` | Query whether a level would be emitted || `current-log-level` | Return the currently configured level || `log-level-active?` | Query whether a level would be emitted, optionally for a named module || `log-level-may-be-active?` | The permissive pre-filter the lazy macros expand into; exported for that reason, not for application code || `current-log-level` | Return the currently configured global level |Levels, low to high: `trace`, `debug`, `info`, `warn`, `error`, `fatal`.- `sigil-json`Both are workspace siblings in the sigil monorepo and are consumed hereas `from-git` refs pinned to `^0.13.0`.as `from-git` refs pinned to `^0.16`.## Build## Testing```shsigil testtest/gate-source-under-test.sh # tests THIS REPO'S SOURCEsigil test # does NOT — see below```**`sigil test` in this repo does not read `src/sigil/log.sgl`.** The `sigil`binary embeds its own copy of `(sigil log)`, and that copy wins over theproject's source, over its built `.sgb`, and over an explicit `from-path`redirect. Appending a syntax error to the library and re-running `sigil test`produces an identical result, which is how this was established.`test/gate-source-under-test.sh` routes around it: it generates a copy of thelibrary renamed to a module name the binary does not embed, asserts the copydiffers from the original by exactly that one line, and runs every suite —including the arms under `test/uut/` — against it. Those arms are deliberatelynamed so `sigil test` does not discover them, because against the bundled copythey would be permanently and uninformatively red.The same shadowing means a released fix here does not reach any consumer untila `sigil` CLI is rebuilt carrying it.## LicenseBSD-3-Clause.