Skip to content
Merged
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
19 changes: 14 additions & 5 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,18 @@ else
fi

# fallow — block the commit on new findings in the changes you're staging.
# --health-baseline grandfathers the same known-complex functions CI does
# (.github/workflows/fallow.yml), so editing a file that merely CONTAINS a
# pre-existing god-function doesn't false-RED; genuinely new complexity (not in
# the baseline) still blocks. Keeps local === CI.
# Keep both commands aligned with .github/workflows/fallow.yml: audit catches
# dead code/duplication and the dedicated production health pass enforces the
# same complexity threshold that gates pull requests. The staged diff keeps
# unrelated working-tree edits out of the line-level verdict.
echo "› fallow"
npx fallow audit --base HEAD --health-baseline .fallow/health-baseline.json
FALLOW_STAGED_DIFF="$(mktemp)"
trap 'rm -f "$FALLOW_STAGED_DIFF"' EXIT HUP INT TERM
git diff --cached --unified=0 -- . > "$FALLOW_STAGED_DIFF"
npx --no-install fallow audit --base HEAD --health-baseline .fallow/health-baseline.json
npx --no-install fallow health \
--production \
--baseline .fallow/health-baseline.json \
--changed-since HEAD \
--diff-file "$FALLOW_STAGED_DIFF" \
--fail-on-issues
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Fixed

- **Report-only correspondence can no longer erase reviewable certification
evidence and render a false “all surfaces identical” result.** If unique path
pairing collapses every presentation finding while the raw differ still has
reviewable DOM/style/state evidence, `reportConsistency` now fails closed as
`presentation_collapsed_while_raw_reviewable`. The report explains the
contradiction and the Action maps it to `CERTIFICATION_FAILED`, which visual
approval cannot clear.

## [4.7.5] - 2026-08-06

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1087,7 +1087,7 @@ Non-visual and framework-injected elements (`<meta>`/`<title>`/`<script>`/`<styl
- `styleproof-init` — scaffold the gate: the capture spec (inventory guard on; Next.js repos get route discovery + the coverage guard, others a crawl-by-default spec), a dedicated `playwright.styleproof.config.ts` (production-build `webServer`, parallel capture), `.gitignore` cache entries, the restore-first report workflow, the approval workflow, and the restore-or-publish pre-push hook. One command. The CI hot path restores exact-SHA maps and runs no browser; a compatible base hit plus head miss captures only the head; a base miss captures the pair; every fallback is published for reuse. Generated commands follow the repo's lockfile (`bun.lock`/`bun.lockb`, `pnpm-lock.yaml`, `yarn.lock`, or npm by default), respect pnpm/Corepack version pins, and detect Vite/Next production preview commands instead of assuming every repo has `start`. Generated files carrying StyleProof's ownership marker are **machine-owned** thin wrappers over packaged commands: after upgrading styleproof, `styleproof-init --check` reports whether they drifted from the release's templates (exit 1 — wire it into CI), and `styleproof-init --upgrade` refreshes them in place without touching your spec, playwright config, or a repository-owned Husky hook. Use the explicit `--hook` command only when you intend to replace that hook.
- `styleproof-map` — capture the current commit's computed-style map through Playwright. By default it writes `.styleproof/maps/current`, keeps screenshots for reports, writes a manifest, and uploads to `styleproof-maps` outside CI when the working tree was clean and a git remote exists. Pass `--crawl-base-url` plus repeated `--crawl-route` to run `styleproof-variants` before capture, `--no-upload`, `--restore --sha <commit>`, `--spec`, `--dir`, `--base-dir`, `--no-screenshots`, or repeated `--dirty-allow <path>` (a tracked file a dev tool rewrites on every run — e.g. `next dev` regenerating a `tsconfig.json` — that must not mark the capture dirty) for custom flows.
- `styleproof-diff` — the certify gate. With no args, it restores cached maps for the current commit and inferred base (`GITHUB_BASE_REF`, `branch.<name>.gh-merge-base`, `gh pr view`, then main/master fallbacks); `styleproof-diff main` / `styleproof-diff master` pins the base; `styleproof-diff <beforeDir> <afterDir>` keeps the manual two-directory form for CI fallback captures. Exits `0` certified (identical); `1` on a reviewable diff — computed-style/DOM/state differences, and equally an unacknowledged inventory removal, an unacknowledged failing data endpoint under an armed `dataResidue: 'gate'`, an incomplete coverage registry, or an unproven-determinism capture; `2` on a usage/capture error (including a **manifest-less side** — since **v4**, a two-directory compare where a side ships maps but no `styleproof-manifest.json` is refused loudly, naming the bare side(s), because the same-environment guard can't be enforced without one; re-capture with current StyleProof; **and** a **missing map** — a bundle that claims to exist yet holds zero captures, i.e. a `styleproof-manifest.json` present with no maps, on either side, or a head capture that produced nothing; refused loudly rather than mislabelled as all-new — **and** the no-args case where the cached base map can't be restored at all: no map-store remote, no cached bundle, nothing to compare. A "nothing was compared" outcome always exits `2`, never a soft `0` that would read as certified; the error names the two ways forward — run in CI where the base is restorable, or use the two-directory form); `3` when only new surfaces are present — surfaces captured only on the **head** side (a surface present only on the **base** side is a **removed** surface, a reviewable change: exit `1`) — (no baseline for _those_ surfaces to diff against — new surfaces against an existing baseline, or a base dir with no maps at all (and hence no manifest), meaning no baseline was ever captured: the first-adoption review path; approval policy decides whether to gate). A clean run prints `0 changed surfaces across N captured surface(s)`, and `--json` includes `compared`. The human output **groups the same way the report does**: surfaces that changed identically collapse into one finding (with the per-surface count on its header), longhands fold into shorthands, and size/position-derived longhands fold behind a `(+N derived longhands)` count — so one real change reads as one entry, not dozens of raw lines. A change that rode the shared frame every view draws (a persistent nav/header/footer) is promoted to a "🧱 Global chrome change" callout up top. `--json` stays the complete, unchanged machine contract — every surface and every raw longhand — regardless of the human grouping.
- `styleproof-report` — render the diff to a Markdown report with before/after crops. With no args, it reports cached maps for the current commit against the inferred base; `styleproof-report main` / `styleproof-report master` pins the base; `styleproof-report <beforeDir> <afterDir> --out <dir>` keeps the manual two-directory form. Add `--include-content` for the opt-in, advisory content section (see above). Shares the same comparison truth as `styleproof-diff` (`reviewableCounts` / `reportConsistency` in `report.json`): raw-only derived reflow noise never claims “all surfaces identical” and exits `1` fail-closed instead of green-with-no-crops.
- `styleproof-report` — render the diff to a Markdown report with before/after crops. With no args, it reports cached maps for the current commit against the inferred base; `styleproof-report main` / `styleproof-report master` pins the base; `styleproof-report <beforeDir> <afterDir> --out <dir>` keeps the manual two-directory form. Add `--include-content` for the opt-in, advisory content section (see above). Shares the same comparison truth as `styleproof-diff` (`reviewableCounts` / `reportConsistency` in `report.json`): raw-only derived reflow noise never claims “all surfaces identical,” and neither does report-only path correspondence when it collapses every presentation finding while raw reviewable evidence remains. Both contradictions fail closed instead of producing a green report with no crops.
- `styleproof-capture` — one-shot capture of any URL (no spec): `styleproof-capture <url> --key <name> --out <dir>`, with `--widths` (omit to auto-detect `@media` bands), `--wait <selector>`, `--ignore <selector>`, `--no-screenshots`, and the crawler flags (`--crawl`, `--setup <file>`, `--require-full-coverage` → exit 4 on residue, `--until-covered`, `--workers <n>`, `--no-data-states`) described in [Match a design pixel-for-pixel](#match-a-design-pixel-for-pixel).
- `styleproof-variants` — crawl a running app for one-step state variants and write `styleproof.variants.generated.json`. Pass `--base-url`, repeat `--route`, and use `--strict` when unresolved skipped/live candidates should fail automation.
- `styleproof-prepush` — the canonical pre-push flow, packaged: reads git's refspecs from stdin, captures the pushed commit only when its tip is the checked-out tree, skips docs-only pushes, restores an already-published exact-SHA map or captures and publishes once, then runs the advisory diff. The hook `styleproof-init` writes is a two-line shim that execs the installed local binary directly, so the rules update with each release instead of drifting in a copied hook file and a missing install fails instead of falling through to a package-registry download — refresh an old hook with `styleproof-init --hook`.
Expand Down
38 changes: 28 additions & 10 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,34 @@ runs:
# Both need human review in approval mode. Every defined verdict publishes a receipt.
if [ "$code" -eq 1 ] || [ "$code" -eq 3 ]; then echo "changed=true" >> "$GITHUB_OUTPUT"; else echo "changed=false" >> "$GITHUB_OUTPUT"; fi

# Generate the human report before trust classification. Report-only structural
# correspondence can discover a presentation/certification contradiction that the
# raw differ cannot know about; merge that one coherence verdict back into the
# machine payload so approval can never clear hidden raw review evidence.
- id: report
shell: bash
run: |
set -euo pipefail
rm -rf styleproof-report
set +e
node "$GITHUB_ACTION_PATH/bin/styleproof-report.mjs" "${{ inputs.baseline-dir }}" "${{ inputs.fresh-dir }}" --out styleproof-report
report_exit_code=$?
set -e
if [ "$report_exit_code" -ne 0 ] && [ "$report_exit_code" -ne 1 ]; then
echo "styleproof-report exited with code $report_exit_code — no trustworthy report was generated" >&2
exit "$report_exit_code"
fi
node <<'NODE'
const fs = require('fs');
const diff = JSON.parse(fs.readFileSync('styleproof-diff.json', 'utf8'));
const generated = JSON.parse(fs.readFileSync('styleproof-report/report.json', 'utf8'));
if (!generated.reportConsistency || typeof generated.reportConsistency.ok !== 'boolean') {
throw new Error('styleproof-report/report.json has no valid reportConsistency verdict');
}
diff.reportConsistency = generated.reportConsistency;
fs.writeFileSync('styleproof-diff.json', `${JSON.stringify(diff, null, 2)}\n`);
NODE

# One machine-readable verdict for every defined diff. Consumers should not
# re-parse styleproof-diff.json or guess whether approval can clear the run.
- id: verdict
Expand Down Expand Up @@ -227,16 +255,6 @@ runs:
GH_TOKEN: ${{ inputs.github-token }}
run: |
set -euo pipefail
rm -rf styleproof-report
set +e
node "$GITHUB_ACTION_PATH/bin/styleproof-report.mjs" "${{ inputs.baseline-dir }}" "${{ inputs.fresh-dir }}" --out styleproof-report
report_exit_code=$?
set -e
if [ "$report_exit_code" -ne 0 ] && [ "$report_exit_code" -ne 1 ]; then
echo "styleproof-report exited with code $report_exit_code — no trustworthy report was generated" >&2
exit "$report_exit_code"
fi

PR='${{ steps.context.outputs.pr-number }}'
REPORT_PATH="pr-${PR:-${{ github.run_id }}}"
REPORT_SHA='${{ steps.context.outputs.head-sha }}'
Expand Down
19 changes: 8 additions & 11 deletions bin/styleproof-report.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -183,16 +183,15 @@ try {
}

const newNote = result.newSurfaces ? ` (+${result.newSurfaces} new surface(s) with no baseline)` : '';
if (result.comparison?.rawOnlyNoReviewable) {
console.log(
`⚠ report consistency: raw certification deltas exist but no reviewable crops — not a clean no-change (fail closed)`,
);
const consistencyFailed = result.reportConsistency?.ok === false;
if (consistencyFailed) {
console.log(`⚠ report consistency: ${result.reportConsistency.reason} — not a clean no-change (fail closed)`);
}
console.log(
result.changedSurfaces === 0
? result.newSurfaces === 0
? result.comparison?.rawOnlyNoReviewable
? '⚠ no reviewable changes — consistency failure written (raw-only derived longhands)'
? consistencyFailed
? '⚠ no presentation changes — report consistency failure written'
: '✓ no changes — empty report written'
: `ℹ ${result.newSurfaces} new surface(s) with no baseline — report written for review`
: `✗ ${result.changedSurfaces} changed surface(s), ${result.totalFindings} finding(s)${newNote}`,
Expand All @@ -201,8 +200,6 @@ console.log(`report: ${result.reportMdPath}`);
if (includeContent && result.contentChanges > 0) {
console.log(`📝 ${result.contentChanges} advisory content change(s) — does not affect the exit code`);
}
// Exit 1 when there is anything to review OR a raw-only consistency failure (never
// exit 0 for "identical" when the certification differ saw deltas).
process.exit(
result.changedSurfaces === 0 && result.newSurfaces === 0 && !result.comparison?.rawOnlyNoReviewable ? 0 : 1,
);
// Exit 1 when there is anything to review OR any report-consistency failure (never
// exit 0 for "identical" when certification evidence was hidden by presentation).
process.exit(result.changedSurfaces === 0 && result.newSurfaces === 0 && !consistencyFailed ? 0 : 1);
Loading
Loading