diff --git a/.audittraining/path-reference-drift/TYPOLOGY.adoc b/.audittraining/path-reference-drift/TYPOLOGY.adoc index 3d284ea8..4788910c 100644 --- a/.audittraining/path-reference-drift/TYPOLOGY.adoc +++ b/.audittraining/path-reference-drift/TYPOLOGY.adoc @@ -44,7 +44,7 @@ tree, prose, table |lexical |*TP_self_drift* |the repo’s OWN manifest/doc asserts a layout that contradicts its tree |TRUE positive |fix the doc -|*TP_real* |a real non-path defect (e.g. workflow missing +|*TP_real* |a real non-path defect (e.g. workflow missing `+timeout-minutes+`) |TRUE positive |fix the source |*FP_relative* |path is real but unanchored — diff --git a/.audittraining/release-candidates/REPORT.adoc b/.audittraining/release-candidates/REPORT.adoc index 5021c8e7..a666b168 100644 --- a/.audittraining/release-candidates/REPORT.adoc +++ b/.audittraining/release-candidates/REPORT.adoc @@ -143,7 +143,7 @@ file in subdirectory) * Consider publishing to crates.io (for Rust) . *For bunsenite (existing releases):* * Check commits since v1.0.2 -* Review for breaking changes vs. patches +* Review for breaking changes vs. patches * Follow semver for version bump . *Manual verification needed:* * supernorma - check deno.json diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index bfe74e6a..43b9a0cb 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -133,6 +133,9 @@ jobs: # --------------------------------------------------------------------------- # Job 3: Empty-linter — invisible character detection + # Canonical scanner ported from rsr-template-repo (rsr#49): byte-safe two-tier + # design — typographic invisibles warn, C0/NUL corruption blocks. Toolset map: + # hyperpolymath/empty-linter docs/INTEGRATION.adoc # --------------------------------------------------------------------------- empty-lint: name: Empty-linter (invisible characters) @@ -142,39 +145,52 @@ jobs: - name: Checkout repository uses: actions/checkout@v7.0.1 + - name: Prove the scanner trips on known-bad fixtures + run: tests/invisible-characters-test.sh + - name: Scan for invisible characters id: lint run: | - # Inline invisible character detection (from empty-linter's core patterns). - # Checks for: zero-width spaces, zero-width joiners, BOM, soft hyphens, - # non-breaking spaces, null bytes, and other invisible Unicode in source files. - set +e - PATTERNS='\xc2\xa0|\xe2\x80\x8b|\xe2\x80\x8c|\xe2\x80\x8d|\xef\xbb\xbf|\xc2\xad|\xe2\x80\x8e|\xe2\x80\x8f|\xe2\x80\xaa|\xe2\x80\xab|\xe2\x80\xac|\xe2\x80\xad|\xe2\x80\xae|\x00' - find "$GITHUB_WORKSPACE" \ - -not -path '*/.git/*' -not -path '*/node_modules/*' \ - -not -path '*/.deno/*' -not -path '*/target/*' \ - -not -path '*/_build/*' -not -path '*/deps/*' \ - -not -path '*/external_corpora/*' -not -path '*/.lake/*' \ - -type f \( -name '*.rs' -o -name '*.ex' -o -name '*.exs' -o -name '*.res' \ - -o -name '*.js' -o -name '*.ts' -o -name '*.json' -o -name '*.toml' \ - -o -name '*.yml' -o -name '*.yaml' -o -name '*.md' -o -name '*.adoc' \ - -o -name '*.idr' -o -name '*.zig' -o -name '*.v' -o -name '*.jl' \ - -o -name '*.gleam' -o -name '*.hs' -o -name '*.ml' -o -name '*.sh' \) \ - -exec grep -Prl "$PATTERNS" {} \; > /tmp/empty-lint-results.txt 2>/dev/null - EL_EXIT=$? - set -e - - FINDINGS=$(wc -l < /tmp/empty-lint-results.txt 2>/dev/null || echo 0) + RESULTS_FILE="$RUNNER_TEMP/empty-lint-results.bin" + BLOCKING_FILE="$RUNNER_TEMP/empty-lint-blocking-results.bin" + if ! scripts/check-invisible-characters.sh \ + "$GITHUB_WORKSPACE" "$RESULTS_FILE" "$BLOCKING_FILE"; then + echo "::error::Invisible-character scanner failed; refusing a partial pass" + exit 2 + fi + + FINDINGS=0 + while IFS= read -r -d '' filepath; do + FINDINGS=$((FINDINGS + 1)) + REL_PATH="${filepath#"$GITHUB_WORKSPACE"/}" + SAFE_PATH="${REL_PATH//'%'/'%25'}" + SAFE_PATH="${SAFE_PATH//$'\r'/'%0D'}" + SAFE_PATH="${SAFE_PATH//$'\n'/'%0A'}" + SAFE_PATH="${SAFE_PATH//':'/'%3A'}" + SAFE_PATH="${SAFE_PATH//','/'%2C'}" + echo "::warning file=${SAFE_PATH}::Invisible Unicode or C0 characters detected" + done < "$RESULTS_FILE" + + BLOCKING=0 + while IFS= read -r -d '' filepath; do + BLOCKING=$((BLOCKING + 1)) + REL_PATH="${filepath#"$GITHUB_WORKSPACE"/}" + SAFE_PATH="${REL_PATH//'%'/'%25'}" + SAFE_PATH="${SAFE_PATH//$'\r'/'%0D'}" + SAFE_PATH="${SAFE_PATH//$'\n'/'%0A'}" + SAFE_PATH="${SAFE_PATH//':'/'%3A'}" + SAFE_PATH="${SAFE_PATH//','/'%2C'}" + echo "::error file=${SAFE_PATH}::C0 control character or NUL byte detected" + done < "$BLOCKING_FILE" + echo "findings=$FINDINGS" >> "$GITHUB_OUTPUT" - echo "exit_code=$EL_EXIT" >> "$GITHUB_OUTPUT" + echo "blocking=$BLOCKING" >> "$GITHUB_OUTPUT" echo "ready=true" >> "$GITHUB_OUTPUT" - # Emit annotations for each file with invisible chars - while IFS= read -r filepath; do - [ -z "$filepath" ] && continue - REL_PATH="${filepath#$GITHUB_WORKSPACE/}" - echo "::warning file=${REL_PATH}::Invisible Unicode characters detected (zero-width space, BOM, NBSP, etc.)" - done < /tmp/empty-lint-results.txt + if [ "$BLOCKING" -gt 0 ]; then + echo "## Empty-linter: BLOCKED — $BLOCKING file(s) contain C0/NUL corruption" >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi - name: Write summary run: | diff --git a/docs/proof-debt.adoc b/docs/proof-debt.adoc index 945fe7c2..461aeffe 100644 --- a/docs/proof-debt.adoc +++ b/docs/proof-debt.adoc @@ -17,7 +17,7 @@ is the worst-case local-tree value the seed should accept without flagging. *Marker count (canonical / tracked):* 5. *Marker count (local-tree max, -incl. agent worktrees):* 15. +incl. agent worktrees):* 15. This file is the *initial seed* — every marker starts in §(d) DEBT and the maintainer triages each into §(a) / §(b) / §(c) / §(d) as diff --git a/docs/proofs/HANDOVER-neural-convergence.adoc b/docs/proofs/HANDOVER-neural-convergence.adoc index f8ae36c0..c1d2e056 100644 --- a/docs/proofs/HANDOVER-neural-convergence.adoc +++ b/docs/proofs/HANDOVER-neural-convergence.adoc @@ -58,7 +58,7 @@ assertion; Agda retired) |✅ |parser totality |`+verification/proofs/lean4/ParserTotality.lean+` |✅ |ABI package + verify package |`+src/abi/*.idr+` -(incl. `+RuleEngine.idr+`), `+verify/src/*.idr+` |✅ +(incl. `+RuleEngine.idr+`), `+verify/src/*.idr+` |✅ |*Neural convergence — PageRank* |`+verification/proofs/lean4/PageRankInvariants.lean+` |⛔ preconditions diff --git a/docs/status/handover-2026-06-20.adoc b/docs/status/handover-2026-06-20.adoc index 29da026d..2042dbc9 100644 --- a/docs/status/handover-2026-06-20.adoc +++ b/docs/status/handover-2026-06-20.adoc @@ -70,7 +70,7 @@ backlog, 71-alert code-scanning backlog. 4 low). Needs the repo’s Dependabot security tab or a `+security_events+`-scoped token; no MCP tool exposes Dependabot vulnerability alerts in-session. 0 open Dependabot PRs currently; -action-group bumps (e.g. #294) have merged since the issue was filed, so +action-group bumps (e.g. #294) have merged since the issue was filed, so some lows may already be cleared. === Fresh-thread items diff --git a/docs/tech-debt-2026-05-26.adoc b/docs/tech-debt-2026-05-26.adoc index ec890d77..1874f4e6 100644 --- a/docs/tech-debt-2026-05-26.adoc +++ b/docs/tech-debt-2026-05-26.adoc @@ -30,7 +30,7 @@ soundness-relevant escape hatches in Haskell/Rust source. *Recommended next move:* triage each finding into one of: (a) discharge by proof, (b) cover with property-tests + a documented refutation -budget, or (c) annotate as a known/necessary axiom (e.g. `+funExt+`) in +budget, or (c) annotate as a known/necessary axiom (e.g. `+funExt+`) in `+docs/proof-debt.md+`. === 2. Licence debt diff --git a/scripts/check-invisible-characters.sh b/scripts/check-invisible-characters.sh new file mode 100755 index 00000000..877800aa --- /dev/null +++ b/scripts/check-invisible-characters.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Byte-safe scanner for invisible Unicode encodings and forbidden C0 controls. +set -u + +scan_root="${1:-}" +results_file="${2:-}" +blocking_results_file="${3:-}" +grep_bin="${INVISIBLE_GREP_BIN:-grep}" +find_bin="${INVISIBLE_FIND_BIN:-find}" + +if [[ -z "$scan_root" || ! -d "$scan_root" || -z "$results_file" ]]; then + echo "usage: $0 SCAN_ROOT RESULTS_FILE" >&2 + exit 2 +fi + +# Scan bytes under the C locale. This detects UTF-8 encodings even when another +# byte in the file is invalid UTF-8, while excluding permitted TAB/LF/CR bytes. +pattern='[\x00-\x08\x0B\x0C\x0E-\x1F]|\xC2(?:\xA0|\xAD)|\xE2\x80[\x8B-\x8F\xAA-\xAF]|\xE2\x81(?:\xA0|[\xA6-\xA9])|\xEF\xBB\xBF' +blocking_pattern='[\x00-\x08\x0B\x0C\x0E-\x1F]' +: > "$results_file" || exit 2 +if [[ -n "$blocking_results_file" ]]; then + : > "$blocking_results_file" || exit 2 +fi +scan_error=0 +enumeration_file="$(mktemp /tmp/rsr-invisible-files.XXXXXX)" || exit 2 +# Remove the temporary NUL-delimited file enumeration when the scanner exits. +# shellcheck disable=SC2329 +cleanup() { + rm -f -- "$enumeration_file" +} +trap cleanup EXIT + +if ! "$find_bin" "$scan_root" \ + -not -path '*/.git/*' -not -path '*/node_modules/*' \ + -not -path '*/.deno/*' -not -path '*/target/*' \ + -not -path '*/_build/*' -not -path '*/deps/*' \ + -not -path '*/external_corpora/*' -not -path '*/.lake/*' \ + -type f \( -name '*.rs' -o -name '*.ex' -o -name '*.exs' -o -name '*.res' \ + -o -name '*.js' -o -name '*.ts' -o -name '*.json' -o -name '*.toml' \ + -o -name '*.yml' -o -name '*.yaml' -o -name '*.md' -o -name '*.adoc' \ + -o -name '*.idr' -o -name '*.zig' -o -name '*.v' -o -name '*.jl' \ + -o -name '*.gleam' -o -name '*.hs' -o -name '*.ml' -o -name '*.sh' \) \ + -print0 > "$enumeration_file"; then + echo "file enumeration failed: $scan_root" >&2 + exit 1 +fi + +while IFS= read -r -d '' filepath; do + LC_ALL=C "$grep_bin" -aPq "$pattern" "$filepath" + status=$? + case "$status" in + 0) + printf '%s\0' "$filepath" >> "$results_file" || scan_error=1 + if [[ -n "$blocking_results_file" ]]; then + LC_ALL=C "$grep_bin" -aPq "$blocking_pattern" "$filepath" + blocking_status=$? + case "$blocking_status" in + 0) printf '%s\0' "$filepath" >> "$blocking_results_file" || scan_error=1 ;; + 1) ;; + *) echo "blocking-classifier error ($blocking_status): $filepath" >&2; scan_error=1 ;; + esac + fi + ;; + 1) ;; + *) echo "scanner error ($status): $filepath" >&2; scan_error=1 ;; + esac +done < "$enumeration_file" + +exit "$scan_error" diff --git a/src/ui/gossamer/BURBLE-DEFERRAL.adoc b/src/ui/gossamer/BURBLE-DEFERRAL.adoc index a989b204..e84d31e5 100644 --- a/src/ui/gossamer/BURBLE-DEFERRAL.adoc +++ b/src/ui/gossamer/BURBLE-DEFERRAL.adoc @@ -32,7 +32,7 @@ lands: dashboard expects Hypatia to emit findings/dispatches via a Burble session rather than via a direct HTTP read of the harness endpoints. . *Multi-operator session.* Two or more operators need to share Hypatia -state (e.g. a review seat watching the safety triangle live while +state (e.g. a review seat watching the safety triangle live while another operator drives dispatches). . *Voice control reaches the GUI.* Burble’s voice-control plane wants to fire `+Msg.Navigate(Department.Verification)+` or similar from outside diff --git a/tests/invisible-characters-test.sh b/tests/invisible-characters-test.sh new file mode 100755 index 00000000..66af9d38 --- /dev/null +++ b/tests/invisible-characters-test.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +fixture_root="$(mktemp -d /tmp/rsr-invisible-test.XXXXXX)" +# cleanup removes the temporary fixture directory when its path matches the expected safe pattern. +cleanup() { + case "$fixture_root" in + /tmp/rsr-invisible-test.*) rm -rf -- "$fixture_root" ;; + *) echo "refusing unsafe cleanup target: $fixture_root" >&2 ;; + esac +} +trap cleanup EXIT + +scanner="$repo_root/scripts/check-invisible-characters.sh" +results="$fixture_root/results.bin" +blocking_results="$fixture_root/blocking-results.bin" +fixtures="$fixture_root/fixtures" +mkdir -p "$fixtures" + +printf 'tab\tline\ncarriage\rreturn\n' > "$fixtures/safe.md" +printf 'nbsp:\302\240\n' > "$fixtures/nbsp.md" +printf 'soft-hyphen:\302\255\n' > "$fixtures/soft-hyphen.adoc" +printf 'zero-width:\342\200\213\n' > "$fixtures/zero-width.json" +printf 'bidi:\342\200\256\n' > "$fixtures/bidi.toml" +printf 'word-joiner:\342\201\240\n' > "$fixtures/word-joiner.yml" +printf '\357\273\277leading bom\n' > "$fixtures/bom.sh" +printf 'nul:\000byte\n' > "$fixtures/nul.rs" +printf 'backspace:\010byte\n' > "$fixtures/backspace.rs" +printf 'invalid:\377 then nbsp:\302\240\n' > "$fixtures/invalid-utf8.md" +printf 'newline name:\302\240\n' > "$fixtures/with +newline.md" + +"$scanner" "$fixtures" "$results" "$blocking_results" + +count=0 +safe_seen=false +newline_seen=false +while IFS= read -r -d '' filepath; do + count=$((count + 1)) + [[ "$filepath" == "$fixtures/safe.md" ]] && safe_seen=true + [[ "$filepath" == "$fixtures/with"$'\n'"newline.md" ]] && newline_seen=true +done < "$results" + +[[ "$count" -eq 10 ]] || { + echo "expected 10 findings, got $count" >&2 + exit 1 +} +[[ "$safe_seen" == false ]] || { + echo "TAB/LF/CR-only safe fixture was incorrectly reported" >&2 + exit 1 +} +[[ "$newline_seen" == true ]] || { + echo "newline-containing filename was not preserved as one record" >&2 + exit 1 +} + +blocking_count=0 +nul_blocked=false +backspace_blocked=false +while IFS= read -r -d '' filepath; do + blocking_count=$((blocking_count + 1)) + [[ "$filepath" == "$fixtures/nul.rs" ]] && nul_blocked=true + [[ "$filepath" == "$fixtures/backspace.rs" ]] && backspace_blocked=true +done < "$blocking_results" +[[ "$blocking_count" -eq 2 && "$nul_blocked" == true && "$backspace_blocked" == true ]] || { + echo "expected only NUL and backspace fixtures in the blocking set" >&2 + exit 1 +} + +if "$scanner" "$fixture_root/missing" "$results"; then + echo "missing scan root did not fail closed" >&2 + exit 1 +fi + +failing_grep="$fixture_root/failing-grep" +printf '#!/usr/bin/env sh\nexit 2\n' > "$failing_grep" +chmod +x "$failing_grep" +if INVISIBLE_GREP_BIN="$failing_grep" "$scanner" "$fixtures" "$results"; then + echo "grep execution errors did not fail closed" >&2 + exit 1 +fi + +failing_find="$fixture_root/failing-find" +printf '#!/usr/bin/env sh\nexit 2\n' > "$failing_find" +chmod +x "$failing_find" +if INVISIBLE_FIND_BIN="$failing_find" "$scanner" "$fixtures" "$results"; then + echo "find execution errors did not fail closed" >&2 + exit 1 +fi + +echo "invisible-character scanner positive and negative controls passed"