DATMarketing PromptsDATMarketing PromptsDATMarketing Prompts
BotsPromptsSkillsWorkflowsCategoriesTagsPromptmastersTaste
Developers
Login
CC0 2026 DATMarketing Prompts
GitHub
All Tags

Testing Prompts

34 prompts
Feature Flag Rollout Plan Builder (YAML)
Text

Describe a risky feature and get a staged, percentage-based rollout plan as YAML: flag design with fail-safe defaults, targeting rules, go and no-go thresholds for every stage, a five-minute kill switch runbook, expand and contract data steps, and a flag removal plan.

1role: >
2 You are a senior release engineer who has shipped risky features to large
3 user bases behind feature flags. You plan progressive rollouts that limit
4 the blast radius, define clear go and no-go signals before anyone flips a
5 switch, and make sure every flag has an owner and a removal date so flags
6 do not turn into permanent technical debt.
7
8task: >
9 Create a complete, staged rollout plan for the feature described below,
10 including flag design, targeting, stage gates with metrics, a kill switch
...+79 more lines
Best PracticesDevOpsPlanning+2
F@f
0
Regex Pattern Explainer and Tester
Skill

Explains Python regular expressions token by token, tests them on must-match and must-not-match samples with spans and named groups, and flags risky patterns: nested quantifiers that backtrack catastrophically, [A-z], unescaped dots, empty matches, and missing anchors, then proposes a safer rewrite. Use when a user asks "what does this regex do?", "why doesn't it match?", or "is this regex safe?". Includes a tested stdlib Python checker.

---
name: regex-pattern-explainer-and-tester
description: Explains Python regular expressions token by token, tests them on must-match and must-not-match samples with spans and named groups, and flags risky patterns: nested quantifiers that backtrack catastrophically, [A-z], unescaped dots, empty matches, and missing anchors, then proposes a safer rewrite. Use when a user asks "what does this regex do?", "why doesn't it match?", or "is this regex safe?". Includes a tested stdlib Python checker.
---

# Regex Pattern Explainer and Tester

You help people understand, fix, and trust regular expressions. You explain a pattern in plain words, prove what it does with real test cases, and find the traps that make regexes fail in production: inputs that should not pass, inputs that should pass but do not, and patterns that hang on long input. You return a corrected pattern with a test table, not just an opinion.

## Files in this skill

- `scripts/regex_check.py` - explains a pattern from Python's own regex parser, runs sample inputs with "+" (must match) and "-" (must not match) expectations, and reports HIGH, WARN, and INFO risks; `--probe` times the pattern on generated worst-case input in a subprocess (Python 3 standard library only)
- `references/regex-pitfalls.md` - every risk the checker reports, why it matters, and the safe pattern to use instead
- `references/flavor-differences.md` - how Python `re` differs from JavaScript, PCRE, Go RE2, .NET, Java, and POSIX grep, so a fix works where the user runs it
- `templates/regex-review.md` - the review report to return
- `examples/example-email-validation-review.md` - a worked review of an email check that hangs on long input

## Workflow

### 1. Collect the facts
Ask, or assume and say so:
- The pattern exactly as written in the code (raw string or escaped string literal) and the language or tool that runs it.
- What it must accept and what it must reject: 4 to 10 real examples of each, including edge cases (empty input, spaces, Unicode, very long input).
- How it is used: validate a whole value (fullmatch), find the first match (search), extract all matches (findall), or replace.
- Where the input comes from. Untrusted input (web forms, logs, API payloads) makes catastrophic backtracking a security issue.

### 2. Run the checker
```bash
python3 scripts/regex_check.py 'PATTERN' -t 'sample one' -t 'sample two'
python3 scripts/regex_check.py 'PATTERN' --cases cases.txt --mode fullmatch --flags a
python3 scripts/regex_check.py 'PATTERN' --probe --no-explain         # confirm a backtracking risk
python3 scripts/regex_check.py 'PATTERN' --cases - --json < cases.txt
```
A cases file has one sample per line: `+ text` must match, `- text` must not match, `# ...` is a comment. Always quote the pattern in single quotes in the shell. The checker uses Python's `re`; for other flavors, read `references/flavor-differences.md` and translate the result. If you cannot run the script, explain the pattern by hand with the same structure and apply `references/regex-pitfalls.md`.

### 3. Explain, then fix
1. Explain the pattern top to bottom in plain words, one line per token or group, using the checker's explanation as the base.
2. Show the test table: each sample, expected result, actual result, and the matched text or groups.
3. Fix in this order: patterns that can hang (HIGH), wrong accepts and wrong rejects (failed expectations), then clarity and the INFO notes.
4. Prefer simple, readable rewrites: one repeat instead of nested ones, negated classes like `[^,]*` instead of `.*`, explicit anchors `\A...\Z` or `fullmatch`, and `re.VERBOSE` with comments for long patterns.
5. Rerun the checker on the corrected pattern with the same cases plus any new edge cases until every expectation passes and there is no HIGH risk. Run `--probe` on the fix if the original had a HIGH risk.

### 4. Deliver
Fill in `templates/regex-review.md`: the plain-language explanation, the test table before and after, the risks found, the corrected pattern in the user's language with correct escaping, and a short list of test cases to keep in their test suite. Follow the style of `examples/example-email-validation-review.md`.

## Rules
- Never claim a pattern is correct without showing it on test cases; add the edge cases the user forgot.
- Do not over-validate: for emails, URLs, and names, recommend a simple shape check plus a real verification step (sending an email, parsing the URL) instead of a giant pattern.
- Keep the user's flavor: do not suggest lookbehind, possessive quantifiers, or atomic groups where their engine does not support them.
- Escape correctly for the target language (raw strings in Python, double backslashes in Java and C# strings, slashes in JavaScript literals).
- Treat catastrophic backtracking on untrusted input as a security bug (ReDoS) and say so plainly; suggest input length limits as a second defense.
FILE:references/regex-pitfalls.md
# Regex pitfalls checked by the script

Each rule: what is detected, why it matters, and the pattern to use instead. Levels: HIGH can hang or be exploited, WARN usually accepts or rejects the wrong input, INFO is a habit worth fixing.

## HIGH (catastrophic backtracking, ReDoS)

### nested-quantifier
Detected: a repeat inside another repeat where nothing else in the outer repeat is required, such as `(a+)+`, `(\w+\s?)+`, `([a-z0-9]+\.?)+`, `(\w+|\d+)+`.
Why: on input that almost matches (a long run of letters followed by a char the pattern rejects), the engine tries every way to split the run between the inner and outer repeat: 2^n ways for n chars. 30 chars can take minutes. On a web form or API this is a denial-of-service bug.
Use instead: one repeat that describes the whole run, with the separator required.
```
(\w+\s?)+$          ->  \w+(?:\s\w+)*$
([a-z0-9]+\.?)+@    ->  [a-z0-9]+(?:\.[a-z0-9]+)*@
```
Python 3.11+ also supports atomic groups `(?>...)` and possessive quantifiers `a++`, which never give back what they matched.

### ambiguous-repeat
Detected: a repeat whose body has variable length and whose required parts can also be eaten by the variable part, such as `(x+x+)+`, `(aa|a)+`, `(foo|f\w+)+`.
Why: same as above, the iterations can be cut in many ways. Growth may be slower (Fibonacci instead of 2^n) but is still exponential.
Use instead: make each iteration start with a char nothing else in the body can match, or merge the parts into one class.

Confirm both with `--probe`: the script runs the pattern on `c * n + tail` for n = 12 to 28 in a subprocess with a 2 second limit and reports how fast the time grows. A growth of more than 3x per 4 extra chars, or a timeout, confirms the risk.

## WARN

### nested-overlap
A repeat whose body has a separator that the inner part also accepts, such as `([\w.]+\.)+`. Make the inner class exclude the separator: `(?:[\w-]+\.)+`.

### adjacent-overlap
Two unbounded single-char repeats in a row that accept the same chars, such as `\d+\d+` or `.*.*`. The split point is ambiguous and a failing match backtracks polynomially (n^2, n^3). Keep one of them or make the boundary explicit.

### range-A-z
`[A-z]` covers ASCII 65 to 122, which includes `[ \ ] ^ _` and the backtick. Use `[A-Za-z]`, or `[^\W\d_]` for any Unicode letter.

### unescaped-dot
`.` between letters or before a word such as `com` or `json` matches any char, so `example-com` passes a check for `example.com`. Write `\.`.

### matches-empty
The pattern accepts the empty string. A validation then accepts blank input, and `findall` returns empty strings between real matches. Use `+` instead of `*` where at least one char is required.

## INFO

### not-anchored
`re.search(r"\d+", "abc123xyz")` succeeds. For validating a whole value, use `re.fullmatch` or anchor with `\A ... \Z`.

### dollar-newline
`$` matches at the end of the string and also just before a final newline, so `^abc$` accepts `"abc\n"` with `re.search` or `re.match`. Use `\Z` or `re.fullmatch` for strict checks.

### unicode-classes
In Python 3, `\d` matches any Unicode digit (for example Arabic-Indic digits) and `\w` matches letters in every script. Good for names and free text; for IDs, card numbers, and codes use `re.ASCII` or `[0-9]`.

### greedy-dotstar and many-dotstar
`.*` takes everything and then gives back one char at a time. Three or more `.*` in one pattern make failures slow on long lines. Describe each field with a negated class: `"[^"]*"` for a quoted string, `[^,]*` for a CSV field.

## Not detected by the script (check by hand)

- Wrong escaping in the host language (a Python string without `r""`, a Java string with single backslashes).
- Business rules a regex cannot express well: valid dates (February 30), checksums (IBAN, card numbers), real email deliverability.
- Locale issues: case-insensitive matching of Turkish dotted and dotless i, German sharp s.
- Performance of huge alternations (thousands of words): use a set lookup or a trie instead.
FILE:references/flavor-differences.md
# Regex flavor differences that break fixes

The checker runs Python `re`. Before handing a pattern to another engine, check this table.

| Feature | Python `re` | JavaScript | PCRE / PHP | Go (RE2) | .NET | Java | POSIX grep -E |
|---|---|---|---|---|---|---|---|
| Lookahead `(?=...)` | yes | yes | yes | no | yes | yes | no |
| Lookbehind `(?<=...)` | fixed width only | yes (ES2018) | fixed or bounded | no | any width | bounded | no |
| Named group | `(?P<name>...)` only | `(?<name>...)` | both | `(?P<name>...)` (also `(?<name>...)` in Go 1.22+) | `(?<name>...)` | `(?<name>...)` | no |
| Atomic group `(?>...)` | 3.11+ | no | yes | no | yes | yes | no |
| Possessive `a++` | 3.11+ | no | yes | no | no | yes | no |
| Backreference `\1` | yes | yes | yes | no | yes | yes | yes (basic) |
| `\d` matches non-ASCII digits | yes (unless re.ASCII) | no | no (unless UCP) | no | yes | no (unless UNICODE_CHARACTER_CLASS) | locale |
| `$` before final newline | yes | no (without m flag) | yes | no | yes | yes | n/a (line based) |
| Inline flags `(?i)` | yes, at start or scoped `(?i:...)` | flags after the literal (`/i`); scoped `(?i:...)` only in the newest engines | yes | yes | yes | yes | no |
| Catastrophic backtracking | possible | possible | possible (has limits) | impossible (linear time) | possible (timeout option) | possible | engine dependent |
| Full match helper | `re.fullmatch` | anchor with `^...$` | anchor | anchor | anchor | `String.matches()` | `-x` flag |

## Translation notes

- JavaScript: write the literal as `/pattern/flags`; escape `/` inside it. Use the `u` flag for Unicode and `v` for set operations in newer engines. `\Z` does not exist: use `$` without the `m` flag.
- Go RE2: no lookarounds or backreferences, but every pattern runs in linear time, so ReDoS is not a concern there. Rewrite lookarounds as a second check in code.
- Java and C#: every backslash doubles in a normal string literal (`"\\d+"`); C# verbatim strings `@"\d+"` and Java text blocks help.
- .NET: set `RegexOptions.NonBacktracking` (.NET 7+) or a match timeout for untrusted input.
- SQL: PostgreSQL `~` uses Tcl-style advanced regexes (lookahead, lookbehind since 9.6, no named groups); MySQL 8 uses ICU. Test in the database itself.
- Shell: `grep -E` has no `\d` on many systems; use `[0-9]`. `grep -P` gives PCRE where available.

## Safe defaults across flavors

- `[0-9]` instead of `\d`, `[A-Za-z]` instead of `\w` when the format is ASCII by definition.
- Anchors `^` and `$` without multiline mode, plus a separate length check (for example at most 254 chars for an email).
- No nested quantifiers on untrusted input, whatever the engine.
FILE:templates/regex-review.md
# Regex review: purpose

**Pattern (original):** `original_pattern`
**Engine:** language_and_engine | **Used as:** validate whole value / find first / find all / replace
**Checker:** HIGH high / WARN warn / INFO info before; expectations pass_before/total pass

## What it does, in plain words

1. (one line per token or group, top to bottom)

## Test results

| # | Input | Expected | Before | After | Notes |
|---|---|---|---|---|---|
| 1 | | match / no match | | | |

## Risks found

| Level | Rule | What it means here | Fix |
|---|---|---|---|
| HIGH | | | |

Probe (if run): input `probe_input`, probe_result

## Corrected pattern

```language
(the corrected pattern, escaped for the user's language, with a VERBOSE or commented version if it is longer than about 40 chars)
```

Why each change: (one line per change)

## Keep these tests

```
+ (must match)
- (must not match)
```

## Notes

- Limits of a regex for this job and the extra check to do in code (length limit, parsing, verification step).
- Flavor caveats if the pattern will run outside Python.
FILE:examples/example-email-validation-review.md
# Example: review of a signup form email check that hangs

## User request

"Our signup API sometimes times out and the profiler points at this line. Can you explain the regex and fix it? It runs in Python 3.12 on the email field of a public form."

```python
EMAIL = re.compile(r"^([a-zA-Z0-9]+\.?)+@[A-z]+.com$")
```

## Cases used (cases.txt)

The user's 5 cases plus 3 edge cases added during the review (a dash instead of the dot, an underscore in the domain, and 30 letters with no `@`, the shape a bot sent):

```
+ ann@example.com
+ first.last@mail.example.co
- bob@@example.com
- no-at-sign.example.com
- trailing-dot@example.
- ann@example-com
- ann@exa_mple.com
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!
```

## Checker run (original)

```
$ python3 scripts/regex_check.py '^([a-zA-Z0-9]+\.?)+@[A-z]+.com$' --cases cases.txt --no-explain --probe
Samples (search):
  PASS     + 'ann@example.com'  -> [0:15] 'ann@example.com' 1='ann'
  FAIL     + 'first.last@mail.example.co'  -> expected a match
  PASS     - 'bob@@example.com'
  PASS     - 'no-at-sign.example.com'
  PASS     - 'trailing-dot@example.'
  FAIL     - 'ann@example-com'  -> [0:15] 'ann@example-com' 1='ann'  (expected no match)
  FAIL     - 'ann@exa_mple.com'  -> [0:16] 'ann@exa_mple.com' 1='ann'  (expected no match)
  TIMEOUT  - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'  -> still running after 2 s (catastrophic backtracking)

Risks and suggestions:
  HIGH nested-quantifier       a quantifier sits inside another quantifier and nothing else in the outer repeat is required ...
       probe 'a' * n + '!': n=12: 0.0002 s, n=16: 0.0031 s, n=20: 0.0593 s, n=24: 0.8419 s, n=28: timeout -> TIMEOUT at length 29: exponential backtracking confirmed
  WARN range-A-z               [A-z] also matches [ \ ] ^ _ and the backtick; use [A-Za-z]
  WARN unescaped-dot           '.' before 'com' matches any char (so a '-' or 'x' passes there too); write \. for a literal dot
  INFO dollar-newline          $ also matches before a trailing newline ...

Summary: HIGH 1  WARN 2  INFO 1 | expectations 4/8 pass | 1 sample(s) timed out
```

## Review (filled template, shortened)

**Checker:** HIGH 1 / WARN 2 / INFO 1 before; expectations 4/8 pass (one timeout). After: HIGH 0 / WARN 0, expectations 8/8 pass.

What it does: start of string; one or more runs of letters or digits, each optionally followed by a dot; an `@`; one or more chars from A to z (including `[ \ ] ^ _` and the backtick); any single char; the text `com`; end of string or before a final newline.

| # | Input | Expected | Before | After | Notes |
|---|---|---|---|---|---|
| 1 | ann@example.com | match | match | match | |
| 2 | first.last@mail.example.co | match | no match | match | only `.com` and one domain label were allowed |
| 3 | bob@@example.com | no match | no match | no match | |
| 4 | no-at-sign.example.com | no match | no match | no match | |
| 5 | trailing-dot@example. | no match | no match | no match | |
| 6 | ann@example-com | no match | match | no match | added: the unescaped dot accepted `-` |
| 7 | ann@exa_mple.com | no match | match | no match | added: `[A-z]` accepted `_` |
| 8 | 30 x "a" then "!" | no match, fast | timeout after 2 s | no match, instant | added: the production hang |

Risks: the `([a-zA-Z0-9]+\.?)+` group lets a run of letters be split in 2^n ways when the `@` is missing, so a 29-char junk input from a bot blocked a worker for more than 2 seconds; this is a ReDoS bug on a public endpoint.

Corrected pattern:

```python
EMAIL = re.compile(r"""
    [A-Za-z0-9._%+-]+          # local part: one run, dots allowed but not repeated in a nested group
    @
    (?:[A-Za-z0-9-]+\.)+       # one or more domain labels, each ending in a literal dot
    [A-Za-z]{2,}               # top-level domain
""", re.VERBOSE | re.ASCII)

def looks_like_email(value: str) -> bool:
    return len(value) <= 254 and EMAIL.fullmatch(value) is not None
```

Why each change: one flat repeat for the local part removes the nested quantifier; `\.` makes the dot literal; `[A-Za-z]` replaces `[A-z]`; any top-level domain is accepted instead of only `com`; `fullmatch` replaces `^...$`; a 254-char limit is a second defense; `re.ASCII` keeps `[...]` classes predictable.

Recheck: `python3 scripts/regex_check.py '[A-Za-z0-9._%+-]+@(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}' --cases cases.txt --mode fullmatch --probe` reports no HIGH or WARN risks (the nested group is reported as INFO nested-separated, a safe shape) and 8/8 expectations pass.

Keep these tests: all 8 rows above, plus a 300-char input to check the length limit. Note that a regex only checks the shape; confirm the address by sending a verification email.
FILE:scripts/regex_check.py
#!/usr/bin/env python3
"""Explain a Python regular expression, test it on sample inputs, and warn about risky patterns.

Usage:
  python3 regex_check.py PATTERN [options]
  python3 regex_check.py PATTERN -t "text one" -t "text two"
  python3 regex_check.py PATTERN --cases cases.txt --mode fullmatch
  python3 regex_check.py PATTERN --cases - < cases.txt        (read cases from stdin)

Options:
  -t, --text TEXT     a sample input (repeatable)
  --cases FILE        one sample per line; "+ text" must match, "- text" must not match,
                      "# ..." is a comment, other lines are shown without an expectation
  --mode MODE         search (default), match, fullmatch, or findall
  --flags LETTERS     any of i m s x a (IGNORECASE, MULTILINE, DOTALL, VERBOSE, ASCII)
  --probe             time the pattern on generated worst-case inputs (subprocess, 2 s limit)
  --no-explain        skip the token-by-token explanation
  --json              print the result as JSON
  --fail-on LEVEL     exit 1 if any risk is at least LEVEL: high (default), warn, info, never
  --timeout S         stop matching one sample after S seconds and report TIMEOUT (default 2)

Risks: HIGH = nested quantifiers that can backtrack exponentially;
WARN = overlapping alternatives or adjacent quantifiers, [A-z], unescaped dots, empty matches;
INFO = anchoring, $ versus \\Z, Unicode \\d and \\w, greedy .*, flags that look missing.
Exit codes: 0 ok, 1 failed expectations or a risk at or above --fail-on, 2 usage or pattern error.
Python 3 standard library only (uses the re module's own parser).
"""
import json
import re
import signal
import string
import subprocess
import sys

try:  # Python 3.11+
    import re._parser as sre_parse
except ImportError:  # pragma: no cover - older Python
    import sre_parse  # type: ignore

LEVELS = {"info": 0, "warn": 1, "high": 2}
FLAG_LETTERS = {"i": re.IGNORECASE, "m": re.MULTILINE, "s": re.DOTALL, "x": re.VERBOSE, "a": re.ASCII}
FLAG_NAMES = [(re.IGNORECASE, "IGNORECASE"), (re.MULTILINE, "MULTILINE"), (re.DOTALL, "DOTALL"),
              (re.VERBOSE, "VERBOSE"), (re.ASCII, "ASCII")]
ALPHABET = string.ascii_letters + string.digits + string.punctuation + " \t\n" + "\u00e9\u0663"
CATEGORY_RE = {"CATEGORY_DIGIT": r"\d", "CATEGORY_NOT_DIGIT": r"\D", "CATEGORY_SPACE": r"\s",
               "CATEGORY_NOT_SPACE": r"\S", "CATEGORY_WORD": r"\w", "CATEGORY_NOT_WORD": r"\W"}
CATEGORY_TEXT = {"CATEGORY_DIGIT": "a digit", "CATEGORY_NOT_DIGIT": "a non-digit",
                 "CATEGORY_SPACE": "whitespace", "CATEGORY_NOT_SPACE": "a non-whitespace char",
                 "CATEGORY_WORD": "a word char (letter, digit, _)", "CATEGORY_NOT_WORD": "a non-word char"}
AT_TEXT = {"AT_BEGINNING": "^ start of string (start of each line with MULTILINE)",
           "AT_BEGINNING_STRING": "\\A start of string only",
           "AT_END": "$ end of string or before a final newline (end of each line with MULTILINE)",
           "AT_END_STRING": "\\Z end of string only",
           "AT_BOUNDARY": "\\b word boundary", "AT_NON_BOUNDARY": "\\B not a word boundary"}
MAXREPEAT = sre_parse.MAXREPEAT


class UsageError(Exception):
    pass


class MatchTimeout(Exception):
    pass


def with_timeout(seconds, fn, *args):
    """Run fn with a wall-clock limit (SIGALRM on Unix; no limit elsewhere)."""
    if not hasattr(signal, "setitimer") or seconds <= 0:
        return fn(*args)
    def handler(_sig, _frame):
        raise MatchTimeout()
    old = signal.signal(signal.SIGALRM, handler)
    signal.setitimer(signal.ITIMER_REAL, seconds)
    try:
        return fn(*args)
    finally:
        signal.setitimer(signal.ITIMER_REAL, 0)
        signal.signal(signal.SIGALRM, old)


def op_name(op):
    return str(op)


def show_char(code):
    ch = chr(code)
    named = {"\n": "\\n", "\t": "\\t", "\r": "\\r", " ": "space"}
    return named.get(ch, repr(ch) if ch.isprintable() else f"U+{code:04X}")


# ---------------------------------------------------------------- char sets

def class_accepts(items, flags):
    """Set of ALPHABET chars accepted by an IN [...] item list."""
    negate = bool(items) and op_name(items[0][0]) == "NEGATE"
    acc = set()
    for op, av in items:
        name = op_name(op)
        if name == "LITERAL":
            acc.add(chr(av))
        elif name == "RANGE":
            acc |= {c for c in ALPHABET if av[0] <= ord(c) <= av[1]}
        elif name == "CATEGORY":
            rx = re.compile(CATEGORY_RE.get(op_name(av), "."), flags & re.ASCII)
            acc |= {c for c in ALPHABET if rx.match(c)}
    if flags & re.IGNORECASE:
        acc |= {c.swapcase() for c in acc}
    return set(ALPHABET) - acc if negate else acc


def first_info(seq, flags):
    """Return (first_chars, can_be_empty) for a token sequence."""
    first, empty = set(), True
    for op, av in seq:
        f, e = token_first(op, av, flags)
        first |= f
        if not e:
            empty = False
            break
    return first, empty


def token_first(op, av, flags):
    name = op_name(op)
    if name == "LITERAL":
        c = chr(av)
        return ({c, c.swapcase()} if flags & re.IGNORECASE else {c}), False
    if name == "NOT_LITERAL":
        return set(ALPHABET) - {chr(av)}, False
    if name == "IN":
        return class_accepts(av, flags), False
    if name == "ANY":
        return (set(ALPHABET) if flags & re.DOTALL else set(ALPHABET) - {"\n"}), False
    if name in ("MAX_REPEAT", "MIN_REPEAT", "POSSESSIVE_REPEAT"):
        lo, _hi, sub = av
        f, e = first_info(sub, flags)
        return f, e or lo == 0
    if name == "SUBPATTERN":
        return first_info(av[3], flags_for(av, flags))
    if name == "ATOMIC_GROUP":
        return first_info(av, flags)
    if name == "BRANCH":
        first, empty = set(), False
        for b in av[1]:
            f, e = first_info(b, flags)
            first |= f
            empty = empty or e
        return first, empty
    if name == "GROUPREF_EXISTS":
        _g, yes, no = av
        f1, e1 = first_info(yes, flags)
        f2, e2 = first_info(no or [], flags)
        return f1 | f2, e1 or e2
    if name == "GROUPREF":
        return set(ALPHABET), True
    return set(), True  # AT, ASSERT, ASSERT_NOT: zero width


def flags_for(av, flags):
    add, delete = av[1], av[2]
    return (flags | add) & ~delete


# ---------------------------------------------------------------- explanation

def quant_text(lo, hi, name):
    mode = {"MAX_REPEAT": "greedy", "MIN_REPEAT": "lazy", "POSSESSIVE_REPEAT": "possessive"}[name]
    if hi == MAXREPEAT:
        base = {0: "zero or more", 1: "one or more"}.get(lo, f"{lo} or more")
    elif lo == hi:
        return f"exactly {lo}"
    elif (lo, hi) == (0, 1):
        base = "optional (zero or one)"
    else:
        base = f"{lo} to {hi}"
    return f"{base} ({mode})"


def class_text(items):
    negate = bool(items) and op_name(items[0][0]) == "NEGATE"
    parts = []
    for op, av in items:
        name = op_name(op)
        if name == "LITERAL":
            parts.append(show_char(av))
        elif name == "RANGE":
            parts.append(f"{show_char(av[0])}-{show_char(av[1])}")
        elif name == "CATEGORY":
            parts.append(CATEGORY_TEXT.get(op_name(av), op_name(av)))
    return ("any char except " if negate else "one of: ") + ", ".join(parts)


def explain(seq, group_names, flags, depth=0, out=None):
    out = [] if out is None else out
    pad = "  " * depth
    i = 0
    while i < len(seq):
        op, av = seq[i]
        name = op_name(op)
        if name == "LITERAL":  # merge runs of literals
            j = i
            while j < len(seq) and op_name(seq[j][0]) == "LITERAL":
                j += 1
            text = "".join(chr(a) for _, a in seq[i:j])
            out.append(f"{pad}the text {text!r}" if j - i > 1 else f"{pad}the char {show_char(av)}")
            i = j
            continue
        if name == "NOT_LITERAL":
            out.append(f"{pad}any char except {show_char(av)}")
        elif name == "IN":
            out.append(f"{pad}{class_text(av)}")
        elif name == "ANY":
            out.append(f"{pad}any char" + ("" if flags & re.DOTALL else " except newline"))
        elif name == "AT":
            out.append(f"{pad}{AT_TEXT.get(op_name(av), op_name(av))}")
        elif name in ("MAX_REPEAT", "MIN_REPEAT", "POSSESSIVE_REPEAT"):
            lo, hi, sub = av
            out.append(f"{pad}{quant_text(lo, hi, name)} of:")
            explain(sub, group_names, flags, depth + 1, out)
        elif name == "SUBPATTERN":
            g = av[0]
            if g is None:
                out.append(f"{pad}non-capturing group:")
            else:
                label = f' "{group_names[g]}"' if g in group_names else ""
                out.append(f"{pad}capturing group {g}{label}:")
            explain(av[3], group_names, flags_for(av, flags), depth + 1, out)
        elif name == "ATOMIC_GROUP":
            out.append(f"{pad}atomic group (no backtracking into it):")
            explain(av, group_names, flags, depth + 1, out)
        elif name == "BRANCH":
            out.append(f"{pad}either:")
            for k, b in enumerate(av[1]):
                out.append(f"{pad}  option {k + 1}:")
                explain(b, group_names, flags, depth + 2, out)
        elif name in ("ASSERT", "ASSERT_NOT"):
            direction, sub = av
            kind = ("lookbehind" if direction < 0 else "lookahead")
            neg = "negative " if name == "ASSERT_NOT" else ""
            out.append(f"{pad}{neg}{kind} (checks without consuming):")
            explain(sub, group_names, flags, depth + 1, out)
        elif name == "GROUPREF":
            out.append(f"{pad}the same text as group {av} matched")
        elif name == "GROUPREF_EXISTS":
            out.append(f"{pad}if group {av[0]} matched:")
            explain(av[1], group_names, flags, depth + 1, out)
            if av[2]:
                out.append(f"{pad}otherwise:")
                explain(av[2], group_names, flags, depth + 1, out)
        else:
            out.append(f"{pad}{name}")
        i += 1
    return out


# ---------------------------------------------------------------- risk analysis

def walk_repeats(seq, flags, path=""):
    """Yield (op, av, flags) for every repeat node, outside lookarounds too."""
    for op, av in seq:
        name = op_name(op)
        if name in ("MAX_REPEAT", "MIN_REPEAT", "POSSESSIVE_REPEAT"):
            yield op, av, flags
            yield from walk_repeats(av[2], flags)
        elif name == "SUBPATTERN":
            yield from walk_repeats(av[3], flags_for(av, flags))
        elif name == "ATOMIC_GROUP":
            yield from walk_repeats(av, flags)
        elif name == "BRANCH":
            for b in av[1]:
                yield from walk_repeats(b, flags)
        elif name in ("ASSERT", "ASSERT_NOT"):
            yield from walk_repeats(av[1], flags)


def flatten(seq):
    """Expand groups so the body of a repeat can be read as one token list."""
    out = []
    for op, av in seq:
        name = op_name(op)
        if name == "SUBPATTERN" and av[1] == 0 and av[2] == 0:
            out.extend(flatten(av[3]))
        else:
            out.append((op, av))
    return out


def token_width(tok, parsed):
    return sre_parse.SubPattern(parsed.state, [tok]).getwidth()


def all_chars(seq, flags):
    """Every ALPHABET char a token sequence can consume anywhere (not only first)."""
    acc = set()
    for op, av in seq:
        name = op_name(op)
        if name in ("LITERAL", "NOT_LITERAL", "IN", "ANY"):
            acc |= token_first(op, av, flags)[0]
        elif name in ("MAX_REPEAT", "MIN_REPEAT", "POSSESSIVE_REPEAT"):
            acc |= all_chars(av[2], flags)
        elif name == "SUBPATTERN":
            acc |= all_chars(av[3], flags_for(av, flags))
        elif name == "ATOMIC_GROUP":
            acc |= all_chars(av, flags)
        elif name == "BRANCH":
            for b in av[1]:
                acc |= all_chars(b, flags)
        elif name == "GROUPREF_EXISTS":
            acc |= all_chars(av[1], flags) | all_chars(av[2] or [], flags)
        elif name == "GROUPREF":
            acc |= set(ALPHABET)
    return acc


def has_unbounded(tok):
    op, av = tok
    name = op_name(op)
    if name in ("MAX_REPEAT", "MIN_REPEAT"):
        return av[1] == MAXREPEAT or has_unbounded_seq(av[2])
    if name == "SUBPATTERN":
        return has_unbounded_seq(av[3])
    if name == "BRANCH":
        return any(has_unbounded_seq(b) for b in av[1])
    return False


def has_unbounded_seq(seq):
    return any(has_unbounded(t) for t in seq)


def sample_char(chars):
    for pool in (string.ascii_lowercase, string.digits, string.ascii_uppercase, ALPHABET):
        for c in pool:
            if c in chars:
                return c
    return None


def analyze(parsed, flags, pattern, modes_seen):
    risks = []

    def add(level, rule, msg, probe=None):
        risks.append({"level": level, "rule": rule, "message": msg, **({"probe": probe} if probe else {})})

    # nested quantifiers: a repeat whose body can match the same text in more than one way
    for op, av, fl in walk_repeats(list(parsed), flags):
        if op_name(op) == "POSSESSIVE_REPEAT" or av[1] <= 1:
            continue
        body = flatten(av[2])
        widths = [token_width(t, parsed) for t in body]
        if sum(w[0] for w in widths) == sum(w[1] for w in widths):
            continue  # fixed-width body such as (\d{4}-)+ cannot be split two ways
        variable = [t for t, w in zip(body, widths) if w[0] != w[1]]
        fixed = [t for t, w in zip(body, widths) if w[0] == w[1] and w[0] > 0]
        var_chars = set().union(*(all_chars([t], fl) for t in variable))
        has_inner_loop = any(has_unbounded(t) for t in variable)
        overlap = set().union(*(all_chars([t], fl) for t in fixed)) & var_chars if fixed else set()
        sample = sample_char(overlap) or sample_char(var_chars)
        if not fixed and has_inner_loop:
            add("high", "nested-quantifier",
                "a quantifier sits inside another quantifier and nothing else in the outer repeat is "
                "required (the classic (a+)+ or (\\w+\\s?)+ shape), so one string can be split in "
                "exponentially many ways; rewrite as a single repeat, make the separator required, or use an "
                "atomic group or possessive quantifier (Python 3.11+)", probe=sample)
        elif any(not (all_chars([t], fl) & var_chars) for t in fixed):
            if has_inner_loop:
                add("info", "nested-separated",
                    "nested quantifier with a required separator that the inner repeat cannot match "
                    "(like ([\\w-]+\\.)+); this shape is safe")
        elif all(all_chars([t], fl) <= var_chars for t in fixed):
            add("high", "ambiguous-repeat",
                "the body of a repeat has variable length and every required part can also be eaten by "
                "the variable part (for example (x+x+)+, (aa|a)+, or (\\w+|\\d+)+), so the iterations "
                "can be split in exponentially many ways; merge the parts into one class or make each "
                "iteration start with a char the rest cannot match", probe=sample)
        else:
            add("warn", "nested-overlap",
                "a repeat with a variable-length body whose separator is also accepted by the inner part "
                "(for example ([\\w.]+\\.)+): long non-matching input can slow down sharply; make the inner "
                "class exclude the separator", probe=sample)

    # adjacent overlapping unbounded repeats in any sequence
    seen_pairs = set()

    def single_char_repeat(op, av):
        return (op_name(op) in ("MAX_REPEAT", "MIN_REPEAT") and av[1] == MAXREPEAT and len(av[2]) == 1
                and op_name(av[2][0][0]) in ("LITERAL", "NOT_LITERAL", "IN", "ANY"))

    def scan_adjacent(seq, fl):
        toks = [t for t in flatten(seq) if op_name(t[0]) not in ("AT", "ASSERT", "ASSERT_NOT")]
        for (a_op, a_av), (b_op, b_av) in zip(toks, toks[1:]):
            if single_char_repeat(a_op, a_av) and single_char_repeat(b_op, b_av):
                if (id(a_av), id(b_av)) in seen_pairs:
                    continue
                seen_pairs.add((id(a_av), id(b_av)))
                fa, _ = first_info(a_av[2], fl)
                fb, _ = first_info(b_av[2], fl)
                if fa & fb:
                    add("warn", "adjacent-overlap",
                        "two unbounded quantifiers in a row accept the same chars (for example \\d+\\d+ or "
                        ".*.*), so the split between them is ambiguous and failures backtrack polynomially; "
                        "keep one of them")
        for op, av in seq:
            name = op_name(op)
            if name == "SUBPATTERN":
                scan_adjacent(av[3], flags_for(av, fl))
            elif name in ("MAX_REPEAT", "MIN_REPEAT", "POSSESSIVE_REPEAT"):
                scan_adjacent(av[2], fl)
            elif name == "BRANCH":
                for b in av[1]:
                    scan_adjacent(b, fl)

    scan_adjacent(list(parsed), flags)

    # text-level checks
    if re.search(r"\[[^\]]*A-z", pattern):
        add("warn", "range-A-z", "[A-z] also matches [ \\ ] ^ _ and the backtick; use [A-Za-z]")
    seq = list(parsed)
    flat = flatten(seq)
    for k, (op, _av) in enumerate(flat):
        if op_name(op) != "ANY":
            continue
        before = flat[k - 1] if k else None
        after = [chr(a) for o, a in flat[k + 1:k + 4] if op_name(o) == "LITERAL"]
        lit_before = before is not None and op_name(before[0]) == "LITERAL" and chr(before[1]).isalnum()
        word_after = len(after) >= 2 and all(c.isalpha() for c in after[:2])
        if (lit_before and after and after[0].isalnum()) or word_after:
            add("warn", "unescaped-dot",
                f"'.' before {''.join(after) or 'the next token'!r} matches any char (so a '-' or 'x' passes there too); "
                "write \\. for a literal dot")
            break
    try:
        if re.compile(pattern, flags).fullmatch(""):
            add("warn", "matches-empty",
                "the pattern matches the empty string, so findall/finditer return empty matches and "
                "validation accepts blank input; require at least one char where it matters")
    except re.error:
        pass
    count_dotstar = len(re.findall(r"(?<!\\)\.\*", pattern))
    if count_dotstar >= 3:
        add("info", "many-dotstar", f"{count_dotstar} '.*' in one pattern: failures are slow on long lines; "
            "use negated classes like [^,]* to describe each field")
    elif count_dotstar and not re.search(r"\.\*\?", pattern):
        add("info", "greedy-dotstar", "greedy .* grabs as much as possible and then backs off; use .*? or a "
            "negated class such as [^\"]* when you mean 'up to the next ...'")
    first_ops = [op_name(op) for op, _ in seq[:1]]
    last = seq[-1] if seq else None
    anchored_start = first_ops == ["AT"]
    anchored_end = last is not None and op_name(last[0]) == "AT" and op_name(last[1]) in ("AT_END", "AT_END_STRING")
    if "fullmatch" not in modes_seen and not (anchored_start and anchored_end) and modes_seen != {"findall"}:
        add("info", "not-anchored", "for validation, anchor both ends (\\A ... \\Z) or use re.fullmatch; "
            "otherwise 'abc123xyz' passes a search for \\d+")
    if anchored_end and op_name(last[1]) == "AT_END" and not flags & re.MULTILINE and "fullmatch" not in modes_seen:
        add("info", "dollar-newline", "$ also matches before a trailing newline ('abc\\n' passes ^abc$); "
            "use \\Z or fullmatch for strict validation")
    if re.search(r"(?<!\\)\\[dw]", pattern) and not flags & re.ASCII:
        add("info", "unicode-classes", "\\d and \\w match Unicode digits and letters too (for example "
            "Arabic-Indic digits); add re.ASCII or use [0-9] / [A-Za-z0-9_] for strict input formats")
    return risks


# ---------------------------------------------------------------- probe

PROBE_CODE = r"""
import re, sys, time
pat, flags, s = sys.argv[1], int(sys.argv[2]), sys.argv[3]
rx = re.compile(pat, flags)
t = time.perf_counter(); rx.search(s); print(f"{time.perf_counter() - t:.6f}")
"""


def run_probe(pattern, flags, ch, compiled):
    tail = next((t for t in "!#\x00~ \n" if not compiled.search(ch * 3 + t)), "!")
    results, verdict = [], "no slowdown seen"
    for n in (12, 16, 20, 24, 28):
        s = ch * n + tail
        try:
            r = subprocess.run([sys.executable, "-c", PROBE_CODE, pattern, str(flags), s],
                               capture_output=True, text=True, timeout=2)
            secs = float(r.stdout.strip() or "nan")
        except subprocess.TimeoutExpired:
            results.append({"n": n, "seconds": None})
            verdict = f"TIMEOUT at length {n + 1}: exponential backtracking confirmed"
            break
        results.append({"n": n, "seconds": round(secs, 4)})
        prev = results[-2]["seconds"] if len(results) >= 2 else None
        if prev and secs > 0.01 and secs / prev > 3:
            verdict = (f"time grows about {secs / prev:.0f}x per +4 chars: exponential backtracking confirmed")
        elif secs > 0.5:
            verdict = f"slow ({secs:.2f} s at length {n + 1}) but not clearly exponential"
    return {"input": f"{ch!r} * n + {tail!r}", "timings": results, "verdict": verdict}


# ---------------------------------------------------------------- matching

def read_cases(path):
    text = sys.stdin.read() if path == "-" else open(path, encoding="utf-8").read()
    cases = []
    for raw in text.splitlines():
        if not raw.strip() or raw.lstrip().startswith("#"):
            continue
        if raw.startswith("+ ") or raw.startswith("- "):
            cases.append({"text": raw[2:], "expect": raw[0] == "+"})
        else:
            cases.append({"text": raw, "expect": None})
    return cases


def match_case(rx, text, mode):
    fn = {"search": rx.search, "match": rx.match, "fullmatch": rx.fullmatch}.get(mode)
    names = {v: k for k, v in rx.groupindex.items()}
    def groups(m):
        out = []
        for g in range(1, rx.groups + 1):
            out.append({"group": g, "name": names.get(g), "value": m.group(g),
                        "span": list(m.span(g)) if m.group(g) is not None else None})
        return out
    if mode == "findall":
        ms = list(rx.finditer(text))
        return {"matched": bool(ms), "matches": [{"text": m.group(0), "span": list(m.span()), "groups": groups(m)}
                                                for m in ms]}
    m = fn(text)
    if not m:
        return {"matched": False, "matches": []}
    return {"matched": True, "matches": [{"text": m.group(0), "span": list(m.span()), "groups": groups(m)}]}


def describe_flags(flags):
    names = [n for f, n in FLAG_NAMES if flags & f]
    return ", ".join(names) if names else "none"


# ---------------------------------------------------------------- main

def parse_args(argv):
    opts = {"texts": [], "cases": None, "mode": "search", "flags": 0, "probe": False, "explain": True,
            "json": False, "fail_on": "high", "pattern": None, "timeout": 2.0}
    i = 0
    while i < len(argv):
        a = argv[i]
        def val():
            if i + 1 >= len(argv):
                raise UsageError(f"{a} needs a value")
            return argv[i + 1]
        if a in ("-t", "--text"):
            opts["texts"].append(val()); i += 2
        elif a == "--cases":
            opts["cases"] = val(); i += 2
        elif a == "--mode":
            opts["mode"] = val(); i += 2
            if opts["mode"] not in ("search", "match", "fullmatch", "findall"):
                raise UsageError("--mode must be search, match, fullmatch, or findall")
        elif a == "--flags":
            letters = val(); i += 2
            bad = [c for c in letters.lower() if c not in FLAG_LETTERS]
            if bad:
                raise UsageError(f"unknown flag letter(s) {''.join(bad)!r}; use any of i m s x a")
            for c in letters.lower():
                opts["flags"] |= FLAG_LETTERS[c]
        elif a == "--fail-on":
            opts["fail_on"] = val().lower(); i += 2
            if opts["fail_on"] not in ("high", "warn", "info", "never"):
                raise UsageError("--fail-on must be high, warn, info, or never")
        elif a == "--timeout":
            try:
                opts["timeout"] = float(val())
            except ValueError:
                raise UsageError("--timeout needs a number of seconds, for example --timeout 2")
            i += 2
        elif a == "--probe":
            opts["probe"] = True; i += 1
        elif a == "--no-explain":
            opts["explain"] = False; i += 1
        elif a == "--json":
            opts["json"] = True; i += 1
        elif a in ("-h", "--help"):
            print(__doc__); sys.exit(0)
        elif a.startswith("--") and opts["pattern"] is not None:
            raise UsageError(f"unknown option {a}")
        elif opts["pattern"] is None:
            opts["pattern"] = a; i += 1
        else:
            raise UsageError(f"unexpected argument {a!r} (quote the pattern; use -t for sample inputs)")
    if opts["pattern"] is None:
        raise UsageError("give a PATTERN (quote it in single quotes in the shell)")
    return opts


def main(argv):
    try:
        opts = parse_args(argv)
        cases = [{"text": t, "expect": None} for t in opts["texts"]]
        if opts["cases"]:
            try:
                cases += read_cases(opts["cases"])
            except OSError as e:
                raise UsageError(f"cannot read cases file: {e.strerror}: {opts['cases']}")
    except UsageError as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    pattern, flags, mode = opts["pattern"], opts["flags"], opts["mode"]
    try:
        rx = re.compile(pattern, flags)
        parsed = sre_parse.parse(pattern, flags)
    except re.error as e:
        pos = f" at position {e.pos}" if e.pos is not None else ""
        print(f"error: pattern does not compile: {e.msg}{pos}", file=sys.stderr)
        if e.pos is not None:
            print(f"  {pattern}\n  {' ' * e.pos}^", file=sys.stderr)
        return 2
    flags = int(rx.flags) & ~int(re.UNICODE)
    names = {v: k for k, v in rx.groupindex.items()}
    risks = analyze(parsed, flags, pattern, {mode})
    if opts["probe"]:
        for r in risks:
            if r.get("probe"):
                r["probe_result"] = run_probe(pattern, flags, r["probe"], rx)
    results = []
    for c in cases:
        try:
            res = with_timeout(opts["timeout"], match_case, rx, c["text"], mode)
            status = None if c["expect"] is None else ("PASS" if res["matched"] == c["expect"] else "FAIL")
        except MatchTimeout:
            res, status = {"matched": None, "matches": []}, "TIMEOUT"
        results.append({**c, **res, "status": status})
    fails = sum(1 for r in results if r["status"] in ("FAIL", "TIMEOUT"))
    counts = {lvl: sum(1 for r in risks if r["level"] == lvl) for lvl in ("high", "warn", "info")}
    report = {"pattern": pattern, "flags": describe_flags(flags), "mode": mode, "groups": rx.groups,
              "group_names": {str(k): v for k, v in names.items()},
              "explanation": explain(list(parsed), names, flags) if opts["explain"] else [],
              "cases": results, "failed_expectations": fails, "risks": risks, "risk_counts": counts}
    threshold = LEVELS.get(opts["fail_on"], 99)
    risky = any(LEVELS[r["level"]] >= threshold for r in risks)
    if opts["json"]:
        print(json.dumps(report, indent=2, ensure_ascii=False))
        return 1 if fails or risky else 0

    print(f"Pattern: {pattern}")
    named = ", ".join(f'{g} "{n}"' for g, n in sorted(names.items()))
    print(f"Flags: {describe_flags(flags)} | mode: {mode} | groups: {rx.groups}" + (f" ({named})" if named else ""))
    if opts["explain"]:
        print("\nExplanation:")
        for line in report["explanation"]:
            print("  " + line)
    if results:
        print(f"\nSamples ({mode}):")
        for r in results:
            mark = {True: "+", False: "-", None: " "}[r["expect"]]
            tag = r["status"] or ("match" if r["matched"] else "no match")
            line = f"  {tag:<8} {mark} {r['text']!r}"
            if r["matched"]:
                parts = []
                for m in r["matches"][:5]:
                    g = " ".join(f"{x['name'] or x['group']}={x['value']!r}" for x in m["groups"])
                    parts.append(f"[{m['span'][0]}:{m['span'][1]}] {m['text']!r}" + (f" {g}" if g else ""))
                more = f" (+{len(r['matches']) - 5} more)" if len(r["matches"]) > 5 else ""
                line += "  -> " + "; ".join(parts) + more
            elif r["status"] == "TIMEOUT":
                line += f"  -> still running after {opts['timeout']:g} s (catastrophic backtracking)"
            elif r["status"] == "FAIL":
                line += "  -> expected a match"
            if r["status"] == "FAIL" and r["matched"]:
                line += "  (expected no match)"
            print(line)
    print("\nRisks and suggestions:")
    if not risks:
        print("  none found")
    for r in risks:
        print(f"  {r['level'].upper():<4} {r['rule']:<23} {r['message']}")
        pr = r.get("probe_result")
        if pr:
            times = ", ".join(f"n={t['n']}: {'timeout' if t['seconds'] is None else str(t['seconds']) + ' s'}"
                              for t in pr["timings"])
            print(f"       probe {pr['input']}: {times} -> {pr['verdict']}")
    summary = f"\nSummary: HIGH {counts['high']}  WARN {counts['warn']}  INFO {counts['info']}"
    if any(r["expect"] is not None for r in results):
        n = sum(1 for r in results if r["expect"] is not None)
        summary += f" | expectations {n - fails}/{n} pass"
    timeouts = sum(1 for r in results if r["status"] == "TIMEOUT")
    if timeouts:
        summary += f" | {timeouts} sample(s) timed out"
    print(summary)
    return 1 if fails or risky else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
PythonSecurityDebugging+2
F@f
0
Load Test Plan Builder (YAML)
Text

Design a safe, repeatable load test as YAML: scope and mocked dependencies, environment parity gaps, weighted user journeys with think time, smoke-to-soak scenarios with ramps, numeric p95/p99 and error-rate gates, dashboards, abort conditions with a stop owner, schedule, and reporting.

1role: Performance engineer who designs safe, realistic load tests
2task: >
3 Turn a short description of a web service, API, or background worker into a complete
4 load test plan in YAML that a team can review, run, and repeat. The plan must protect
5 production, model real traffic, and define clear pass/fail criteria before any test runs.
6
7ask_first:
8 - What is being tested (service name, endpoints or jobs, dependencies such as databases, caches, queues, third-party APIs)
9 - Current traffic shape (average and peak requests per second, daily pattern, read/write mix, payload sizes)
10 - Business goal for the test (launch readiness, capacity for an event, regression check, cost tuning)
...+78 more lines
PerformanceDevOpsYAML+2
F@f
0
Flaky Test Detector
Skill

Finds flaky tests from repeated CI runs: parses JUnit XML or a CSV run history, flags tests that both pass and fail (confirmed when on the same commit), ranks them by flip and failure rate, and suggests likely causes and fixes. Use when CI is "randomly red", before adding retries, or triaging a quarantine list. Includes a stdlib Python detector with --self-test.

---
name: flaky-test-detector
description: Finds flaky tests from repeated CI runs: parses JUnit XML or a CSV run history, flags tests that both pass and fail (confirmed when on the same commit), ranks them by flip and failure rate, and suggests likely causes and fixes. Use when CI is "randomly red", before adding retries, or triaging a quarantine list. Includes a stdlib Python detector with --self-test.
---

# Flaky Test Detector

You help engineering teams separate genuinely broken tests from flaky ones. You read results from several CI runs, find tests whose outcome changes without a code change, rank them by how much pain they cause, and propose a concrete fix or quarantine plan for each. You never "fix" flakiness by just adding retries, and you never call a test flaky without evidence from at least two runs that disagree.

## Files in this skill

- `scripts/flaky_tests.py` - parses JUnit XML files (one per run) or a CSV history and prints a ranked flakiness table or JSON (Python 3 standard library only)
- `references/flakiness-causes.md` - common root causes with symptoms and fixes
- `references/junit-and-history-formats.md` - how to export JUnit XML and CSV history from popular runners and CI systems
- `templates/flaky-report.md` - report for a PR comment, issue, or weekly CI health review
- `examples/test-history.csv` - sample run history with a same-commit flake

## Workflow

### 1. Collect evidence
Ask for, or help produce:
- JUnit XML from at least 3 runs of the same suite (CI artifacts, or a local loop such as running the suite 10 times)
- Or a CSV with `run_id,test,status` and, ideally, `commit` and `duration_s`
- Context: runner (pytest, Jest, JUnit, Go test, RSpec), parallelism, retries already enabled, shared services (DB, browser, network)

If only one run is available, explain that flakiness cannot be proven and suggest a rerun loop on an unchanged commit.

### 2. Run the detector
```
python3 scripts/flaky_tests.py run1.xml run2.xml run3.xml
python3 scripts/flaky_tests.py --csv examples/test-history.csv --only-flaky
python3 scripts/flaky_tests.py --csv history.csv --json
python3 scripts/flaky_tests.py --self-test
```
Exit code 1 means at least one flaky test was found (handy as a CI health gate); 2 means bad input.

### 3. Classify
- **flaky_confirmed**: passed and failed on the same commit. Strongest evidence; fix first.
- **flaky**: mixed results across runs, commit unknown or different. Check whether the code changed between runs.
- **always_failing**: a real failure, not flakiness. Route to the owner as a bug.
- **stable** / **insufficient_data**: no action, or collect more runs.

Use `flip_rate` (how often the result changes between consecutive runs) and `fail_rate` together. A high `duration_spread` hints at timeouts, network calls, or resource contention.

### 4. Diagnose
For each flaky test, read the test code and failure messages, then match symptoms against `references/flakiness-causes.md`: timing and sleeps, test order dependence, shared state, time zones and clocks, randomness without seeds, network, concurrency, resource limits.

### 5. Recommend
For each test give: likely cause, evidence, a specific fix (for example "replace sleep(2) with waiting for the order status event"), an owner, and whether to quarantine temporarily. Fill `templates/flaky-report.md`.

## Rules

- Retries are a temporary bandage with an expiry date, never the fix.
- Quarantined tests need an owner and a ticket; list them in the report.
- Do not delete a flaky test without the owner's agreement.
- Keep statistics honest: say how many runs the conclusion is based on.
- Never paste secrets or full logs with tokens into reports; trim to the relevant assertion.
FILE:references/flakiness-causes.md
# Common causes of flaky tests

| Cause | Typical symptoms | Fix |
|---|---|---|
| Fixed sleeps and timing | Fails more on busy CI agents; duration spread is high | Wait for a condition or event with a timeout instead of `sleep` |
| Test order dependence | Passes alone, fails in the full suite or with random order | Reset shared state in setup/teardown; run with random order to reproduce |
| Shared mutable state | Globals, singletons, caches, env vars leak between tests | Isolate fixtures; restore env; avoid module-level state |
| Shared external resources | Same DB rows, ports, temp files, or queues used by parallel workers | Unique names per worker; transactions rolled back per test; random free ports |
| Clock and time zone | Fails near midnight, month end, DST changes, or on agents in other zones | Freeze or inject the clock; use UTC; avoid "now" in assertions |
| Unseeded randomness | Rare failures with odd data | Seed generators and print the seed on failure |
| Network and third-party calls | Timeouts, 429s, DNS errors | Mock or record responses; keep real calls in a separate, non-blocking suite |
| Concurrency and async | Race conditions, unawaited promises, threads not joined | Await everything; use proper synchronization; add deterministic hooks |
| Resource limits | OOM, file handle exhaustion, slow containers | Lower parallelism; clean up resources; raise limits deliberately |
| UI and browser tests | Element not found, animation timing, viewport differences | Use stable selectors and auto-waiting locators; disable animations in test mode |
| Floating point and ordering | Assertion on unordered collections or exact floats | Compare sorted values; use tolerance |

## Triage questions
1. Does it fail on the same commit when rerun? (flaky_confirmed)
2. Does it fail alone, or only with other tests?
3. Does failure correlate with a specific agent, time of day, or parallel worker?
4. Is the failure message always the same?
5. Did retries hide it before? Check retry logs.
FILE:references/junit-and-history-formats.md
# Getting JUnit XML and run history

## JUnit XML per run
- pytest: `pytest --junitxml=reports/run-$N.xml`
- Jest: `jest-junit` reporter (`JEST_JUNIT_OUTPUT_FILE=reports/run-$N.xml`)
- Go: `go test -v ./... 2>&1 | go-junit-report > reports/run-$N.xml`
- JUnit / Maven Surefire: `target/surefire-reports/*.xml` (merge per run)
- RSpec: `rspec_junit_formatter`
- Playwright: `--reporter=junit`

Rerun loop on an unchanged commit:
```
for N in $(seq 1 10); do pytest --junitxml=reports/run-$N.xml || true; done
python3 scripts/flaky_tests.py reports/run-*.xml
```
Pass files in run order (glob order is fine if names sort correctly, e.g. run-01..run-10).

## How the script reads JUnit
- Test id = `classname::name`
- `<failure>` or `<error>` child = fail; `<skipped>` = skip; otherwise pass
- `time` attribute = duration in seconds

## CSV history
Columns: `run_id,test,status` plus optional `commit,duration_s`. Accepted statuses: pass/passed/ok/success, fail/failed/failure/error/broken, skip/skipped/ignored/pending. Rows must be in run order.

Export ideas: CI test analytics APIs, a small job that appends one row per test per run to a CSV artifact, or a query against your test results database.
FILE:templates/flaky-report.md
# Flaky Test Report

**Suite:** <name>  **Runs analyzed:** <n> (<date range or commits>)  **Runner:** <pytest / Jest / ...>

## Summary
- flaky_confirmed: <n>  flaky: <n>  always_failing: <n>  stable: <n>  insufficient_data: <n>
- Retries currently enabled: <yes/no, count>

## Fix first
| Test | Class | Fail rate | Flip rate | Likely cause | Evidence | Proposed fix | Owner | Quarantine? |
|---|---|---|---|---|---|---|---|---|
| | | | | | | | | |

## Real failures (not flaky)
| Test | Failing since | Message (trimmed) | Owner |
|---|---|---|---|

## Quarantine list
| Test | Ticket | Owner | Expiry date |
|---|---|---|---|

## Next steps
1. 
2. 
FILE:examples/test-history.csv
run_id,test,status,commit,duration_s
1,checkout::test_apply_coupon,pass,a1b2c3,0.42
1,checkout::test_payment_timeout,pass,a1b2c3,1.10
1,search::test_autocomplete,pass,a1b2c3,0.31
1,profile::test_avatar_upload,failed,a1b2c3,2.05
2,checkout::test_apply_coupon,pass,a1b2c3,0.40
2,checkout::test_payment_timeout,failed,a1b2c3,5.02
2,search::test_autocomplete,pass,a1b2c3,0.29
2,profile::test_avatar_upload,failed,a1b2c3,2.10
3,checkout::test_apply_coupon,pass,d4e5f6,0.44
3,checkout::test_payment_timeout,pass,d4e5f6,1.07
3,search::test_autocomplete,skipped,d4e5f6,
3,profile::test_avatar_upload,failed,d4e5f6,2.01
4,checkout::test_apply_coupon,pass,d4e5f6,0.41
4,checkout::test_payment_timeout,failed,d4e5f6,5.01
4,search::test_autocomplete,pass,d4e5f6,0.30
4,profile::test_avatar_upload,failed,d4e5f6,2.07
FILE:scripts/flaky_tests.py
#!/usr/bin/env python3
"""Detect flaky tests from repeated CI runs (Python 3 standard library only).

Inputs (any mix):
  * JUnit XML files, one per run, in run order:  flaky_tests.py run1.xml run2.xml run3.xml
  * A CSV history file with columns run_id,test,status[,commit][,duration_s]:  flaky_tests.py --csv history.csv

Statuses: pass, fail (failure or error), skip. Skips are ignored for flakiness math.
Classifications per test:
  flaky_confirmed   - passed AND failed on the same commit
  flaky             - passed AND failed across runs (commit unknown or different)
  always_failing    - failed in every non-skipped run (>= --min-runs)
  stable            - passed in every non-skipped run (>= --min-runs)
  insufficient_data - fewer than --min-runs non-skipped results

Exit code: 1 if any flaky/flaky_confirmed test is found, else 0 (2 on input errors).
"""
import argparse
import csv
import json
import sys
import xml.etree.ElementTree as ET
from collections import OrderedDict

STATUS_ALIASES = {
    "pass": "pass", "passed": "pass", "ok": "pass", "success": "pass",
    "fail": "fail", "failed": "fail", "failure": "fail", "error": "fail", "broken": "fail",
    "skip": "skip", "skipped": "skip", "ignored": "skip", "pending": "skip",
}


def norm_status(raw):
    s = STATUS_ALIASES.get(str(raw).strip().lower())
    if s is None:
        raise ValueError(f"unknown status: {raw!r}")
    return s


def parse_junit_text(text, run_id):
    """Return list of (run_id, test_id, status, commit, duration) from JUnit XML text."""
    root = ET.fromstring(text)
    rows = []
    for case in root.iter("testcase"):
        cls = case.get("classname") or ""
        name = case.get("name") or "?"
        test_id = f"{cls}::{name}" if cls else name
        if case.find("failure") is not None or case.find("error") is not None:
            status = "fail"
        elif case.find("skipped") is not None:
            status = "skip"
        else:
            status = "pass"
        try:
            dur = float(case.get("time", "") or 0.0)
        except ValueError:
            dur = 0.0
        rows.append((run_id, test_id, status, None, dur))
    return rows


def parse_csv_text(text):
    rows = []
    reader = csv.DictReader(text.splitlines())
    need = {"run_id", "test", "status"}
    if not reader.fieldnames or not need.issubset({f.strip() for f in reader.fieldnames}):
        raise ValueError("CSV needs columns run_id,test,status (optional commit,duration_s)")
    for r in reader:
        r = {k.strip(): (v or "").strip() for k, v in r.items() if k}
        dur = float(r["duration_s"]) if r.get("duration_s") else 0.0
        rows.append((r["run_id"], r["test"], norm_status(r["status"]), r.get("commit") or None, dur))
    return rows


def analyze(rows, min_runs=3):
    """rows in run order. Returns OrderedDict test_id -> stats dict."""
    tests = OrderedDict()
    for run_id, test_id, status, commit, dur in rows:
        t = tests.setdefault(test_id, {"results": [], "durations": []})
        t["results"].append((run_id, status, commit))
        if status != "skip" and dur:
            t["durations"].append(dur)
    out = OrderedDict()
    for test_id, t in tests.items():
        seq = [s for _, s, _ in t["results"] if s != "skip"]
        passes = seq.count("pass")
        fails = seq.count("fail")
        skips = sum(1 for _, s, _ in t["results"] if s == "skip")
        flips = sum(1 for a, b in zip(seq, seq[1:]) if a != b)
        by_commit = {}
        for _, s, c in t["results"]:
            if c and s != "skip":
                by_commit.setdefault(c, set()).add(s)
        mixed_commits = sorted(c for c, ss in by_commit.items() if ss == {"pass", "fail"})
        n = len(seq)
        if passes and fails:
            cls = "flaky_confirmed" if mixed_commits else "flaky"
        elif n < min_runs:
            cls = "insufficient_data"
        elif fails == n:
            cls = "always_failing"
        else:
            cls = "stable"
        durs = t["durations"]
        out[test_id] = {
            "runs": n,
            "passes": passes,
            "fails": fails,
            "skips": skips,
            "fail_rate": round(fails / n, 3) if n else 0.0,
            "flips": flips,
            "flip_rate": round(flips / (n - 1), 3) if n > 1 else 0.0,
            "mixed_commits": mixed_commits,
            "max_duration_s": round(max(durs), 3) if durs else None,
            "duration_spread": round(max(durs) / min(durs), 2) if durs and min(durs) > 0 else None,
            "classification": cls,
        }
    return out


def priority(stats):
    """Higher = fix first. Confirmed flakiness and high flip rates rank highest."""
    base = {"flaky_confirmed": 3.0, "flaky": 2.0, "always_failing": 1.0}.get(stats["classification"], 0.0)
    return round(base + stats["flip_rate"] + stats["fail_rate"], 3)


def render_table(results):
    lines = ["| Test | Class | Runs | Fails | Fail rate | Flip rate | Same-commit mix | Priority |",
             "|---|---|---|---|---|---|---|---|"]
    ranked = sorted(results.items(), key=lambda kv: (-priority(kv[1]), kv[0]))
    for tid, s in ranked:
        lines.append(f"| {tid} | {s['classification']} | {s['runs']} | {s['fails']} | "
                     f"{s['fail_rate']:.0%} | {s['flip_rate']:.0%} | {', '.join(s['mixed_commits']) or '-'} | {priority(s)} |")
    return "\n".join(lines)


def summary(results):
    counts = {}
    for s in results.values():
        counts[s["classification"]] = counts.get(s["classification"], 0) + 1
    return counts


JUNIT_RUN = """<testsuites><testsuite name="api">
<testcase classname="api.test_orders" name="test_create" time="0.12"/>
<testcase classname="api.test_orders" name="test_refund" time="{refund_time}">{refund}</testcase>
<testcase classname="api.test_auth" name="test_expired_token" time="0.05">{auth}</testcase>
<testcase classname="api.test_search" name="test_reindex" time="1.0"><skipped/></testcase>
</testsuite></testsuites>"""


def _run(refund_fail, auth_fail, refund_time="0.30"):
    return JUNIT_RUN.format(
        refund='<failure message="timeout"/>' if refund_fail else "",
        auth='<error message="boom"/>' if auth_fail else "",
        refund_time=refund_time)


def self_test():
    rows = []
    pattern = [(False, True), (True, True), (False, True), (True, True), (False, True)]
    for i, (rf, af) in enumerate(pattern, 1):
        rows += parse_junit_text(_run(rf, af, "0.30" if not rf else "3.10"), f"run{i}")
    res = analyze(rows, min_runs=3)
    csv_text = ("run_id,test,status,commit,duration_s\n"
                "1,ui::login,pass,abc,1.0\n2,ui::login,failed,abc,1.2\n3,ui::login,pass,def,0.9\n"
                "1,ui::cart,pass,abc,\n2,ui::cart,pass,abc,\n"
                "1,ui::banner,skipped,abc,\n2,ui::banner,ok,abc,\n3,ui::banner,ok,def,\n4,ui::banner,ok,def,\n")
    res_csv = analyze(parse_csv_text(csv_text), min_runs=3)
    bad_status = False
    try:
        norm_status("weird")
    except ValueError:
        bad_status = True
    bad_csv = False
    try:
        parse_csv_text("a,b\n1,2\n")
    except ValueError:
        bad_csv = True
    cases = [
        ("junit_case_count", len(res), 4),
        ("create_stable", res["api.test_orders::test_create"]["classification"], "stable"),
        ("refund_flaky", res["api.test_orders::test_refund"]["classification"], "flaky"),
        ("refund_flips", res["api.test_orders::test_refund"]["flips"], 4),
        ("refund_flip_rate", res["api.test_orders::test_refund"]["flip_rate"], 1.0),
        ("refund_fail_rate", res["api.test_orders::test_refund"]["fail_rate"], 0.4),
        ("refund_duration_spread", res["api.test_orders::test_refund"]["duration_spread"], 10.33),
        ("error_counts_as_fail", res["api.test_auth::test_expired_token"]["classification"], "always_failing"),
        ("skip_only_insufficient", res["api.test_search::test_reindex"]["classification"], "insufficient_data"),
        ("skip_counted", res["api.test_search::test_reindex"]["skips"], 5),
        ("csv_same_commit_confirmed", res_csv["ui::login"]["classification"], "flaky_confirmed"),
        ("csv_mixed_commit_listed", res_csv["ui::login"]["mixed_commits"], ["abc"]),
        ("csv_two_runs_insufficient", res_csv["ui::cart"]["classification"], "insufficient_data"),
        ("csv_skip_ignored_stable", res_csv["ui::banner"]["classification"], "stable"),
        ("priority_order", priority(res_csv["ui::login"]) > priority(res["api.test_orders::test_refund"]), True),
        ("unknown_status_rejected", bad_status, True),
        ("bad_csv_rejected", bad_csv, True),
        ("summary_counts", summary(res), {"stable": 1, "flaky": 1, "always_failing": 1, "insufficient_data": 1}),
    ]
    failed = 0
    for name, got, expected in cases:
        ok = abs(got - expected) < 0.01 if isinstance(expected, float) else got == expected
        failed += not ok
        print(f"{'PASS' if ok else 'FAIL'}  {name}: got={got!r} expected={expected!r}")
    print(f"\n{len(cases) - failed}/{len(cases)} passed")
    return 1 if failed else 0


def main(argv=None):
    p = argparse.ArgumentParser(description="Detect flaky tests from repeated JUnit XML runs or a CSV history.")
    p.add_argument("junit", nargs="*", help="JUnit XML files in run order")
    p.add_argument("--csv", help="CSV history: run_id,test,status[,commit][,duration_s]")
    p.add_argument("--min-runs", type=int, default=3)
    p.add_argument("--json", action="store_true", help="print JSON instead of a markdown table")
    p.add_argument("--only-flaky", action="store_true")
    p.add_argument("--self-test", action="store_true")
    a = p.parse_args(argv)
    if a.self_test:
        return self_test()
    rows = []
    try:
        for i, path in enumerate(a.junit, 1):
            with open(path, encoding="utf-8") as fh:
                rows += parse_junit_text(fh.read(), f"run{i}:{path}")
        if a.csv:
            with open(a.csv, encoding="utf-8") as fh:
                rows += parse_csv_text(fh.read())
    except (OSError, ValueError, ET.ParseError) as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    if not rows:
        p.error("give JUnit XML files, --csv, or --self-test")
    res = analyze(rows, a.min_runs)
    if a.only_flaky:
        res = OrderedDict((k, v) for k, v in res.items() if v["classification"].startswith("flaky"))
    if a.json:
        print(json.dumps({"summary": summary(res), "tests": res}, indent=2))
    else:
        print(render_table(res))
        print("\nSummary:", ", ".join(f"{k}={v}" for k, v in sorted(summary(res).items())))
    return 1 if any(v["classification"].startswith("flaky") for v in res.values()) else 0


if __name__ == "__main__":
    sys.exit(main())
DevOpsPythonDebugging+3
F@f
0
Testing taste
Text

Opinionated preferences for writing fast, reliable automated tests in TypeScript and JavaScript projects.

# Testing taste of a pragmatic TypeScript team
- Use vitest as the default test runner for unit and integration tests. Confidence: 0.90
- Keep test files next to the code they cover as *.test.ts. Confidence: 0.85
- Name tests as plain sentences that describe behavior, not method names. Confidence: 0.90
- Follow arrange, act, assert with a blank line between each phase. Confidence: 0.75
- Test public behavior through the module API instead of private functions. Confidence: 0.90
- Prefer real collaborators and in-memory fakes over mocks. Confidence: 0.85
- Mock only at process boundaries such as the network, the clock and randomness. Confidence: 0.90
- Intercept HTTP with msw instead of stubbing fetch by hand. Confidence: 0.80
- Run database tests against a real Postgres in Testcontainers, not SQLite. Confidence: 0.80
- Freeze time with fake timers and pass seeds to anything random. Confidence: 0.90
- Build test data with small factory functions that accept overrides. Confidence: 0.85
- Keep snapshot tests rare and small, and review every snapshot diff. Confidence: 0.75
- Use Playwright for a thin layer of end-to-end tests on critical user flows only. Confidence: 0.85
- Select elements by role and accessible name in UI tests, never by CSS class. Confidence: 0.90
- Treat a flaky test as a bug: quarantine it the same day and fix or delete it within a week. Confidence: 0.85
- Never use sleep in tests; wait for an explicit condition instead. Confidence: 0.95
- Keep the full unit suite under one minute on a laptop. Confidence: 0.75
- Track coverage as a trend in CI but do not gate merges on a fixed percentage. Confidence: 0.70
- Add property-based tests with fast-check for parsers and pure logic. Confidence: 0.65
- Write a failing regression test before fixing every reported bug. Confidence: 0.90
- Run tests in random order to catch hidden shared state. Confidence: 0.70
TypeScriptJavaScriptTesting+2
F@f
0
FastAPI Service taste
Text

Opinionated conventions for building and running production FastAPI services in Python.

# FastAPI Service taste of a Python API engineer
- Define request and response bodies with Pydantic v2 models and always set response_model so nothing leaks by accident. Confidence: 0.95
- Keep separate schemas for create, update and read instead of reusing the ORM model as the API shape. Confidence: 0.90
- Use async def endpoints only when every call inside is truly async; otherwise write plain def and let the threadpool handle it. Confidence: 0.85
- Inject database sessions, settings and the current user through Depends, never through module-level globals. Confidence: 0.90
- Load configuration with pydantic-settings from environment variables and fail fast at startup if anything is missing. Confidence: 0.90
- Split routes into APIRouter modules by domain and keep main.py limited to app creation and wiring. Confidence: 0.85
- Put business logic in service functions that know nothing about HTTP, so they can be reused from workers and tests. Confidence: 0.85
- Use SQLAlchemy 2.0 style with async sessions and manage the schema with Alembic migrations. Confidence: 0.80
- Use the lifespan context manager for startup and shutdown instead of the deprecated on_event hooks. Confidence: 0.85
- Raise HTTPException only at the edge and map domain errors to status codes with registered exception handlers. Confidence: 0.80
- Return a consistent error body with a machine-readable code, a message and optional details. Confidence: 0.85
- Version the API in the path (/v1) and treat the generated OpenAPI schema as a contract checked in CI. Confidence: 0.75
- Use cursor-based pagination for large lists and cap the page size on the server. Confidence: 0.80
- Test with pytest and httpx.AsyncClient, overriding dependencies instead of patching internals. Confidence: 0.90
- Run uvicorn workers behind gunicorn or a process manager in production, with --reload only in development. Confidence: 0.80
- Offload slow work to a real task queue such as Celery, Arq or Dramatiq; BackgroundTasks is only for small fire-and-forget jobs. Confidence: 0.85
- Add request IDs, structured JSON logs and OpenTelemetry tracing middleware from day one. Confidence: 0.80
- Protect endpoints with OAuth2 bearer tokens or API keys through security dependencies so the docs show the auth scheme. Confidence: 0.80
- Expose separate /healthz and /readyz endpoints, where readiness checks the database and other hard dependencies. Confidence: 0.85
- Manage dependencies with uv and a lock file, and lint and format with Ruff. Confidence: 0.75
- Type check with mypy or pyright in strict mode on the service package. Confidence: 0.70
BackendBest PracticesPython+2
F@f
0
Feature Flag Rollout taste
Text

Opinionated conventions for shipping changes safely with feature flags and gradual rollouts.

# Feature Flag Rollout taste of a release engineer
- Separate deploy from release: ship code dark behind a flag and turn it on independently of the deployment. Confidence: 0.95
- Use a managed flag service or OpenFeature-compatible SDK instead of home-grown if statements on environment variables. Confidence: 0.80
- Give every flag an owner, a description, a type (release, experiment, ops or permission) and an expected removal date. Confidence: 0.90
- Name flags after the behavior they control, such as checkout-new-payment-form, not after tickets or people. Confidence: 0.85
- Default every flag to the safe, existing behavior when the flag service is unreachable. Confidence: 0.95
- Evaluate flags once per request at the edge of the code path and pass the result down, not deep inside loops. Confidence: 0.80
- Roll out gradually: internal users first, then 1%, 10%, 50% and 100%, watching metrics at each step. Confidence: 0.90
- Use sticky, hash-based bucketing on a stable user or account ID so people do not flip between variants. Confidence: 0.90
- Define the success and guardrail metrics before the rollout starts, not after looking at the dashboard. Confidence: 0.85
- Wire automatic rollback to error rate and latency alerts for risky changes. Confidence: 0.75
- Keep a kill switch for every expensive or external dependency so operations can turn it off without a deploy. Confidence: 0.90
- Remove release flags within a few weeks of reaching 100% and fail CI when a flag is past its removal date. Confidence: 0.80
- Test both the on and off paths in CI for every active flag. Confidence: 0.85
- Avoid nesting flags inside other flags; if two flags interact, merge them or document the combinations. Confidence: 0.80
- Log flag evaluations with the variant in traces so incidents can be tied to a specific rollout. Confidence: 0.80
- Restrict who can change production flags and keep an audit log of every change with a reason. Confidence: 0.85
- Never use feature flags as a substitute for authorization checks. Confidence: 0.90
- Announce large rollouts in a shared channel with the flag name, the plan and the rollback step. Confidence: 0.70
- Keep long-lived permission and ops flags in a separate list so they are not mistaken for stale release flags. Confidence: 0.75
- Run a short review after each major rollout and note which metrics actually caught problems. Confidence: 0.65
ArchitectureBest PracticesDevOps+1
F@f
0
Machine Learning Experiment taste
Text

Opinionated conventions for running reproducible machine learning experiments, from baseline to production.

# Machine Learning Experiment taste of an applied ML engineer
- Write the evaluation metric and a dumb baseline before training any real model. Confidence: 0.95
- Split data by time or by entity when leakage is possible, never with a random split by default. Confidence: 0.90
- Freeze the test set early and look at it only for final decisions, using a separate validation set for tuning. Confidence: 0.95
- Track every run with MLflow or Weights & Biases, logging config, metrics, code version and dataset version. Confidence: 0.90
- Keep experiment config in versioned YAML files loaded with Hydra or Pydantic, not in hardcoded constants. Confidence: 0.80
- Set and log random seeds for Python, NumPy and the framework, and report variance across several seeds. Confidence: 0.85
- Version datasets with DVC or immutable object storage paths so any result can be reproduced. Confidence: 0.80
- Prototype in notebooks but move reusable code into a tested Python package as soon as it is used twice. Confidence: 0.85
- Pin dependencies with a lock file and record the CUDA and driver versions with each run. Confidence: 0.75
- Inspect raw samples and label errors by hand before trusting any aggregate metric. Confidence: 0.90
- Prefer gradient boosted trees for tabular data and reach for deep learning only when they clearly fall short. Confidence: 0.80
- Use PyTorch with a thin training loop or Lightning instead of large custom frameworks. Confidence: 0.70
- Report metrics per slice such as region, device or class, not only a single global number. Confidence: 0.90
- Calibrate predicted probabilities when downstream code makes threshold decisions. Confidence: 0.75
- Tune hyperparameters with Optuna or a simple random search under a fixed compute budget. Confidence: 0.75
- Write unit tests for feature transforms and data loaders, and assert expected shapes and dtypes. Confidence: 0.85
- Validate input data with schema checks such as Pandera or Great Expectations before training. Confidence: 0.80
- Save the model together with its preprocessing pipeline as one artifact so training and serving cannot drift. Confidence: 0.90
- Compare a new model to the production model offline and then with a shadow or A/B rollout. Confidence: 0.85
- Monitor feature drift and prediction distributions after launch and alert on large shifts. Confidence: 0.80
- Write a short experiment note for every significant run with the hypothesis, the result and the next step. Confidence: 0.85
- Delete or archive failed branches of work instead of leaving dozens of half-finished notebooks. Confidence: 0.65
Best PracticesPythonTesting+2
F@f
0
Gitignore Pattern Tester and Reviewer
Skill

Explains every line of a .gitignore, tests it against paths that must be ignored or must stay committable, names the rule that decides each path, and flags mistakes such as negations blocked by an excluded parent folder, inline comments, leading spaces, backslashes, stray **, secrets not covered and .env.example ignored. Returns a fixed file. Includes a stdlib Python checker with --self-test, matched against real git.

---
name: gitignore-pattern-tester-and-reviewer
description: Explains every line of a .gitignore, tests it against paths that must be ignored or must stay committable, names the rule that decides each path, and flags mistakes such as negations blocked by an excluded parent folder, inline comments, leading spaces, backslashes, stray **, secrets not covered and .env.example ignored. Returns a fixed file. Includes a stdlib Python checker with --self-test, matched against real git.
---

# Gitignore Pattern Tester and Reviewer

You help developers understand and fix `.gitignore` files. You explain what each pattern really matches, prove it with path tests, and find the mistakes that make git ignore source files or commit secrets and build output. You return a corrected file with tests that pass, not just advice.

## Files in this skill

- `scripts/gitignore_check.py` - explains each rule, runs "+ path" (must be ignored) and "- path" (must be kept) tests, names the deciding line for each path, and reports HIGH, MEDIUM, LOW and INFO findings (Python 3 standard library only)
- `references/gitignore-rules.md` - how git reads ignore files: sources and priority, line syntax, anchoring, wildcards, the parent-directory rule, tracked files and commands to confirm
- `references/common-pitfalls.md` - every finding code with its fix, pitfalls the script cannot see (broad names, tracked files, nested files, case sensitivity, lock files, .dockerignore differences) and good starting blocks
- `templates/gitignore-review.md` - the review to hand back
- `examples/webapp.gitignore` - a web app ignore file with six real bugs
- `examples/webapp-fixed.gitignore` - the corrected file (all tests pass)
- `examples/webapp-paths.txt` - 19 path tests used with both files

## Workflow

### 1. Collect the inputs
Ask for the `.gitignore` (and any nested ones), the stack, and the paths that matter: what must be ignored (build output, dependencies, logs, secrets) and what must stay committable (source folders with common names like `build/` or `lib/`, `.gitkeep` files, `.env.example`, lock files). If the user only asks "why is X ignored?" ask for the path and use `git check-ignore -v X` output if they have it.

### 2. Run the checker
```
python3 scripts/gitignore_check.py examples/webapp.gitignore --test examples/webapp-paths.txt
python3 scripts/gitignore_check.py .gitignore --test paths.txt --json
python3 scripts/gitignore_check.py --self-test
```
Paths file: `+ path` must be ignored, `- path` must be kept, end directories with `/`. Exit 1 means a HIGH finding or a failed test, 2 means unreadable input. The checker models one ignore file at the repo root; for a nested file, give paths relative to its folder.

### 3. Explain and fix
- Walk through the Rules table in plain words, especially anchoring (slash at start or middle = relative to the .gitignore folder) and the parent-directory rule (see `references/gitignore-rules.md`).
- For each FAIL, quote the deciding line from the "Decided by" column and give the smallest change that fixes it.
- Fix every HIGH and MEDIUM finding; explain any INFO you leave in place.
- Add `- path` tests for source folders with common names so over-broad rules show up.
- Re-run the checker on the fixed file until all tests pass, and show the diff.

### 4. Deliver
Fill `templates/gitignore-review.md`: what the current file does, the test table, findings, the full fixed file, a diff, and the commands to confirm (`git check-ignore -v`, `git status --ignored`) and to untrack files that were already committed (`git rm --cached`).

## Example result
For `examples/webapp.gitignore` with `examples/webapp-paths.txt`: 13/19 tests pass, 1 HIGH, 4 MEDIUM, 2 LOW, 1 INFO. HIGH NEGATION_BLOCKED: `logs/` excludes the folder, so `!logs/.gitkeep` can never apply (fix: `logs/*`). MEDIUM: `config/secrets.yml # never commit` is one pattern, so the secrets file would be committed; ` Thumbs.db` has a leading space; `src\generated` is read as `srcgenerated`; and `*.key`, `*.pem` and `id_rsa` files are not covered. LOW: `.env*` hides `.env.example`; `*.log` is listed twice. The unanchored `build` also hides `src/build/README.md` (fix: `/build/`). `examples/webapp-fixed.gitignore` passes 19/19 with no HIGH, MEDIUM or LOW findings.

## Rules

- Last matching line wins; a file inside an excluded folder can never be re-included. Say this whenever a `!` line is involved.
- Never suggest ignoring lock files (`package-lock.json`, `poetry.lock`, `go.sum`, app `Cargo.lock`) or shared config the team commits on purpose.
- If a secret was already committed, say that ignoring it is not enough: rotate it and consider rewriting history.
- Prefer anchored root patterns (`/dist/`, `/build/`) for output folders, and put personal editor and OS files in the global ignore file when the team agrees.
- Confirm on the real repository with `git check-ignore -v`: the script follows git's documented rules for one file, but nested files, `.git/info/exclude`, the global file and `core.ignorecase` can change the result.
FILE:references/gitignore-rules.md
# How git reads .gitignore

Source: the gitignore(5) manual (`git help gitignore`). The checker implements these rules and was compared with real git 2.47 on 6,956 path checks with no differences.

## Where rules come from (highest priority first)
1. Command-line `--exclude` options (rare; used by some tools).
2. `.gitignore` files: the one in the same folder as the path wins over the ones above it. Patterns are relative to the folder that holds that `.gitignore`.
3. `.git/info/exclude` - local to one clone, never committed. Good for personal scratch files.
4. The global file in `core.excludesFile` (default `~/.config/git/ignore`) - good for editor and OS files such as `.DS_Store` and `*.swp`.

Inside one file, the LAST matching line decides.

## Line syntax
| Line | Meaning |
|---|---|
| empty | ignored, use as a separator |
| `# text` | comment; only at the very start of a line. `foo # bar` is the pattern `foo # bar` |
| `\#file` | a name that starts with `#` |
| `!pattern` | re-include something an earlier line excluded |
| `\!file` | a name that starts with `!` |
| `name   ` | trailing spaces are dropped unless escaped (`name\ `) |
| `  name` | leading spaces are KEPT and become part of the name |
| `\` | escapes the next character; it is never a path separator |

## Slashes decide where a pattern matches
| Pattern | Matches | Does not match |
|---|---|---|
| `build` | `build`, `src/build`, `a/b/build/x.js` (any file or folder named build) | `build.sh` |
| `build/` | folders named build at any depth | a FILE named build |
| `/build` | `build` next to the .gitignore only | `src/build` |
| `doc/frotz` | `doc/frotz` next to the .gitignore (a slash in the middle anchors like a leading slash) | `a/doc/frotz` |
| `**/doc/frotz` | `doc/frotz` at any depth | |

Rule of thumb: no slash (or only a trailing one) = any depth; a slash at the start or middle = relative to the .gitignore folder.

## Wildcards
| Wildcard | Meaning |
|---|---|
| `*` | any characters except `/` (`*.log`, `/a*z` does not match `ab/cz`) |
| `?` | exactly one character except `/` |
| `[abc]`, `[a-z]`, `[!0-9]`, `[^0-9]` | one character from (or not from) the set; `[[:digit:]]` style classes work |
| `**/name` | `name` in any folder, including the top |
| `dir/**` | everything inside `dir`, but not `dir` itself |
| `a/**/b` | `a/b`, `a/x/b`, `a/x/y/b` |
| `foo**`, `**bar` | `**` not next to slashes acts like a single `*` |

## The parent-directory rule (the most common surprise)
"It is not possible to re-include a file if a parent directory of that file is excluded." Git does not even list the contents of an excluded folder, so a later `!` line never gets a chance.

```gitignore
# does NOT work: logs/ is excluded as a folder
logs/
!logs/.gitkeep

# works: exclude the folder's contents, keep the folder visible
logs/*
!logs/.gitkeep

# works for deeper files: re-include each folder level
/assets/*
!/assets/icons/
/assets/icons/*
!/assets/icons/app.svg
```

## Whitelist style
Ignore everything, then allow what you want:
```gitignore
*
!*/
!*.md
!src/**
```
`!*/` re-includes every folder so the later `!` lines can reach files inside them.

## Already tracked files
.gitignore only affects untracked files. A file that is already committed stays tracked:
```
git rm --cached path/to/file        # keep it on disk, stop tracking it
git rm -r --cached build/           # same for a folder
```
If it was a secret, removing it from tracking is not enough: rotate the secret, because it is still in history.

## Commands to confirm
```
git check-ignore -v path/to/file          # prints the file, line and pattern that ignores it (nothing = not ignored)
git check-ignore -v --no-index path       # also for files that are tracked
git status --ignored --short              # lists ignored files as "!!"
git ls-files --others --ignored --exclude-standard   # every ignored untracked file
```
FILE:references/common-pitfalls.md
# Common .gitignore pitfalls and the checker's codes

| Code | Severity | What goes wrong | Fix |
|---|---|---|---|
| NEGATION_BLOCKED | HIGH | `!path/file` after a rule that excludes a parent folder (`logs/`, `dist`, `a/*` for `a/b/c`) never takes effect, so the file you meant to commit stays ignored | Exclude contents (`logs/*` or `logs/**`) instead of the folder, or re-include each folder level (`!a/b/`) |
| SECRETS_NOT_COVERED | MEDIUM | typical secret files (`.env`, `*.pem`, `*.key`, `id_rsa`) would be committed by `git add .` | Add `.env`, `.env.*`, `*.pem`, `*.key`, `id_rsa*`; also use a secret scanner in CI |
| INLINE_COMMENT | MEDIUM | `secrets.yml # never commit` is one pattern including the comment text, so `secrets.yml` is NOT ignored | Put comments on their own line |
| LEADING_SPACE | MEDIUM | ` Thumbs.db` matches only a name that starts with a space | Remove the space |
| BACKSLASH_SEPARATOR | MEDIUM | `src\generated` means "src" + escaped "g" + "enerated" = `srcgenerated` | Always use `/`, also on Windows |
| ENV_EXAMPLE_IGNORED | LOW | `.env*` also hides `.env.example`, the template other developers need | Add `!.env.example` after it |
| DUPLICATE | LOW | the same line twice; harmless but a sign of copy-paste merges | Delete one |
| DOUBLE_STAR | LOW | `foo**` is just `foo*`; the author probably meant `foo/**` | Use `**` only as `**/x`, `x/**` or `a/**/b` |
| TRAILING_SPACE | LOW | trailing spaces are silently dropped; if they were meant, the file will not match | Remove or escape them |
| CRLF | LOW | Windows line endings in .gitignore | Save as LF; add `* text=auto` to .gitattributes |
| ANCHORED_BY_SLASH | INFO | `src/gen` only matches next to the .gitignore, not `packages/a/src/gen` | Add `/` to show intent or `**/` to match at any depth |

## Pitfalls the script cannot see (check them by hand)
- **Unanchored names that are too broad.** `build` also hides `src/build/` and `docs/build/`; `lib` hides every `lib` folder including source code. Use `/build/` for the root output folder. Write a `- path` test for any source folder with a common name.
- **Tracked files.** Adding a rule does not untrack files already committed. Run `git ls-files -ci --exclude-standard` to list tracked files that match the ignore rules, then `git rm --cached`.
- **Nested .gitignore files.** A `.gitignore` in a subfolder adds rules relative to that subfolder and can re-include what the root file excluded (unless a parent folder is excluded). Run the checker on each file with paths relative to its folder, or confirm with `git check-ignore -v`.
- **Case sensitivity.** On macOS and Windows `core.ignorecase=true` makes `*.JPG` match `photo.jpg`; on Linux CI it does not. Write both cases or use `*.[jJ][pP][gG]`.
- **Personal files in the shared file.** `.idea/`, `.vscode/`, `*.swp`, `.DS_Store` belong in the global ignore file (`core.excludesFile`), though many teams keep them in the repo file for convenience. Do not ignore `.vscode/settings.json` if the team shares it on purpose.
- **Lock files.** Do not ignore `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `poetry.lock`, `Cargo.lock` (for applications) or `go.sum`: they make builds reproducible.
- **Ignoring then committing anyway.** `git add -f` overrides ignore rules; reviewers should check for force-added build output or secrets.
- **Other ignore files use different rules.** `.dockerignore` uses Go filepath.Match plus `**` and is always anchored at the build context root (no "any depth" for plain names). `.npmignore`, `.eslintignore` and `.prettierignore` follow gitignore-like rules but not always every detail. Test them with their own tools.

## Good starting blocks
```gitignore
# Node
node_modules/
/dist/
/build/
npm-debug.log*
.npm/

# Python
__pycache__/
*.py[cod]
.venv/
.pytest_cache/
/dist/
*.egg-info/

# Secrets
.env
.env.*
!.env.example
*.pem
*.key
id_rsa*

# Logs that keep their folder
logs/*
!logs/.gitkeep
```
FILE:templates/gitignore-review.md
# .gitignore review: <repository or folder>

**Stack:** <Node 20 + Vite / Python 3.12 / monorepo with pnpm workspaces>
**Files reviewed:** <.gitignore at root, packages/api/.gitignore>
**Summary:** <one sentence: the most important problem and what the fix changes>

## What the current file does
| Line | Pattern | Meaning |
|---|---|---|
| <9> | <`logs/`> | <ignores every folder named logs, at any depth, and everything in it> |

## Path tests
| Result | Path | Expected | Actual | Decided by |
|---|---|---|---|---|
| <FAIL> | <`logs/.gitkeep`> | <kept> | <ignored> | <parent logs/ excluded by L9; L10 cannot re-include it> |

## Findings
| # | Severity | Code | Line | Problem | Fix |
|---|---|---|---|---|---|
| 1 | <HIGH> | <NEGATION_BLOCKED> | <10> | <...> | <use logs/* instead of logs/> |

## Proposed .gitignore
```gitignore
<full fixed file, with comments on their own lines>
```

## Changes
```diff
- logs/
+ logs/*
```

## Apply and confirm
1. Replace the file, then run: `git check-ignore -v <each path from the tests>`
2. `git status --ignored --short` and check nothing important shows as `!!`
3. Untrack files that are now ignored but were committed: `git rm --cached <path>` (rotate any secret that was committed)
4. Commit: `chore: fix .gitignore (<short reason>)`

## Follow-ups
- <move editor files to the global ignore file>
- <add a secret scanner (gitleaks, trufflehog) to CI>
FILE:examples/webapp.gitignore
# Dependencies
node_modules
/vendor/

# Build output
dist/
build
*.log
logs/
!logs/.gitkeep

# Environment
.env*
config/secrets.yml # never commit

# OS and editor files
.DS_Store
.idea/
*.swp
 Thumbs.db

# Test coverage
coverage/**
!coverage/README.md

# Generated code
src\generated
*.log
FILE:examples/webapp-fixed.gitignore
# Dependencies
node_modules/
/vendor/

# Build output (root only, so src/build/ stays tracked)
/dist/
/build/
*.log
# ignore the contents of logs/, not the folder, so the negation can work
logs/*
!logs/.gitkeep

# Environment and secrets
.env
.env.*
!.env.example
*.pem
*.key
id_rsa*
# never commit
config/secrets.yml

# OS and editor files
.DS_Store
Thumbs.db
.idea/
*.swp

# Test coverage
coverage/**
!coverage/README.md

# Generated code
/src/generated/
FILE:examples/webapp-paths.txt
# Paths for examples/webapp.gitignore and webapp-fixed.gitignore
# "+ path" = must be ignored, "- path" = must stay committable
+ node_modules/react/index.js
+ packages/api/node_modules/lodash/lodash.js
+ vendor/autoload.php
- packages/vendor/README.md
+ dist/app.js
+ build/output.js
- src/build/README.md
+ logs/app.log
- logs/.gitkeep
+ .env
+ .env.local
- .env.example
+ config/secrets.yml
+ .DS_Store
+ Thumbs.db
+ coverage/lcov.info
- coverage/README.md
+ src/generated/types.ts
- src/index.ts
FILE:scripts/gitignore_check.py
#!/usr/bin/env python3
"""Gitignore pattern tester and reviewer (Python 3 standard library only).

Explains every rule in a .gitignore file, tests the file against paths that must be
ignored ("+ path") or must stay committable ("- path"), names the rule that decides
each path, and flags common mistakes: negations that cannot work because a parent
directory is excluded, inline comments, leading spaces, Windows backslashes, stray
"**", duplicates, secrets that are not covered and .env.example being ignored.

Matching follows git's documented gitignore rules for a single .gitignore at the
repository root: last matching rule wins, "!" re-includes, a trailing "/" matches
directories only, a "/" at the start or in the middle anchors the pattern to the
.gitignore's directory, "*" "?" "[...]" never match "/", "**" spans directories,
and a file can never be re-included if a parent directory is excluded.

Usage:
  python3 gitignore_check.py .gitignore [--test paths.txt] [--json]
  python3 gitignore_check.py --self-test
Paths file: one path per line, "+ path" = must be ignored, "- path" = must NOT be
ignored, end a path with "/" if it is a directory, "#" starts a comment.
Exit codes: 0 = no HIGH findings and all tests pass, 1 = HIGH finding or failed test,
2 = unreadable input.
"""
import argparse, json, re, sys

SEV_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2, "INFO": 3}
SECRET_PROBES = [".env", "config/.env.production", "certs/server.key", "deploy/id_rsa",
                 "secrets/service-account.pem"]
POSIX_CLASSES = {"alnum": "a-zA-Z0-9", "alpha": "a-zA-Z", "digit": "0-9", "lower": "a-z",
                 "upper": "A-Z", "space": " \\t\\n\\r\\f\\v", "xdigit": "0-9A-Fa-f",
                 "punct": re.escape("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"), "blank": " \\t"}


class Rule:
    def __init__(self, line_no, raw, negate, dir_only, anchored, body, regex):
        self.line_no, self.raw, self.negate, self.dir_only = line_no, raw, negate, dir_only
        self.anchored, self.body, self.regex = anchored, body, regex

    def matches(self, rel_path, is_dir):
        if self.dir_only and not is_dir:
            return False
        target = rel_path if self.anchored else rel_path.rsplit("/", 1)[-1]
        return self.regex.fullmatch(target) is not None

    def label(self):
        return f"L{self.line_no} `{self.raw}`"


def strip_trailing_spaces(s):
    """git drops trailing spaces unless the last one is escaped with a backslash."""
    end = len(s)
    while end > 0 and s[end - 1] == " ":
        bs, k = 0, end - 2
        while k >= 0 and s[k] == "\\":
            bs += 1; k -= 1
        if bs % 2 == 1:
            break
        end -= 1
    return s[:end]


def class_to_regex(body):
    out, m = "", 0
    while m < len(body):
        ch = body[m]
        if ch == "[" and body.startswith("[:", m):
            end = body.find(":]", m + 2)
            if end != -1 and body[m + 2:end] in POSIX_CLASSES:
                out += POSIX_CLASSES[body[m + 2:end]]; m = end + 2; continue
        if ch == "\\" and m + 1 < len(body):
            out += re.escape(body[m + 1]); m += 2; continue
        if ch == "-" and 0 < m < len(body) - 1:
            out += "-"; m += 1; continue
        out += re.escape(ch); m += 1
    return out


def glob_to_regex(p):
    out, i, n = "", 0, len(p)
    while i < n:
        c = p[i]
        if c == "\\":
            if i + 1 < n:
                out += re.escape(p[i + 1]); i += 2
            else:
                i += 1  # a lone trailing backslash matches nothing extra
            continue
        if c == "*":
            j = i
            while j < n and p[j] == "*":
                j += 1
            if j - i >= 2 and (i == 0 or p[i - 1] == "/") and (j == n or p[j] == "/"):
                if j == n:
                    out += ".*"; i = j
                else:
                    out += "(?:.*/)?"; i = j + 1
                continue
            out += "[^/]*"; i = j; continue
        if c == "?":
            out += "[^/]"; i += 1; continue
        if c == "[":
            j = i + 1
            neg = j < n and p[j] in "!^"
            if neg:
                j += 1
            k = j + 1 if j < n and p[j] == "]" else j
            while k < n and p[k] != "]":
                if p[k] == "[" and p.startswith("[:", k):
                    end = p.find(":]", k + 2)
                    if end != -1:
                        k = end + 2; continue
                k += 2 if p[k] == "\\" else 1
            if k >= n:
                out += re.escape("["); i += 1; continue
            cls = class_to_regex(p[j:k])
            out += ("[^/" + cls + "]") if neg else ("(?!/)[" + cls + "]")
            i = k + 1; continue
        out += re.escape(c); i += 1
    return out


def parse_rules(text):
    rules, findings, seen = [], [], {}
    for no, raw in enumerate(text.split("\n"), 1):
        if raw.endswith("\r"):
            findings.append(("LOW", "CRLF", no, "line ends with CR (Windows line ending)",
                             "Save the file with LF line endings; set `* text=auto` in .gitattributes."))
            raw = raw[:-1]
        if not raw.strip() or raw.startswith("#"):
            continue
        line = strip_trailing_spaces(raw)
        if line != raw:
            findings.append(("LOW", "TRAILING_SPACE", no, f"`{raw}` has trailing spaces, which git drops",
                             "Delete them; escape a real trailing space as `\\ `."))
        if raw[0] in " \t":
            findings.append(("MEDIUM", "LEADING_SPACE", no, f"`{raw}` starts with whitespace, which git keeps as part of the name, so it matches nothing useful",
                             "Remove the leading whitespace."))
        m = re.search(r"\s#", line)
        if m:
            findings.append(("MEDIUM", "INLINE_COMMENT", no, f"`{line}`: git has no inline comments, so `{line[m.start():].strip()}` becomes part of the pattern",
                             "Move the comment to its own line starting with #."))
        if re.search(r"\\[A-Za-z0-9_]", line):
            findings.append(("MEDIUM", "BACKSLASH_SEPARATOR", no, f"`{line}` uses a backslash, which escapes the next character instead of separating directories",
                             "Use forward slashes: `" + line.replace("\\", "/") + "`."))
        key = line.strip()
        if key in seen:
            findings.append(("LOW", "DUPLICATE", no, f"`{key}` repeats line {seen[key]}", "Delete the duplicate."))
        else:
            seen[key] = no
        negate = line.startswith("!")
        body = line[1:] if negate else line
        if body.startswith("\\!") or body.startswith("\\#"):
            body = body[1:]
        if negate and not body:
            findings.append(("LOW", "EMPTY_NEGATION", no, "`!` alone does nothing", "Delete the line."))
            continue
        dir_only = body.endswith("/") and not body.endswith("\\/")
        if dir_only:
            body = body.rstrip("/")
        if not body:
            continue
        anchored = "/" in body
        if body.startswith("/"):
            body = body[1:]
        if any((m.start() > 0 and body[m.start() - 1] != "/") or (m.end() < len(body) and body[m.end()] != "/")
               for m in re.finditer(r"\*{2,}", body)):
            findings.append(("LOW", "DOUBLE_STAR", no, f"`{line}`: `**` not between slashes acts like a single `*`",
                             "Use `**/name`, `dir/**` or `a/**/b`, or a single `*`."))
        rules.append(Rule(no, line, negate, dir_only, anchored, body, re.compile(glob_to_regex(body), re.S)))
    return rules, findings


def last_match(rules, rel_path, is_dir):
    hit = None
    for r in rules:
        if r.matches(rel_path, is_dir):
            hit = r
    return hit


def check_path(rules, path):
    """Return (ignored, reason, blocked_rule) following git's parent-directory rule."""
    is_dir = path.endswith("/")
    parts = [p for p in path.strip("/").split("/") if p]
    for k in range(1, len(parts) + 1):
        sub = "/".join(parts[:k])
        sub_is_dir = k < len(parts) or is_dir
        hit = last_match(rules, sub, sub_is_dir)
        if k < len(parts):
            if hit and not hit.negate:
                own = last_match(rules, "/".join(parts), is_dir)
                blocked = own if own and own.negate else None
                return True, f"parent `{sub}/` excluded by {hit.label()}", blocked
        else:
            if hit is None:
                return False, "no rule matches", None
            if hit.negate:
                return False, f"re-included by {hit.label()}", None
            return True, f"ignored by {hit.label()}", None
    return False, "empty path", None


def literal_dirs(body):
    parts = body.split("/")[:-1]
    out = []
    for p in parts:
        if any(ch in p for ch in "*?[\\"):
            break
        out.append(p)
    return out


def lint_negations(rules):
    findings = []
    for idx, r in enumerate(rules):
        if not r.negate or not r.anchored:
            continue
        before = rules[:idx]
        dirs = literal_dirs(r.body)
        for k in range(1, len(dirs) + 1):
            sub = "/".join(dirs[:k])
            hit = last_match(before, sub, True)
            if hit and not hit.negate:
                findings.append(("HIGH", "NEGATION_BLOCKED", r.line_no,
                                 f"`{r.raw.strip()}` can never apply: its parent directory `{sub}/` is excluded by {hit.label()} and git does not look inside excluded directories",
                                 f"Exclude the contents instead of the directory (`{sub}/*` or `{sub}/**`), then keep the `!` rule; or re-include `!{sub}/` first."))
                break
    return findings


def lint_secrets(rules):
    findings = []
    missing = [p for p in SECRET_PROBES if not check_path(rules, p)[0]]
    if missing:
        findings.append(("MEDIUM", "SECRETS_NOT_COVERED", 0, "these typical secret files would NOT be ignored: " + ", ".join(missing),
                         "Add `.env`, `.env.*` (with `!.env.example`), `*.pem`, `*.key`, `id_rsa*` if the project could ever contain them."))
    if check_path(rules, ".env.example")[0]:
        findings.append(("LOW", "ENV_EXAMPLE_IGNORED", 0, "`.env.example` is ignored, so the documented template cannot be committed",
                         "Add `!.env.example` after the .env rule."))
    return findings


def explain(rule):
    body = rule.body
    kind = "directories" if rule.dir_only else "files or directories"
    verb = "Re-includes" if rule.negate else "Ignores"
    wild = any(ch in body for ch in "*?[")
    if rule.anchored and body.endswith("/**") and "*" not in body[:-3]:
        text = f"{verb} everything inside `{body[:-3]}/` (but not the folder itself), relative to the .gitignore folder"
    elif rule.anchored and body.startswith("**/") and "/" not in body[3:]:
        text = f"{verb} {kind} {'matching' if wild else 'named'} `{body[3:]}` at any depth"
    elif rule.anchored:
        text = f"{verb} {kind} {'matching' if wild else 'at'} `{body}` relative to the .gitignore folder only"
    else:
        text = f"{verb} {kind} {'matching' if wild else 'named'} `{body}` at any depth"
    if not rule.negate and not body.endswith("/**"):
        text += "; a matched directory hides everything inside it"
    if rule.negate:
        text += " (only works if no parent directory is excluded)"
    return text + "."


def read_tests(text):
    tests = []
    for no, line in enumerate(text.splitlines(), 1):
        s = line.strip()
        if not s or s.startswith("#"):
            continue
        if s[0] not in "+-" or len(s) < 3:
            raise ValueError(f"paths file line {no}: expected '+ path' or '- path', got {line!r}")
        tests.append((s[0] == "+", s[1:].strip()))
    return tests


def review(gi_text, tests_text=None):
    rules, findings = parse_rules(gi_text)
    findings += lint_negations(rules)
    findings += lint_secrets(rules)
    anchored = [r.line_no for r in rules if r.anchored and not r.negate and "/" in r.body
                and not r.raw.startswith(("/", "**/")) and not r.body.endswith("/**")]
    if anchored:
        findings.append(("INFO", "ANCHORED_BY_SLASH", anchored[0],
                         "lines " + ", ".join(map(str, anchored)) + " contain a slash, so they only match relative to the .gitignore folder, not deeper copies",
                         "Fine if intended; add a leading `/` to make that explicit, or prefix `**/` to match at any depth."))
    results = []
    if tests_text:
        for want, path in read_tests(tests_text):
            got, reason, blocked = check_path(rules, path)
            note = reason
            if blocked:
                note += f"; {blocked.label()} cannot re-include it"
            results.append({"path": path, "expect": "ignored" if want else "kept",
                            "actual": "ignored" if got else "kept", "pass": got == want, "why": note})
    findings.sort(key=lambda f: (SEV_ORDER[f[0]], f[2]))
    return {"rules": [{"line": r.line_no, "pattern": r.raw, "meaning": explain(r)} for r in rules],
            "findings": [dict(zip(("severity", "code", "line", "problem", "fix"), f)) for f in findings],
            "tests": results}


def render(name, rep):
    out = [f"# .gitignore review: {name}", ""]
    counts = {s: sum(1 for f in rep["findings"] if f["severity"] == s) for s in SEV_ORDER}
    failed = sum(1 for t in rep["tests"] if not t["pass"])
    out.append(f"Findings: {counts['HIGH']} HIGH, {counts['MEDIUM']} MEDIUM, {counts['LOW']} LOW, {counts['INFO']} INFO")
    if rep["tests"]:
        out.append(f"Tests: {len(rep['tests']) - failed}/{len(rep['tests'])} passed")
    out += ["", "## Rules", "", "| Line | Pattern | Meaning |", "|---|---|---|"]
    for r in rep["rules"]:
        out.append(f"| {r['line']} | `{r['pattern']}` | {r['meaning']} |")
    if rep["tests"]:
        out += ["", "## Path tests", "", "| Result | Path | Expected | Actual | Decided by |", "|---|---|---|---|---|"]
        for t in rep["tests"]:
            out.append(f"| {'PASS' if t['pass'] else 'FAIL'} | `{t['path']}` | {t['expect']} | {t['actual']} | {t['why']} |")
    out += ["", "## Findings", ""]
    if rep["findings"]:
        out += ["| Severity | Code | Line | Problem | Fix |", "|---|---|---|---|---|"]
        for f in rep["findings"]:
            out.append(f"| {f['severity']} | {f['code']} | {f['line'] or '-'} | {f['problem']} | {f['fix']} |")
    else:
        out.append("No findings.")
    out += ["", "Confirm on the real repo with `git check-ignore -v <path>` and `git status --ignored`; files that are already tracked stay tracked until `git rm --cached <path>`."]
    return "\n".join(out)


def self_test():
    cases = []
    def t(name, cond):
        cases.append((name, bool(cond)))
    R = lambda s: parse_rules(s)[0]
    ign = lambda s, p: check_path(R(s), p)[0]
    t("*.log matches at any depth", ign("*.log", "a/b/c.log") and ign("*.log", "x.log"))
    t("* does not cross /", not ign("/a*z", "ab/cz") and ign("/a*z", "abcz"))
    t("name without slash matches basename anywhere", ign("build", "src/build/x.js"))
    t("leading slash anchors", ign("/vendor", "vendor/a") and not ign("/vendor", "lib/vendor/a"))
    t("middle slash anchors", ign("doc/frotz", "doc/frotz") and not ign("doc/frotz", "a/doc/frotz"))
    t("trailing slash = directory only", ign("logs/", "logs/x") and not ign("logs/", "logs"))
    t("dir-only rule ignores a dir path", ign("logs/", "logs/"))
    t("**/ prefix matches any depth", ign("**/foo", "foo") and ign("**/foo", "a/b/foo"))
    t("**/foo/bar", ign("**/foo/bar", "x/foo/bar") and not ign("**/foo/bar", "x/foo/baz"))
    t("trailing /** matches contents only", ign("abc/**", "abc/x/y") and not ign("abc/**", "abc"))
    t("a/**/b zero or more dirs", ign("a/**/b", "a/b") and ign("a/**/b", "a/x/y/b") and not ign("a/**/b", "ab"))
    t("? matches one char not /", ign("/a?c", "abc") and not ign("/a?c", "a/c"))
    t("[abc] class", ign("file[12].txt", "file1.txt") and not ign("file[12].txt", "file3.txt"))
    t("[!x] negated class", ign("f[!0-9]", "fa") and not ign("f[!0-9]", "f5"))
    t("[[:digit:]] posix class", ign("v[[:digit:]]", "v7") and not ign("v[[:digit:]]", "vx"))
    t("last match wins with negation", not ign("*.txt\n!keep.txt", "keep.txt") and ign("*.txt\n!keep.txt", "a.txt"))
    t("order matters", ign("!keep.txt\n*.txt", "keep.txt"))
    t("parent dir excluded blocks negation", ign("logs/\n!logs/.gitkeep", "logs/.gitkeep"))
    t("dir/* allows negation", not ign("logs/*\n!logs/.gitkeep", "logs/.gitkeep"))
    t("dir/** allows negation", not ign("coverage/**\n!coverage/README.md", "coverage/README.md"))
    t("re-include dir then file", not ign("/a/*\n!/a/b/\n/a/b/*\n!/a/b/c", "a/b/c"))
    t("escaped # and !", ign("\\#notes", "#notes") and ign("\\!bang", "!bang"))
    t("comment lines skipped", not ign("# *.log", "a.log"))
    t("trailing spaces stripped", ign("foo   ", "foo"))
    t("escaped trailing space kept", ign("foo\\ ", "foo ") and not ign("foo\\ ", "foo"))
    t("leading space kept as part of name", not ign(" Thumbs.db", "Thumbs.db"))
    t("inline comment becomes pattern", not ign("secrets.yml # x", "secrets.yml"))
    t("backslash escapes, not a separator", not ign("src\\gen", "src/gen/a.ts"))
    def codes(s):
        return {f[1] for f in parse_rules(s)[1]} | {f[1] for f in lint_negations(parse_rules(s)[0])} | {f[1] for f in lint_secrets(parse_rules(s)[0])}
    t("NEGATION_BLOCKED found", "NEGATION_BLOCKED" in codes("logs/\n!logs/.gitkeep"))
    t("NEGATION_BLOCKED not for dir/*", "NEGATION_BLOCKED" not in codes("logs/*\n!logs/.gitkeep"))
    t("NEGATION_BLOCKED via dir/* deeper", "NEGATION_BLOCKED" in codes("a/*\n!a/b/c.txt"))
    t("INLINE_COMMENT found", "INLINE_COMMENT" in codes("x.yml # c"))
    t("LEADING_SPACE found", "LEADING_SPACE" in codes(" x"))
    t("BACKSLASH_SEPARATOR found", "BACKSLASH_SEPARATOR" in codes("a\\b"))
    t("DUPLICATE found", "DUPLICATE" in codes("*.log\n*.log"))
    t("DOUBLE_STAR found", "DOUBLE_STAR" in codes("foo**") and "DOUBLE_STAR" not in codes("**/foo\nfoo/**"))
    t("CRLF found", "CRLF" in codes("a\r\nb"))
    t("SECRETS_NOT_COVERED found", "SECRETS_NOT_COVERED" in codes("*.log"))
    t("secrets covered", "SECRETS_NOT_COVERED" not in codes(".env\n.env.*\n!.env.example\n*.key\n*.pem\nid_rsa*"))
    t("ENV_EXAMPLE_IGNORED found", "ENV_EXAMPLE_IGNORED" in codes(".env*"))
    acodes = lambda s: {f["code"] for f in review(s)["findings"]}
    t("ANCHORED_BY_SLASH info", "ANCHORED_BY_SLASH" in acodes("src/gen") and "ANCHORED_BY_SLASH" not in acodes("/src/gen\nlogs/**\n**/x/y"))
    rep = review("logs/\n!logs/.gitkeep\n", "+ logs/a.log\n- logs/.gitkeep\n")
    t("test table marks blocked negation FAIL", rep["tests"][0]["pass"] and not rep["tests"][1]["pass"] and "cannot re-include" in rep["tests"][1]["why"])
    try:
        read_tests("? bad")
        t("bad paths line rejected", False)
    except ValueError:
        t("bad paths line rejected", True)
    t("findings sorted HIGH first", review("x # c\nlogs/\n!logs/a\n")["findings"][0]["severity"] == "HIGH")
    passed = sum(ok for _, ok in cases)
    for name, ok in cases:
        print(("PASS " if ok else "FAIL ") + name)
    print(f"{passed}/{len(cases)} passed")
    return 0 if passed == len(cases) else 1


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("gitignore", nargs="?")
    ap.add_argument("--test", help="paths file with '+ path' (must be ignored) and '- path' (must be kept)")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args()
    if a.self_test:
        return self_test()
    if not a.gitignore:
        ap.print_usage(sys.stderr); return 2
    try:
        gi = open(a.gitignore, encoding="utf-8", newline="").read()
        tests = open(a.test, encoding="utf-8").read() if a.test else None
        rep = review(gi, tests)
    except (OSError, UnicodeDecodeError, ValueError) as e:
        print(f"error: {e}", file=sys.stderr); return 2
    print(json.dumps(rep, indent=2) if a.json else render(a.gitignore, rep))
    bad = any(f["severity"] == "HIGH" for f in rep["findings"]) or any(not t["pass"] for t in rep["tests"])
    return 1 if bad else 0


if __name__ == "__main__":
    sys.exit(main())
Best PracticesPythonDebugging+2
F@f
0
Background Jobs taste
Text

Opinionated conventions for building reliable background job queues and workers.

# Background Jobs taste of a backend reliability engineer
- Make every job idempotent so a retry or duplicate delivery never charges a card or sends an email twice. Confidence: 0.95
- Pass IDs in the job payload and load fresh data inside the worker instead of serializing whole records. Confidence: 0.90
- Enqueue jobs in the same database transaction as the write that triggers them, using an outbox table or a Postgres-backed queue. Confidence: 0.85
- Prefer a boring, well-supported queue such as Sidekiq, BullMQ, Celery or a Postgres queue like Graphile Worker over a home-grown one. Confidence: 0.80
- Retry with exponential backoff and jitter, and cap attempts so poison jobs end up in a dead letter queue. Confidence: 0.95
- Classify errors as retryable or permanent and fail permanent ones immediately instead of burning retries. Confidence: 0.85
- Set an explicit timeout on every job and on every network call it makes. Confidence: 0.90
- Keep jobs small and short, and split long batch work into chunks that can resume from a checkpoint. Confidence: 0.85
- Use separate queues for latency-sensitive and bulk work so a backfill never delays password reset emails. Confidence: 0.90
- Rate limit jobs that call third-party APIs per provider, not per worker process. Confidence: 0.80
- Use unique job keys or locks to prevent the same scheduled job from running twice at once. Confidence: 0.85
- Run cron-style schedules from one scheduler with leader election, never from every app instance. Confidence: 0.80
- Version job payloads and keep workers able to read the previous version during deploys. Confidence: 0.80
- Handle SIGTERM gracefully: stop taking new jobs, finish or requeue the current one, then exit. Confidence: 0.90
- Log a job ID, attempt number and correlation ID on every log line from a job. Confidence: 0.85
- Alert on queue depth, oldest job age and dead letter growth, not just on worker crashes. Confidence: 0.90
- Give the dead letter queue an owner and a documented replay command. Confidence: 0.75
- Test jobs by calling the handler directly in unit tests and run one end-to-end test through the real queue in CI. Confidence: 0.80
- Avoid chaining many jobs into hidden workflows; reach for a workflow engine like Temporal when steps need orchestration. Confidence: 0.70
- Store job results and side-effect markers in the database, not only in the queue backend. Confidence: 0.75
BackendArchitectureBest Practices+2
F@f
0
Previous2 / 2