| index 809775e..38a50c8 100644 |
| --- a/tools/prompt-registry.mjs |
| +++ b/tools/prompt-registry.mjs |
| @@ -22,10 +22,13 @@ |
| // your name is the same choice as publishing at all |
| // --personal <file> contributor's personal redaction list: one literal |
| // string (or /regex/) per line, removed as |
| -// ⟦redacted: personal⟧. The policy allows a SMALL |
| -// budget of these for personal reasons; the count |
| -// publishes per session like every other category, |
| -// so the budget is kept honest in the open. |
| +// ⟦redacted: personal⟧. A /regex/ line also matches |
| +// itself literally, so a path-shaped literal is |
| +// never silently reinterpreted as a pattern. The |
| +// policy allows a SMALL budget of these for personal |
| +// reasons; the count publishes per session like |
| +// every other category, so the budget is kept |
| +// honest in the open. |
| // --sessions <file> session-level controls. Sessions are in the |
| // ledger by default (opting in is what contributing |
| // means), but per line a contributor may: |
| @@ -201,10 +204,18 @@ const LOCAL_MD_SENTINELS = ["sync-claude-local.sh", "CLAUDE.local.md (deploy run |
| // Personal redactions run FIRST so a personal phrase wins over (and isn't |
| // split by) the mechanical rules. |
| const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); |
| +// A slash-wrapped line is a /regex/ — but it ALSO gets a literal rule (first, |
| +// so a whole-literal occurrence is redacted in one piece before the pattern |
| +// nibbles at substrings). A literal that merely LOOKS slash-wrapped (a path, |
| +// say) would otherwise be silently reinterpreted as a pattern that can miss |
| +// its own text — and the fail-closed verifier shares these rules, so it can't |
| +// catch what a rule can't match (this leaked a personal string three times |
| +// before the 2026-07-27 accrual caught it by literal grep). |
| const personalRules = argAll("--personal").flatMap((f) => |
| readFileSync(f, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")) |
| - .map((l) => ["personal", l.length > 2 && l.startsWith("/") && l.endsWith("/") |
| - ? new RegExp(l.slice(1, -1), "gi") : new RegExp(escapeRe(l), "gi")])); |
| + .flatMap((l) => l.length > 2 && l.startsWith("/") && l.endsWith("/") |
| + ? [["personal", new RegExp(escapeRe(l), "gi")], ["personal", new RegExp(l.slice(1, -1), "gi")]] |
| + : [["personal", new RegExp(escapeRe(l), "gi")]])); |
| |
| const RULES = [ |
| ...personalRules, |