Skip to content

chore: add Code of Conduct, control-byte/bidi CI gate, and pre-merge check verifier (#38, #288, #289) - #293

Merged
dean0x merged 14 commits into
wave/v0.4.0-wave1from
ticket/pr6-community-release-safety
Aug 13, 2026
Merged

chore: add Code of Conduct, control-byte/bidi CI gate, and pre-merge check verifier (#38, #288, #289)#293
dean0x merged 14 commits into
wave/v0.4.0-wave1from
ticket/pr6-community-release-safety

Conversation

@dean0x

@dean0x dean0x commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

This PR must land first in the wave: it remediates 13 live hazard codepoints and adds the source-hygiene gate that three later PRs (which author control-character tests) depend on.

Changes

New files:

  • CODE_OF_CONDUCT.md — Contributor Covenant 2.1
  • scripts/verify-no-control-bytes.mjs — hazard scanner (D-CB1..D-CB8)
  • scripts/verify-pr-checks.mjs — pre-merge verifier (D-PR1..D-PR6)
  • scripts/hooks/pre-commit — opt-in hook (mode 100755); reads git index
  • scripts/__test__/verify-no-control-bytes.spec.mjs — 51 tests, all ACs
  • scripts/__test__/verify-pr-checks.spec.mjs — evaluateChecks offline tests
  • scripts/__test__/fixtures/ — 7 fixture files (upstream CoC, live API snapshots)

Modified files:

Breaking Changes

None. No published wire contracts change. The only crates/ edits are comment text in test files (not compiled into any artifact). WASM size guard is unaffected.

Reviewer Focus Areas

  • PF-018 self-enforcement (D-CB2): the scanner and its tests contain no hazard literals or backslash-u escapes. The full-tree scan (AC-5) and AC-15 (self-scan) verify this.
  • Positive controls (ADR-009/PF-013): AC-7, AC-8, AC-11 each plant a real hostile byte at runtime (never in tracked source) and assert the scanner detects it.
  • Non-vacuity guards (PF-016): AC-6 (zero-files → exit 1), AC-25 (zero check-runs → exit 1), AC-4 (non-vacuity guard fires before evaluateChecks even runs).
  • PF-017 coverage: AC-24 parametrically tests all 8 non-success terminal+non-terminal states for required contexts. AC-23 covers the partial case (gh pr checks --required exits 0 on it, this tool exits 1).
  • --staged index isolation (AC-18): Case A (clean staged + hostile working tree → exit 0), Case B (hostile staged + clean working tree → exit 1) prove the hook reads the index, not the working tree.

Commits

Closes #38, #288, #289

dean0x and others added 14 commits August 13, 2026 00:32
…-018)

Seven U+0085 (C1 NEL) bytes were injected into comment text in three test
files by the edit tooling decoding backslash-u escapes to live bytes. Six
U+2028/U+2029 literals in transform.spec.mjs replace live chars with
String.fromCodePoint() so no hazard literal lives in tracked source.

- crates/mds-cli/tests/cli_lint.rs (5 occurrences): rewrite comment prose
  to say U+0085 without embedding the character. Assertions unchanged.
- crates/mds-napi/__test__/index.spec.mjs (1): same pattern
- crates/mds-wasm/tests/web.rs (1): same pattern
- packages/bundler-utils/__test__/transform.spec.mjs (6): replace live
  U+2028/U+2029 char literals with String.fromCodePoint(0x2028/0x2029)

No logic changes; only comment text and string-construction form change.
This is the S0 step that must land before the control-byte gate (PR6 S1-S6)
so CI is not immediately red on the wave branch.

avoids PF-018, applies D-CB4 (fix rather than allowlist)

Co-Authored-By: Claude <noreply@anthropic.com>
…ifier (#38, #288, #289)

#38 — CODE_OF_CONDUCT.md (Contributor Covenant 2.1):
- Added at repo root with deanshrn@gmail.com as enforcement contact
- Linked from CONTRIBUTING.md and README.md
- Removed satisfied prerequisite from RELEASING.md one-time-prerequisites list
- Upstream fixture committed for offline diff verification (D-COC2)

#288 — scripts/verify-no-control-bytes.mjs:
- Pure Node codepoint scanner; no grep (BSD grep lacks -P, exits 2 silently)
- Hazard class: C0 excl. TAB/LF, DEL, C1 at codepoint level (catches U+0085
  as 0xC2 0x85), 12 Bidi_Control codepoints (Trojan Source / CVE-2021-42574),
  U+2028, U+2029, U+FEFF — derived from common/mod.rs with CR/CRLF exception
- D-CB5: zero-files-scanned is exit 1, not exit 0 (non-vacuity)
- D-CB8: --staged mode reads git index via git cat-file blob, never working tree
- D-CB6: two empty allowlists (BINARY_ALLOWLIST, HAZARD_ALLOWLIST); stale
  entries are self-invalidating (exit 1)
- CI: source-hygiene job in ci.yml (pull_request); step in release.yml
  version-gate (tag pushes); opt-in pre-commit hook at scripts/hooks/pre-commit
- 51-test suite covering all ACs including positive controls (ADR-009/PF-013),
  golden class set (D-CB1a), and staged-vs-working-tree isolation (AC-18)

#289 — scripts/verify-pr-checks.mjs:
- D-PR1: pure evaluateChecks() function; gh runner injected for offline tests
- D-PR2: required contexts read live from branch protection (exit 2 on 404/403);
  --required-from flag for unprotected wave-branch base
- D-PR2a: required contexts resolved against union of check-runs AND statuses
- D-PR3: Tier A (required, must be completed+success), Tier B (non-required,
  failure/cancelled/stale = FAIL), Tier C (legacy statuses, advisory)
- D-PR4: zero check-runs = exit 1 (the #239 shape — not a pass)
- D-PR4a: filter=latest pinned; pagination bounded at MAX_PAGES=20, exit 2
- D-PR5: PASS emits gh pr merge --squash --match-head-commit <sha>
- D-PR6: exit 0 PASS, 1 FAIL, 2 indeterminate; never 0 for "cannot tell"
- Fixtures from live API: 113f472 (18 checks, PASS), f168944/#239 (0 checks,
  FAIL), e9dace1/#240 (0 checks + Snyk error, FAIL)
- Partial case: 17 of 18 required contexts present → FAIL naming the absent one

applies ADR-009, avoids PF-013, avoids PF-016, avoids PF-017, avoids PF-018

Co-Authored-By: Claude <noreply@anthropic.com>
- verify-no-control-bytes.mjs: drop unused execFileSync/join imports;
  simplify hexContext double-ternary to template literal; drop redundant
  entry.staged in read-mode branch (isStaged already covers all staged
  entries set by getStagedFiles)
- verify-pr-checks.mjs: remove dead checkGhVersion function (never
  called; ghVersion() is the real path); remove spurious async from
  main() (contains no await)
- verify-no-control-bytes.spec.mjs: drop unused mkdirSync import
- verify-pr-checks.spec.mjs: drop unused dirname import; remove unused
  loadStatuses call in 113f472 baseline test (result was loaded but []
  was passed to evaluateChecks)

All 51 tests pass; scanner passes its own gate.
Scanner + verifier (#288, #289):
- Entry-point guard no longer compares import.meta.url to a hand-built
  file:// string. That comparison is false for any path with a space and for
  any symlinked path (macOS /tmp, /var/folders), so main() never ran and both
  tools exited 0 having scanned/verified nothing — a silent pass. Now compared
  by realpath, with a regression test that runs each script from a spaced path.
- evaluateChecks now exits 2 (indeterminate) instead of 0 when the required
  context set is empty; fetchRequiredContexts exits 2 when a protected branch
  lists zero required checks, and reads the UNION of contexts[] and checks[].
  'All 0 required contexts passed' was a vacuous green (applies ADR-009).
- A required context is now evaluated across EVERY check-run sharing its name;
  previously a later success masked an earlier failure.
- HAZARD_ALLOWLIST entries can now be exercised: scanBuffer reports allowlisted
  hits so the caller records them, instead of every valid entry reading stale.
- --staged mode exits 2 when 'git diff --cached' fails instead of treating the
  failure as 'nothing staged' and passing the pre-commit hook.

Tests:
- Replaced the AC-28/AC-29 source-text greps (asserting a file contains the
  string 'process.exit(2)' proves nothing about reachability) with 16 tests that
  drive main() through an injected gh runner: 404, 403, stale gh, page cap,
  truncated page set, empty required set, API error, call budget, filter=latest.
- Added AC-17 allowlist cases, AC-20 symlink skip, AC-16 git-not-on-PATH, and
  entry-point regressions. Mutation-checked: reverting each fix fails tests.
- CoC fixture provenance is pinned to a measured sha256 + exact byte count;
  'hash.length === 64' is true of every sha256 and asserted nothing. Fixture
  normalized to be byte-identical to the upstream 2.1 body.

Docs: corrected the U+0085 remediation comments, which described the Rust
escape form while the assertions check the 6-character JSON escape.
…C-30, scope)

AC-6: remove `&& !isStaged` guard from non-vacuity check in
verify-no-control-bytes.mjs (D-CB5). A --staged scan that finds zero
staged files previously exited 0, printing "Scanned 0 file(s)" —
indistinguishable from a working scan of a clean index. Now exits 1
unconditionally when fileEntries is empty. Test added.

AC-16: change exit 2 → exit 1 for 'git not on PATH' and 'not inside a
git work tree' in verify-no-control-bytes.mjs. AC-16 and D-CB5 mandate
exit 1 for all four named failure conditions; these two are known,
named failures (fail-closed), not indeterminate tool errors (exit 2).
Tests updated; header comment revised.

AC-22: evaluateChecks in verify-pr-checks.mjs now names every required
context as absent when nChecks===0, instead of returning after the
single "zero check-runs" line. The test titled "naming all 6 required
contexts" now asserts that all six context strings appear in the
failure output.

AC-30: pre-compute hexCtx inside the hazard-hit scan loop and store it
in hazardHits instead of the raw `buf`. The file buffer is released at
the end of each file's iteration rather than being retained until all
files are scanned. Test added to prove hex context is reported
correctly across multiple hazardous files.

Scope: remove unplanned --head-sha override from verify-pr-checks.mjs
(USAGE string, header comment, argv parsing, and headShaOverride usage
in main()). The plan did not include this flag; it had zero test
coverage and no provenance note.

Self-verification: positive control (planted 0x1B) still exits 1;
full-tree scan of 514 files / 4,609,419 bytes exits 0.

Co-Authored-By: Claude <noreply@anthropic.com>
Explicit enumeration at ci.yml:331 deviated from plan S2/AC-19 and the
established repo convention (7 of 8 node --test call sites use the
*.spec.mjs glob). Any spec file added to scripts/__test__/ in the
future would be silently skipped — the same "gate that matches nothing"
failure mode this PR exists to eliminate, one layer up.

Bash on ubuntu-latest expands the glob before passing arguments to node;
a non-matching glob (no .spec.mjs files) makes node --test error rather
than silently succeed, so the gate remains fail-closed.

Co-Authored-By: Claude <noreply@anthropic.com>
…ent exit codes

D-CB5 requires the gate to fail closed. Previously the hook printed a
'skipping' message and exited 0 when scripts/verify-no-control-bytes.mjs
was not found — meaning a commit that deletes or renames the scanner
silently disabled the local gate. Change to exit 1 with an actionable
message directing the developer to restore the script or pass --no-verify
deliberately.

Also correct the exit-code header: (a) exit 1 now covers both the
hazard-byte and missing-scanner cases, and (b) document the exit-2
path that propagates from the scanner's three-value contract but was
previously undocumented in the hook header.

Co-Authored-By: Claude <noreply@anthropic.com>
…tore BREAKING-first order

The PR6 commit introduced a second `### Added` heading at the top of the
[Unreleased] block, ahead of all the `### **BREAKING**` sections.  This
violated the file's established ordering (both [Unreleased] and [0.3.0] lead
with BREAKING) and left a duplicate heading (one already existed at line 452).

Fix: remove the stray `### Added` block from the top of [Unreleased]; merge
its three bullets (Code of Conduct #38, source-hygiene gate #288, pre-merge
verifier #289) into the existing `### Added` section at the end of the block,
just before `### Changed`.

Co-Authored-By: Claude <noreply@anthropic.com>
The version-gate job in release.yml ran the source-hygiene scanner
(verify-no-control-bytes.mjs) but not the positive-control and class-
completeness suite (scripts/__test__/*.spec.mjs). Because ci.yml does
not trigger on tag pushes, the suite that pins HAZARD_RANGES (D-CB1a)
never executed on the release path.

A regression that silently narrowed the hazard class would exit 0 in
the release gate while being caught on PRs and pushes to main. The fix
is one additional step — the same glob ci.yml uses — added immediately
after the scanner step.

Applies ADR-009 / avoids PF-013: a completeness claim must be backed
by a positive control that detects the hostile artifact when present.

Co-Authored-By: Claude <noreply@anthropic.com>
CONTRIBUTING.md hazard-class description (lines 82-86) misplaced U+2028,
U+2029, and U+FEFF inside the twelve Bidi_Control=Yes codepoints. They are
not Bidi_Control members; U+061C is. Corrected to: 'the twelve Unicode
Bidi_Control=Yes codepoints including U+061C (Trojan Source, CVE-2021-42574),
plus U+2028 (LS), U+2029 (PS), and U+FEFF (BOM).' This matches
verify-no-control-bytes.mjs and CHANGELOG.md, which both already stated it
correctly.

CONTRIBUTING.md Merging section exit-code paragraph and CHANGELOG.md entry
for #289 both documented exit 1 as only 'a required context is missing or
not successful'. This understated the contract: exit 1 also fires for Tier B
(any non-required check-run concluded failure/cancelled/timed_out/
action_required/stale) and for zero check-runs. Added Tier A/B/C taxonomy.
Added a 'Tier B is load-bearing' note because source-hygiene is NOT a required
branch-protection context, making Tier B the only binding mechanism for admin
merges.

CONTRIBUTING.md Scope paragraph now states two additional limitations: (a)
source-hygiene is not a required context so --admin bypasses it outright and
Tier B is the binding mechanism, (b) Tier B skips queued/in_progress
check-runs, so a verifier pass issued while source-hygiene is still running
has verified nothing about source hygiene.

CONTRIBUTING.md Source hygiene section now documents the scanner's 0/1/2
exit-code contract, matching the verifier's documented contract in the same
file.

Co-Authored-By: Claude <noreply@anthropic.com>
… pre-flight

Root package.json already has the script (landed in HEAD), CONTRIBUTING.md
already references it, but RELEASING.md pre-flight was missing it. Any
contributor who edits the gate scripts locally and breaks detection gets zero
signal until CI. Now both normative docs list the local invocation path.

Also expand AC-15 spec to cover the PF-018 backslash-u-escape vector across all
gate scripts and spec files, not just the scanner source. Builds the detection
pattern from char codes so the test cannot trip its own new rule.

Co-Authored-By: Claude <noreply@anthropic.com>
The spec file includes itself in the AC-15 fileSet (along with the other
gate scripts). The HEAD version used src.includes('grep -P') to check
for the forbidden grep flag, but that string literal is present in the
spec file itself — causing the test to fail its own self-check.

Fix: build the forbidden-flag string by concatenation ('grep' + ' -P')
and build the test description without the contiguous substring, so the
spec file passes its own check.

Avoids PF-018 (edit tooling injects live bytes/strings that trip the
very gates they guard).

Co-Authored-By: Claude <noreply@anthropic.com>
…ation

Move the AC-1/AC-2 Code of Conduct describe block from
scripts/__test__/verify-pr-checks.spec.mjs into its own
scripts/__test__/code-of-conduct.spec.mjs so the repo's
one-spec-per-module convention is upheld and the CI glob
(scripts/__test__/*.spec.mjs, already in place) picks it
up automatically.

Also strengthens the provenance comment: adds the exact
upstream URL, the awk front-matter-stripping command, and
the unstripped upstream digest at capture time so a
reviewer can re-derive FIXTURE_SHA256 independently rather
than trusting the self-measured constant (applies ADR-009,
avoids PF-013).  Verified 2026-08-13: the strip command
produces sha256 369bf730.../5478 bytes, matching the
committed fixture exactly.

Remove now-unused `statSync` and `createHash` imports from
verify-pr-checks.spec.mjs.

Co-Authored-By: Claude <noreply@anthropic.com>
Finding #1/#5 — deletion-only commits rejected by non-vacuity guard:
In --staged mode, getStagedFiles() uses --diff-filter=ACMR which excludes
deletions. A deletion-only commit yields zero ACMR-filtered paths, tripping
the D-CB5 non-vacuity guard (exit 1) and blocking a valid `git rm` commit.

Finding #2 (HIGH) — git commit --amend --no-edit rejected:
During amend the index equals HEAD, so `git diff --cached --diff-filter=ACMR`
is legitimately empty. The guard exited 1, blocking a routine workflow and
training contributors to reach for --no-verify.

Fix: D-CB5 non-vacuity guard now applies to full-tree mode only. In --staged
mode, when the ACMR-filtered set is empty the scanner runs an unfiltered
`git diff --cached --name-only -z` check and exits 0 with an explicit message:
'0 content-bearing staged paths (N deletion(s)) — nothing to scan' for the
deletion-only case, or 'no staged content — nothing to scan' for amend/empty.
Exit 1 is retained only for the full-tree scan (broken path discovery) and for
genuine tool failures (exit 2).

Finding #3 — decodeUtf8 materialised entire file as heap-object array:
Removed decodeUtf8(); fused UTF-8 decode + hazard check into a single inline
pass in scanBuffer(). Only hit records are allocated. Eliminates the ~53x heap
amplification measured on package-lock.json (148 KB file → 7.5 MB heapUsed
delta). The nextCp CRLF look-ahead uses buf[nextStart] (lead byte), which is
correct because isHazardous() only compares nextCp to 0x0A, and LF is ASCII.

Finding #4 — one git cat-file subprocess per staged file:
Replaced readIndexBlob() (N per-file spawns) with readAllIndexBlobs() which
uses `git cat-file --batch` to read all staged blobs in a single subprocess.
Measured: 300 staged files at ~11 ms/spawn → ~3.4 s eliminated.

Tests: updated AC-6 --staged test to expect exit 0; added deletion-only
commit test; incorporated prior session's AC-15 grep-P / backslash-u test.
All 37 tests pass. Full-tree scan: 515 files, 4,629,254 bytes, 0 hits.

Co-Authored-By: Claude <noreply@anthropic.com>
@dean0x
dean0x merged commit 282af17 into wave/v0.4.0-wave1 Aug 13, 2026
16 checks passed
@dean0x
dean0x deleted the ticket/pr6-community-release-safety branch August 13, 2026 07:56
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.

1 participant