Skip to content

\u{...} is JavaScript-only, so ten rules never compiled outside the TS engine - #509

Merged
eeee2345 merged 3 commits into
mainfrom
fix/portable-unicode-escapes
Aug 23, 2026
Merged

eeee2345 merged 3 commits into
mainfrom
fix/portable-unicode-escapes

Conversation

@eeee2345

Copy link
Copy Markdown
Contributor

Sixteen regex conditions across ten rules used \u{XXXXX}. That syntax requires
the RegExp u flag and exists only in JavaScript. Python re wants
\U000E0000, Go wants \x{E0000}, and neither accepts the JS form — so every
consumer outside the TypeScript engine dropped those rules silently.

Measured on this tree: of 3,353 regex conditions, 28 fail to compile under Python
re, and sixteen of them are this. Rewriting them as literal characters — the
one spelling all three engines accept — takes the affected rule count from 15 to
5. What is left is variable-width lookbehind (11 conditions across 4 rules) and a
single backreference to a non-capturing group, which are separate problems and
not addressed here.

A literal astral character needs the u flag too: without it JavaScript reads a
surrogate pair, and a class range written with literals is a SyntaxError rather
than a range. So the flag decision changes from "does the pattern contain \u{"
to "does the pattern need u". That test lived in two places — src/engine.ts
and tests/validate-rules.ts, which had its own copy — and both are updated. The
validator copy is why the first attempt at this passed typecheck and build and
still failed rule validation.

Turning u on where it was previously off is stricter about escapes, and it
surfaced one: ATR-2026-00391 carried \" inside a character class, which is a
permitted identity escape without u and invalid with it. The backslash was
never needed.

BEHAVIOUR ON THE JS SIDE IS UNCHANGED, AND CHECKED

Every benign and malicious skill sample evaluated at two event shapes, 896 rows
of sorted rule-id sets, diffed against origin/main: zero differences. The probe
aborts before emitting anything if its control fails, so an empty diff cannot
come from a harness that never ran.

This is the source fix promised in microsoft/PyRIT#1893 — the Python scorer work
needs these rules to compile, and until now six of them dropped entirely.

… could not compile ten rules

Sixteen regex conditions across ten rules used \u{XXXXX}. That syntax needs the
RegExp u flag and exists only in JavaScript: Python re wants \U000E0000, Go wants
\x{E0000}, and neither accepts the JS form. Any consumer outside the TypeScript
engine simply dropped those rules.

Measured on this tree: of 3,353 regex conditions, 28 fail to compile under Python
re. Sixteen of them are this. Rewriting them as literal characters -- the one
spelling all three engines accept -- takes the affected rule count from 15 to 5.
What remains is variable-width lookbehind (11 conditions, 4 rules) and one
backreference, which are different problems.

A literal astral character needs the u flag too, or JS reads a surrogate pair and
a class range becomes a SyntaxError. So the flag decision moves from "does the
pattern contain \u{" to "does the pattern need u", in engine.ts and in
tests/validate-rules.ts, which had its own copy of that test.

Turning u on where it was previously off is stricter about escapes, and it found
one: ATR-2026-00391 had \" inside a character class, which is a valid identity
escape without u and invalid with it. The backslash was never needed.

Behaviour on the JavaScript side is unchanged, and that is checked rather than
assumed: every benign and malicious skill sample evaluated at two event shapes,
896 rows of rule-id sets, diffed against origin/main -- zero differences.
@github-actions

Copy link
Copy Markdown
Contributor

ATR Rule Quality Report

Status: PASS

Validation

  • Passed: 11
  • Failed: 0

Test Cases

  • Passed: 98
  • Failed: 0

Quality Checklist

  • Schema validation
  • Test cases pass

Label: quality-ready

…nothing

The first pass changed the flag test in src/engine.ts and tests/validate-rules.ts
and looked done: typecheck, build, rule validation and RE2 portability all green
locally. CI then failed three gates, and the reason was that four more copies of
the same expression exist.

scripts/eval-generalization.ts was the damaging one. Still testing for the
literal string \u{, it stopped adding u once the patterns became literals, so
every astral class range threw, the throw was caught as no-match, and eight rules
were reported as no longer matching their own authored true positives. The rules
were fine. The gate had gone blind.

There is now one definition — needsUnicodeFlag, exported from src/engine.ts — and
five consumers import it: eval-generalization, fn-mine-llm,
verify-re2-equivalence, lib/visibility-verify, and validate-rules.

fn-mine-llm also instructed the model to emit \u{XXXX} for astral codepoints,
promising the engine would add the flag. Every rule mined under that prompt would
have reintroduced exactly this. It now asks for the literal character and says
why.

Measurement re-run for the eleven touched rules: fp_count identical on all
eleven, only the fingerprints moved. That is the shape a rewrite that preserves
behaviour should have, and it restores the evidence the eligibility gate reads,
so the rules keep the response tiers they had already earned.

Worth recording for the next person: the 896-row corpus A/B in the previous
commit showed zero differences and was not enough. The benign and malicious skill
samples contain none of the unicode these rules exist to match, so the diff was
empty for the wrong reason. gate:generalization caught it because it tests each
rule against the payload its own author wrote.
@eeee2345
eeee2345 requested a review from a team as a code owner August 23, 2026 08:40
@github-actions

Copy link
Copy Markdown
Contributor

ATR Rule Quality Report

Status: PASS

Validation

  • Passed: 11
  • Failed: 0

Test Cases

  • Passed: 98
  • Failed: 0

Quality Checklist

  • Schema validation
  • Test cases pass

Label: quality-ready

Two things CI caught that local gates did not.

The needsUnicodeFlag import in verify-re2-equivalence.ts landed at line 240,
after a later occurrence of the word import, so tsc -p tsconfig.scripts.json
failed on an undefined name while npm run typecheck passed. Different tsconfig,
different file set — running only the one is not running the check.

data/re2-equivalence.json was stale, which is the point of that artifact: making
ten rules portable changed which rules diverge under RE2, and the committed
record still described the old set. Regenerated, and it moved in the intended
direction — 37 lines out, 23 in, four divergent rules remaining, all of them
lookbehind or backreference rather than \u{...}.
@github-actions

Copy link
Copy Markdown
Contributor

ATR Rule Quality Report

Status: PASS

Validation

  • Passed: 11
  • Failed: 0

Test Cases

  • Passed: 98
  • Failed: 0

Quality Checklist

  • Schema validation
  • Test cases pass

Label: quality-ready

@eeee2345
eeee2345 merged commit 35847f4 into main Aug 23, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant