From b71b0b2683d397ee166db2b22fad649f4a8980e6 Mon Sep 17 00:00:00 2001 From: Amit Patnaik Date: Fri, 17 Apr 2026 16:48:58 +0530 Subject: [PATCH 1/2] Review follow-ups: scrub share snippets, sync guard, a11y polish Code - build_share_snippet now scrubs /Users and /home paths and inline ISO timestamps from the audit body; docstring rewritten to match. - Share snippet renders through rich.Markdown in the CLI, matching the main audit output. - Three new tests: path scrubbing, timestamp scrubbing, and a CliRunner smoke test for `vera audit --share`. CI - New .github/scripts/check_prompt_sync.py: diffs PROMPT.md's fenced block against the const PROMPT literal in docs/index.html. Wire into CI in a follow-up commit. Landing page - rel="noopener" -> rel="noopener noreferrer" on all external links. - Inline SVG favicon (data URI, no asset file). - Visually hidden role="status" aria-live node wired to the copy flow so screen readers announce copy success/failure. Shell - launch/ship.sh now drops `pip install --user` when inside a venv. Docs - examples/README notes audit.share.md is a one-time snapshot. --- .github/scripts/check_prompt_sync.py | 57 ++++++++++++++++++++++++++++ docs/index.html | 27 +++++++++---- examples/README.md | 3 +- launch/ship.sh | 7 +++- tests/test_audit.py | 46 ++++++++++++++++++++++ vera/audit.py | 11 +++++- vera/cli.py | 2 +- 7 files changed, 141 insertions(+), 12 deletions(-) create mode 100755 .github/scripts/check_prompt_sync.py diff --git a/.github/scripts/check_prompt_sync.py b/.github/scripts/check_prompt_sync.py new file mode 100755 index 0000000..3814732 --- /dev/null +++ b/.github/scripts/check_prompt_sync.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Verify docs/index.html embeds the current PROMPT.md fenced block. + +The landing page ships the prompt as an inline JS template literal. +If PROMPT.md is edited without updating docs/index.html, phone visitors +copy a stale prompt. This check fails CI in that case. +""" +from __future__ import annotations +import re +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parents[2] +PROMPT_MD = REPO / "PROMPT.md" +LANDING = REPO / "docs" / "index.html" + +FENCE_RE = re.compile(r"```\n(.*?)\n```", re.DOTALL) +JS_PROMPT_RE = re.compile(r"const PROMPT = `(.*?)`;", re.DOTALL) + + +def extract_prompt_md() -> str: + text = PROMPT_MD.read_text() + match = FENCE_RE.search(text) + if not match: + sys.exit(f"{PROMPT_MD}: no fenced prompt block found") + return match.group(1).strip() + + +def extract_landing_prompt() -> str: + text = LANDING.read_text() + match = JS_PROMPT_RE.search(text) + if not match: + sys.exit(f"{LANDING}: no `const PROMPT = ...` template literal found") + return match.group(1).strip() + + +def main() -> int: + md = extract_prompt_md() + js = extract_landing_prompt() + if md == js: + print("PROMPT.md and docs/index.html are in sync.") + return 0 + print("PROMPT.md and docs/index.html have diverged.", file=sys.stderr) + print("Update docs/index.html `const PROMPT = `...`` to match PROMPT.md.", file=sys.stderr) + print("", file=sys.stderr) + import difflib + diff = difflib.unified_diff( + md.splitlines(), js.splitlines(), + fromfile="PROMPT.md", tofile="docs/index.html", + lineterm="", + ) + print("\n".join(diff), file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/index.html b/docs/index.html index 036ae80..d5f9e5d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -12,6 +12,7 @@ + @@ -144,7 +152,8 @@

AI with clerk discipline.

Copy the Vera prompt
- + +

     
@@ -155,9 +164,9 @@

AI with clerk discipline.

Open your AI app
- Open Claude - Open ChatGPT - Open Gemini + Open Claude + Open ChatGPT + Open Gemini
@@ -170,14 +179,14 @@

AI with clerk discipline.

Paste the prompt as your first message. Ask a factual question. Every claim comes tagged [CITED], [INFERRED], or [ASSUMED]. Type /audit after a few turns for self-critique.
- Keep it on permanently. Paste the prompt into Custom Instructions of a Claude Project, a ChatGPT GPT, or a Gemini Gem. Every new chat in that container inherits it. + Keep it on permanently. Paste the prompt into Custom Instructions of a Claude Project, a ChatGPT GPT, or a Gemini Gem. Every new chat in that container inherits it.
@@ -198,6 +207,7 @@

AI with clerk discipline.

document.getElementById('prompt-preview').textContent = PROMPT; const btn = document.getElementById('copy-btn'); +const status = document.getElementById('copy-status'); const originalText = btn.textContent; let resetTimer = null; @@ -228,13 +238,16 @@

AI with clerk discipline.

if (ok) { btn.textContent = 'Copied'; btn.classList.add('btn-copied'); + status.textContent = 'Prompt copied to clipboard.'; } else { btn.textContent = 'Select and copy manually'; + status.textContent = 'Copy failed. Select the prompt text manually and copy.'; } if (resetTimer) clearTimeout(resetTimer); resetTimer = setTimeout(() => { btn.textContent = originalText; btn.classList.remove('btn-copied'); + status.textContent = ''; }, 2000); }); diff --git a/examples/README.md b/examples/README.md index 62a1dc9..76f3e69 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,7 +7,8 @@ evaluate the tool without installing or spending API credits. - `transcript.md` — a realistic chat transcript Vera would write to `~/vera/memory/`. - `audit.md` — what `vera audit` produces when the auditor reads that transcript. - `audit.share.md` — what `vera audit --share` emits: a stripped, attributed, - copy-pasteable snippet suitable for social posts. + copy-pasteable snippet suitable for social posts. Committed as a one-time + snapshot; regenerate from `audit.md` if the share-snippet format changes. The audit here flags three failure modes worth seeing: sycophancy, unsupported claims, and provenance gaps. diff --git a/launch/ship.sh b/launch/ship.sh index ecb173a..1753824 100755 --- a/launch/ship.sh +++ b/launch/ship.sh @@ -17,7 +17,12 @@ ok "Tools present" bold "" bold "Step 1/6 — Build dist/" -python3 -m pip install --user --quiet --upgrade build twine >/dev/null +# --user is rejected inside a venv; only pass it outside one. +pip_user_flag="--user" +if [[ -n "${VIRTUAL_ENV:-}" ]] || python3 -c "import sys; sys.exit(0 if sys.prefix != sys.base_prefix else 1)"; then + pip_user_flag="" +fi +python3 -m pip install $pip_user_flag --quiet --upgrade build twine >/dev/null rm -rf dist build ./*.egg-info python3 -m build python3 -m twine check dist/* diff --git a/tests/test_audit.py b/tests/test_audit.py index b89766c..64797c4 100644 --- a/tests/test_audit.py +++ b/tests/test_audit.py @@ -46,3 +46,49 @@ def test_build_share_snippet_handles_missing_header(tmp_path): _, snippet = build_share_snippet(audit) assert "## Sycophancy" in snippet assert "Turn 1 flagged." in snippet + + +def test_build_share_snippet_scrubs_home_paths(tmp_path): + audit = tmp_path / "a.md" + audit.write_text( + "# Vera Audit 2026-04-15 14:30\n\n" + "## Provenance gaps\n" + "Primary referenced /Users/alice/projects/payments/README.md verbatim.\n" + ) + _, snippet = build_share_snippet(audit) + assert "/Users/alice" not in snippet + assert "~/…" in snippet + + +def test_build_share_snippet_scrubs_inline_timestamps(tmp_path): + audit = tmp_path / "a.md" + audit.write_text( + "# Vera Audit 2026-04-15 14:30\n\n" + "## Sycophancy\nSeen at 2026-04-15 14:28 in transcript.\n" + ) + _, snippet = build_share_snippet(audit) + assert "2026-04-15 14:28" not in snippet + assert "[timestamp]" in snippet + + +def test_audit_cli_share_flag_writes_sibling(tmp_path, monkeypatch): + from click.testing import CliRunner + from vera import cli as cli_mod + + audit_dir = tmp_path / "audits" + audit_dir.mkdir() + report = audit_dir / "2026-04-15_1430.md" + report.write_text( + "# Vera Audit 2026-04-15 14:30\n\n## Sycophancy\nClean.\n" + ) + + monkeypatch.setattr(cli_mod, "run_audit", lambda provider, mem: report) + monkeypatch.setattr(cli_mod, "detect_provider", lambda: object()) + monkeypatch.setattr(cli_mod, "MEMORY_DIR", tmp_path / "memory") + + runner = CliRunner() + result = runner.invoke(cli_mod.main, ["audit", "--share"]) + assert result.exit_code == 0, result.output + share_path = report.with_suffix(".share.md") + assert share_path.exists() + assert "# What Vera caught in my own chat history" in share_path.read_text() diff --git a/vera/audit.py b/vera/audit.py index de1dbb7..caae41b 100644 --- a/vera/audit.py +++ b/vera/audit.py @@ -3,6 +3,7 @@ claims the user didn't push back on.""" from __future__ import annotations import json +import re from datetime import datetime from pathlib import Path from .config import Provider, AUDIT_DIR, PROVENANCE_LOG @@ -51,18 +52,24 @@ def run_audit(provider: Provider, transcripts_dir: Path) -> Path: return out +_HOME_PATH = re.compile(r"(?:/Users|/home)/[^\s/]+(/[\w.\-/]*)?") +_ISO_TIMESTAMP = re.compile(r"\b\d{4}-\d{2}-\d{2}(?:[ _T]\d{2}[:_]?\d{2}(?::?\d{2})?)?\b") + + def build_share_snippet(audit_path: Path) -> tuple[Path, str]: """Produce an anonymized, copy-pasteable snippet from an audit report. - Strips local paths and timestamps, keeps only the auditor's findings. + Drops the timestamped header, scrubs absolute home paths and ISO + timestamps from the body, and appends the Vera attribution footer. Writes a sibling `.share.md` file and returns (path, snippet_text). """ raw = audit_path.read_text() - # Drop the first-line header with the timestamp; keep the body. lines = raw.splitlines() if lines and lines[0].startswith("# Vera Audit"): lines = lines[1:] body = "\n".join(lines).strip() + body = _HOME_PATH.sub("~/…", body) + body = _ISO_TIMESTAMP.sub("[timestamp]", body) snippet = ( "# What Vera caught in my own chat history\n\n" f"{body}\n" diff --git a/vera/cli.py b/vera/cli.py index 4679236..d6f184c 100644 --- a/vera/cli.py +++ b/vera/cli.py @@ -187,7 +187,7 @@ def audit(share_flag: bool): console.print() console.print(f"[green]Shareable snippet written:[/green] {share_path}") console.print("[dim]Copy below and post wherever. No paths, no timestamps.[/dim]\n") - console.print(snippet) + console.print(Markdown(snippet)) @main.command() def rules(): From e30bcda28e46df9a58ec3dc6d548ade29bcb6579 Mon Sep 17 00:00:00 2001 From: Amit Patnaik Date: Fri, 17 Apr 2026 16:58:16 +0530 Subject: [PATCH 2/2] CI: concurrency group + prompt-sync job - cancel-in-progress on same ref so successive pushes don't queue duplicate runs - new prompt-sync job runs .github/scripts/check_prompt_sync.py, failing CI if PROMPT.md diverges from the embedded literal in docs/index.html --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28dfe2f..9e9ded6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,18 @@ on: pull_request: branches: [main] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: + prompt-sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: docs/index.html embeds current PROMPT.md + run: python3 .github/scripts/check_prompt_sync.py + test: runs-on: ubuntu-latest strategy: