Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/rules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@
- **Pre-push gate**: A git pre-push hook (`.git/hooks/pre-push`) runs the full suite if no `timings.jsonl` entry exists for HEAD. If a run already exists for the current commit, the hook skips (no redundant re-run). Bypass with `git push --no-verify` in emergencies.
- **Pre-commit gate**: A git pre-commit hook (`tools/git-hooks/pre-commit`, installed via `tools/install-git-hooks.sh`) runs `tools/check-parens.sh` on staged `.rkt` files. ~100ms per file (read-syntax via Racket); blocks the commit on delimiter mismatch with exact line:column. Closes the bug class where mechanical edits (sed surgery, batch refactors) introduce unbalanced parens that break `raco pkg install` downstream — a pattern that hit `main` once already (commit `d7bd97a4`, fixed in PR #29). Bypass with `git commit --no-verify` for genuine emergencies; failures otherwise are real bugs to fix before committing.
- **Parameter-leakage lint** (A3-static-lint, BSP-LE Track 2B addendum) -- `racket tools/lint-parameters.rkt` classifies each `make-parameter` call as private / test-registered / unclassified. Uses a baseline file (`tools/parameter-lint-baseline.txt`) to track currently-accepted unclassified parameters; only flags NEW additions. Run: `racket tools/lint-parameters.rkt` (report), `--strict` (exit non-zero if new unclassified found — for CI / manual audit), `--save-baseline` (accept current state as new baseline). Architectural answer is PM Track 12 (parameters → cells for module loading) which obsoletes this lint. Longitudinal pattern 7 (two-context boundary bugs, 6+ PIRs) — tactical near-term protection against silent regressions.
- **Hygiene lint gate** (2026-08-05) -- `tools/lint-hygiene.sh` runs the correctness lints in one shot; it is pre-commit Gate 3 and a CI job. BLOCKING (baseline-gated, only NEW findings fail): `tools/lint-pnet-registration.rkt` (AST node missing pnet-serialize registration → the vector-impostor class, pipeline.md § New AST Node step 6), `tools/lint-fire-fn-capture.rkt` (fire fn reads/writes a captured stale network → the silent write-loss class, propagator-design.md § Fire Function Network Parameter), `tools/lint-memo-hash.rkt` (equal-based hash near memo/cache context → the depth-bounded-hash O(N³) class, GitHub #58), plus `lint-parameters --strict`. REPORT-ONLY: `raco review` (unused identifiers/requires, shadowing; install once with `raco pkg install review`; require-ordering noise is filtered). ⚠ review costs ~10-15s PER FILE on this tree's large modules (a full-tree sweep is ~25 min — it blew CI's 10-min lint job on PR #81), so the review step is time-capped via `LINT_REVIEW_TIMEOUT` (default 240s; pre-commit uses 60s) and CI diff-scopes it to the PR's changed files (`--diff REF`); the blocking custom lints always scan the whole tree (~2s). Each lint also runs standalone with `--strict` / `--save-baseline`; baselines live next to the lints in `tools/` and should only shrink. When a blocking lint fires on your commit, fix the finding (the report names the rule doc) — baseline updates are for audited-safe cases only, stated in the commit message.
34 changes: 34 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,40 @@ on:
pull_request:

jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- uses: actions/checkout@v4

- name: Install Racket
uses: Bogdanp/setup-racket@v1.11
with:
version: '9.0'

- name: Install review linter
run: raco pkg install --auto --skip-installed review

# raco review costs ~10-15s PER FILE on this tree's large modules — a
# full-tree sweep is ~25 min and blew this job's 10-min timeout (PR #81).
# The blocking custom lints always scan the whole tree (~2s); only the
# report-only review step is diff-scoped to what the PR touched.
- name: Fetch PR base (for diff-scoped review)
if: github.event_name == 'pull_request'
run: git fetch --no-tags --depth=1 origin ${{ github.event.pull_request.base.sha }}

- name: Hygiene lints (custom lints blocking, raco review report-only)
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
tools/lint-hygiene.sh --diff ${{ github.event.pull_request.base.sha }}
else
# push to main: blocking lints over the whole tree; review skipped
# (clean checkout has no modified files — the per-PR report already
# covered these changes on the way in).
tools/lint-hygiene.sh
fi

test:
runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down
9 changes: 9 additions & 0 deletions racket/prologos/tests/lint-fixtures/fire-clean.rktl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#lang racket/base
;; FIXTURE: corrected twin — every cell op goes through the fire fn's own
;; `net` parameter. Must produce zero findings.
(define n (make-network))
(define (install!)
(net-add-propagator n (list some-cid) (list result-cid)
(lambda (net)
(define val (net-cell-read net some-cid))
(net-cell-write net result-cid val))))
10 changes: 10 additions & 0 deletions racket/prologos/tests/lint-fixtures/fire-doc-bug.rktl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#lang racket/base
;; FIXTURE (read by lint, never compiled): the exact WRONG example from
;; propagator-design.md § Fire Function Network Parameter — the fire fn
;; writes through the captured installation-time network `n`.
(define n (make-network))
(define (install!)
(net-add-propagator n (list some-cid) (list result-cid)
(lambda (net)
(define val (net-cell-read net some-cid))
(net-cell-write n result-cid val))))
8 changes: 8 additions & 0 deletions racket/prologos/tests/lint-fixtures/fire-named-helper.rktl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#lang racket/base
;; FIXTURE: the Track 2B "discrimination propagator" shape written as a
;; NAMED helper (no "fire" in the name) passed by reference — rule (c).
(define n (make-network))
(define (discriminate-step net2)
(net-cell-write n out-cid 42))
(define (install!)
(net-add-propagator n (list in-cid) (list out-cid) discriminate-step))
3 changes: 3 additions & 0 deletions racket/prologos/tests/lint-fixtures/memo-clean.rktl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#lang racket/base
;; memoization table — FIXTURE: eq-keyed twin, must produce zero findings.
(define memo-table (make-hasheq))
6 changes: 6 additions & 0 deletions racket/prologos/tests/lint-fixtures/memo-eol.rktl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#lang racket/base
;; memoization table for whnf results — FIXTURE: hash constructor with its
;; argument on the next line; the end-of-line regex case.
(define memo-table
(make-hash
))
106 changes: 106 additions & 0 deletions racket/prologos/tests/test-hygiene-lints.rkt
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#lang racket/base

;;;
;;; test-hygiene-lints.rkt — regression tests for the custom hygiene lints
;;;
;;; The lints (tools/lint-fire-fn-capture.rkt, tools/lint-memo-hash.rkt,
;;; tools/lint-pnet-registration.rkt) are static guards for documented bug
;;; classes. These tests pin their DETECTION behavior against committed
;;; fixtures in tests/lint-fixtures/ — bug shapes flagged, corrected twins
;;; clean — so a lint refactor cannot silently lose the very case each lint
;;; exists for. (PR #81's original validation ran against session-local
;;; fixtures that were never committed; this file closes that gap.)
;;;
;;; Fixtures are READ by the lints, never compiled — they reference unbound
;;; identifiers freely. They use the .rktl extension DELIBERATELY: the suite
;;; runner's precompile-modules! walks tests/ RECURSIVELY compiling every
;;; *.rkt (bench-lib.rkt), and `raco test tests/` would try to run them —
;;; a .rkt fixture with unbound identifiers breaks both. .rktl is invisible
;;; to every \.rkt$ glob while the lints read file CONTENT, not extension.
;;;

(require rackunit
racket/port
racket/system
racket/path)

(define here (path-only (path->complete-path (syntax-source #'here))))
(define tools-dir (simplify-path (build-path here 'up "tools")))
(define fixtures-dir (build-path here "lint-fixtures"))
;; exec-file can come back RELATIVE (just "racket"); system* does not search
;; PATH, so resolve it ourselves in that case.
(define racket-bin
(let ([p (find-system-path 'exec-file)])
(if (absolute-path? p) p (or (find-executable-path p) p))))

;; Run a lint script on fixture files; return (values exit-ok? output).
;; Without --strict the lints always exit 0, so detection is asserted via
;; the "NEW: n" count in the report line.
(define (run-lint lint-name . fixture-names)
(define script (build-path tools-dir (string-append lint-name ".rkt")))
(define args (for/list ([f (in-list fixture-names)])
(path->string (build-path fixtures-dir f))))
(define out (open-output-string))
(define ok?
(parameterize ([current-output-port out]
[current-error-port out])
(apply system* racket-bin (path->string script) args)))
(values ok? (get-output-string out)))

(define (new-count output)
(define m (regexp-match #px"NEW: (\\d+)" output))
(and m (string->number (cadr m))))

;; ============================================================
;; lint-fire-fn-capture
;; ============================================================

(test-case "fire-fn-capture: the propagator-design.md WRONG example is flagged"
(define-values (ok? out) (run-lint "lint-fire-fn-capture" "fire-doc-bug.rktl"))
(check-true ok? out)
(check-equal? (new-count out) 1 out)
;; the report names the captured variable
(check-regexp-match #px"uses 'n'" out))

(test-case "fire-fn-capture: corrected twin (net param everywhere) is clean"
(define-values (ok? out) (run-lint "lint-fire-fn-capture" "fire-clean.rktl"))
(check-true ok? out)
(check-equal? (new-count out) 0 out))

(test-case "fire-fn-capture: named helper passed by reference is flagged (rule c)"
(define-values (ok? out) (run-lint "lint-fire-fn-capture" "fire-named-helper.rktl"))
(check-true ok? out)
(check-equal? (new-count out) 1 out))

;; ============================================================
;; lint-memo-hash
;; ============================================================

(test-case "memo-hash: (make-hash at end-of-line near memo context is flagged"
(define-values (ok? out) (run-lint "lint-memo-hash" "memo-eol.rktl"))
(check-true ok? out)
(check-equal? (new-count out) 1 out))

(test-case "memo-hash: make-hasheq twin is clean"
(define-values (ok? out) (run-lint "lint-memo-hash" "memo-clean.rktl"))
(check-true ok? out)
(check-equal? (new-count out) 0 out))

;; ============================================================
;; lint-pnet-registration (fixed surfaces: syntax.rkt + pnet-serialize.rkt —
;; no fixture mode; pin that it parses the real tree and stays baseline-clean)
;; ============================================================

(test-case "pnet-registration: parses the real tree, strict-clean vs baseline"
(define script (build-path tools-dir "lint-pnet-registration.rkt"))
(define out (open-output-string))
(define ok?
(parameterize ([current-output-port out]
[current-error-port out])
(system* racket-bin (path->string script) "--strict")))
(define s (get-output-string out))
(check-true ok? s)
;; struct discovery actually worked (the tree has 300+ expr structs);
;; a parse regression that found 0 structs would otherwise pass silently
(define m (regexp-match #px"\\((\\d+) structs in syntax.rkt\\)" s))
(check-true (and m (> (string->number (cadr m)) 300)) s))
7 changes: 7 additions & 0 deletions racket/prologos/tools/fire-fn-capture-baseline.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# fire-fn-capture-baseline.txt
# Accepted fire-scope captured-network findings as of last baseline
# save (key: file::op::variable). Each entry should be audited: a
# genuine capture is the silent stale-network bug of
# propagator-design.md § Fire Function Network Parameter.
# Regenerate with: racket tools/lint-fire-fn-capture.rkt --save-baseline

Loading
Loading