Skip to content

tools: correctness-focused Racket hygiene lints (pnet registration, fire-fn capture, memo-hash) + lint gate - #81

Draft
kumavis wants to merge 8 commits into
mainfrom
claude/racket-hygiene-tooling-rhpku6
Draft

tools: correctness-focused Racket hygiene lints (pnet registration, fire-fn capture, memo-hash) + lint gate#81
kumavis wants to merge 8 commits into
mainfrom
claude/racket-hygiene-tooling-rhpku6

Conversation

@kumavis

@kumavis kumavis commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

What

Correctness-focused hygiene tooling for the Racket tree — the "lint rules" layer, deliberately skipping formatting and Typed Racket. Three new custom lints derived from this repo's own documented bug classes, an orchestrator that also runs the off-the-shelf review linter, and wiring into the pre-commit hook + CI.

New custom lints (all baseline-gated: only NEW findings block)

Lint Bug class it guards Source rule
tools/lint-pnet-registration.rkt AST node missing pnet-serialize registration → silent vector-impostor deserialization failing a distant struct match pipeline.md § New AST Node step 6 (Numerics Q11)
tools/lint-fire-fn-capture.rkt fire fn reads/writes a captured stale network → BSP merge silently drops other propagators' writes propagator-design.md § Fire Function Network Parameter (Track 2 Bug #2, 2B Phase 1a)
tools/lint-memo-hash.rkt equal-based (make-hash) memo on expr trees → depth-bounded hashing degenerates to O(N³) pipeline.md § New Memo/Cache (GitHub #58)

Each is a standalone read-only script (--strict / --save-baseline), self-tested against fixtures reproducing the documented bug shapes (bugs flagged, corrected twins clean).

Orchestrator + wiring

  • tools/lint-hygiene.sh — runs the three new lints + the existing lint-parameters --strict as blocking gates (~2s total), then raco review (report-only, if installed) with require-ordering noise filtered out. Modes: modified (default) / --staged / --all / explicit files.
  • Pre-commit hook gains Gate 3 (--staged); check-parens.sh + the hook now resolve Racket portably ($RACKET → macOS path → PATH), so they work in Linux/CI/web sessions too.
  • CI gains a fast lint job (custom lints block; review reports; no project compile needed).
  • Documented at the ambient tier in .claude/rules/testing.md.

Violation caught and fixed (separate commit, as per instructions)

The gate immediately caught lint-parameters --strict red at HEAD: current-check-fire-invariants? and current-residuation-enabled? were added after the last baseline save. Audited both — #f-default flags that are only ever parameterized (dynamic scope self-unwinds; no imperative mutation sites), so no leakage path → re-baselined per the lint's own resolution flow. The regeneration also dropped 6 stale entries now registered in test-support.rkt.

Discovered debt (baselined, not fixed here)

  • 133 of 345 syntax.rkt structs are unregistered in pnet-serialize.rkt — each is a latent vector-impostor if it ever lands in a cached module body (the expr-p*-if-nar pattern). Now tracked in tools/pnet-registration-baseline.txt; registering them is a follow-up that should be validated by the full suite.
  • raco review --all reports ~1,800 correctness-class warnings tree-wide (mostly unused identifiers/requires) — report-only signal, worth an opportunistic sweep someday. Install once with raco pkg install review.
  • Fire-fn capture scan: 0 findings at HEAD (132 files) — the documented bug class is currently clean.
  • Memo-hash scan: 7 findings, all audited sound (URI-string / shallow-pair / symbol keys) and baselined.

Not included (deliberately)

Typed Racket (too heavy per request), fmt (formatting), resyntax (rules lean stylistic; can be added later diff-scoped if wanted), raco check-requires (needs a compiled tree; review's unused-require warning covers the common case cheaply).

Validation

Racket was installed in this session's container; every lint was run against the tree and against negative-test fixtures; the orchestrator was exercised in passing and failing configurations with exit codes verified; raco make driver.rkt compiles clean post-change (lints are read-only, no production code touched). The full test suite was not run here — no production .rkt semantics changed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GDBXgAcZS43bsR2p6WF1vi


Generated by Claude Code

claude added 8 commits August 5, 2026 22:51
…ation registration

Static guard for pipeline.md § New AST Node step 6: every struct in
syntax.rkt must appear in pnet-serialize.rkt's registration code, else
cached module bodies deserialize it as a raw vector impostor that fails
a distant struct match (the Numerics Q11 failure mode). Reads both
modules via the module reader (comments stripped), compares struct
names against pnet-serialize's code symbols, and flags NEW gaps against
a baseline of the 133 pre-existing unregistered structs (latent debt,
now tracked; the baseline should only shrink).

--strict exits non-zero on new gaps (for hooks/CI); --save-baseline
regenerates. Self-tested: injected probe struct is caught, clean tree
passes strict.

No tests: standalone read-only lint script, validated by direct run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDBXgAcZS43bsR2p6WF1vi
…e networks

Static guard for propagator-design.md § Fire Function Network Parameter:
a fire fn that reads/writes cells through a variable captured from the
installation-time scope operates on a stale network, and BSP's merge of
the returned network silently drops other propagators' writes (Track 2
Bug #2; BSP-LE 2B Phase 1a). Fire scopes are found two ways — lambdas
inside installer-call arguments (net-add-propagator and friends,
elab-add-propagator) and define/let bindings matching #px"fire" — then
every net-cell-*/elab-cell-* call whose network argument is not bound
anywhere inside the scope is flagged. Binder collection (lambda formals,
define, let family, named let, for clauses, match patterns) deliberately
over-approximates so flags are high-signal.

Production tree scans clean (132 files, 0 findings; baseline empty).
Self-tested against a fixture reproducing both documented bug shapes
(for/fold-accumulator capture and installer-position anon lambda) — both
flagged; the corrected twin is not.

No tests: standalone read-only lint script, validated by fixture run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDBXgAcZS43bsR2p6WF1vi
Static guard for pipeline.md § New Memo / Cache Keyed on an AST Node
(the GitHub #58 class): equal-hash-code is depth-bounded (~17 levels),
so an equal-based (make-hash) memo keyed on expr trees collapses deep
terms into a handful of buckets and degenerates to O(N^3) — while
hiding behind a green suite. Line-based scan (comments count — that is
where 'memoization' lives) for make-hash/make-weak-hash within +-2
lines of memo/cache/seen context, keyed by enclosing define name,
gated against a baseline.

The 7 baselined findings were each audited and are sound as-is:
lsp/server.rkt state tables key on URI STRINGS (equal-based required),
macros.rkt coercion-fn-cache keys on freshly-consed shallow pairs
(equal-based required, depth ~2 so the depth bound is harmless), and
pnet-serialize.rkt dynamic-ctor-cache keys on symbols (cold fallback
path). No expr-tree-keyed equal hashes exist at HEAD; the lint gates
new ones.

No tests: standalone read-only lint script, validated by fixture run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDBXgAcZS43bsR2p6WF1vi
…ate 3 + CI job)

Orchestrates the correctness-focused lints: the four custom lints run
BLOCKING (baseline-gated — only NEW findings fail): lint-pnet-
registration, lint-fire-fn-capture, lint-memo-hash, lint-parameters.
raco review (if installed: raco pkg install review) runs REPORT-ONLY
over the target files — unused identifiers/requires, shadowing — with
require-ordering warnings filtered out (formatting, not correctness).
Target selection: modified (default) / --staged / --all / explicit
files. Total custom-lint wall time ~2s, pre-commit viable.

Wiring:
- tools/git-hooks/pre-commit gains Gate 3 (lint-hygiene.sh --staged)
- .github/workflows/test.yml gains a lint job (custom lints block,
  review reports; no project compile needed — the lints are read-only)
- check-parens.sh + pre-commit now resolve Racket portably ($RACKET
  env → project-standard macOS path → PATH), so hooks and CI work on
  Linux/web sessions too
- .claude/rules/testing.md documents the gate at the ambient tier

Note: the gate currently fails on main by design — lint-parameters
--strict catches 2 parameters added since the last baseline save
(current-check-fire-invariants?, current-residuation-enabled?); the
follow-up commit resolves them.

No tests: tooling/CI change; validated by direct runs in both passing
and failing configurations (exit codes verified unpiped).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDBXgAcZS43bsR2p6WF1vi
…e entries out

The new hygiene gate caught lint-parameters --strict failing at HEAD:
current-check-fire-invariants? (propagator.rkt, Scheduler O(diff) S-b
debug gate) and current-residuation-enabled? (global-env.rkt, PPN 4C
Addendum 4B.5.a DQ4 residuation gate) were added after the last
baseline save. Audit: both are #f-default flags that are only ever
parameterize'd (test-scheduler-odiff.rkt / driver.rkt process-file
path) — dynamic scope self-unwinds, no imperative mutation sites exist,
so no cross-test leakage path; recorded in the baseline per the lint's
resolution option 4.

Regeneration also drops 6 entries that are no longer unclassified
(current-schema/selection/session/strategy/process-registry +
current-elaborating-name are since registered in test-support.rkt's
parameterize blocks) and a hand-added dated section comment — the file
is generated (its header says so); durable audit notes belong in commit
messages.

No tests: baseline data file only; verified by lint-parameters --strict
going red -> green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDBXgAcZS43bsR2p6WF1vi
One Racket startup for the whole file set instead of one per file;
--all drops from ~135 startups to 1 (full-tree gate ~2.5min, dominated
by review's per-module analysis; --staged/default stay seconds since
they only review changed files).

No tests: tooling change, validated by timed --all run (exit 0, output
identical modulo batching).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDBXgAcZS43bsR2p6WF1vi
The lint job's review sweep over all ~140 production files blew the
job's 10-min timeout: raco review costs ~10-15s PER FILE on this tree's
large modules (measured: zonk.rkt alone 16.7s; a full sweep is ~25 min).
The blocking custom lints were green in 2.5s; only the report-only step
overran.

- lint-hygiene.sh: new --diff REF mode (merge-base when computable,
  REF itself in shallow CI clones) + LINT_REVIEW_TIMEOUT bound (default
  240s, coreutils timeout when present) with an explicit TRUNCATED note.
- test.yml lint job: PRs fetch the base sha and run --diff over exactly
  the PR's changed files (~seconds); push-to-main runs blocking lints
  only, review already reported per-PR.
- pre-commit Gate 3: LINT_REVIEW_TIMEOUT=60 so commits stay fast;
  comment no longer claims review is ~1-2s.
- testing.md: record the per-file cost + the capping/diff-scoping so
  the next session doesn't re-learn it.

No tests: CI/tooling shell + docs only, no production .rkt semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDBXgAcZS43bsR2p6WF1vi
…n tests

Antagonistic review of the PR's own tooling, every finding confirmed by
a live probe before fixing:

1. fire-fn-capture missed the historical bug shape when the fire fn is a
   NAMED helper passed by reference (no "fire" in the name — the Track
   2B discrimination-propagator form). New rule (c): any define whose
   name appears in an installer call's argument positions is a fire
   scope. Tree-wide findings stay 0 — pure coverage, zero noise.
2. memo-hash regex required a char after "make-hash", so '(make-hash'
   with its argument on the next line evaded the scan entirely. Plain
   bug, fixed with an EOL alternative.
3. memo-hash +-2-line context window missed the ordinary comment-block +
   blank + define layout. Widened to +-4; measured tree-wide cost: one
   extra finding, already covered by an existing baseline key.
4. lint-hygiene review filter dropped only 'should come before' —
   'should come after' require-ordering noise leaked through (observed
   live on zonk.rkt). Filter now covers both spellings.
5. The pre-commit 60s review cap was FICTION on macOS (no coreutils
   timeout there — the primary dev machine ran uncapped). perl-alarm
   fallback (alarm survives exec; rc 142) with the same truncation note.

Tests: the PR's original fixtures lived only in the session scratchpad —
nothing pinned detection behavior. tests/test-hygiene-lints.rkt now runs
each lint against committed fixtures (bug flagged / corrected twin
clean / rule-c helper flagged / EOL memo flagged) plus a pnet-lint
smoke test asserting struct discovery still sees 300+ structs. Fixtures
are .rktl DELIBERATELY: precompile-modules! recursively raco-makes every
tests/**/*.rkt and raco test would execute them — a .rkt fixture with
unbound identifiers breaks both (verified live).

Known limitation left standing (documented, by design): the
over-approximate binder set means an unrelated in-scope binding of the
captured name (e.g. a match pattern) suppresses a true positive —
the false-negative-only direction keeps flag noise at zero.

Validated: 6/6 via raco test; tree-wide strict runs green (0 NEW on all
lints); batch-runner DEAD-WORKERS banner reproduces on an untouched
known-good test in this container, i.e. environmental, not this change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDBXgAcZS43bsR2p6WF1vi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants