From 7dee24d68d3301b3bd62468b102c70f7f322a2dc Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 31 May 2026 22:26:55 -0600
Subject: [PATCH 01/55] Add redaction-first log import
---
README.md | 4 ++
docs/commands.md | 2 +-
docs/real-log-dogfood.md | 112 ++++++++++++++++++++++++++++++++++++++
redline/cli.py | 14 +++++
redline/import_logs.py | 10 ++++
redline/mcp.py | 4 ++
tests/test_cli_config.py | 53 ++++++++++++++++++
tests/test_import_logs.py | 55 +++++++++++++++++++
tests/test_mcp.py | 1 +
tests/test_packaging.py | 2 +
10 files changed, 256 insertions(+), 1 deletion(-)
create mode 100644 docs/real-log-dogfood.md
diff --git a/README.md b/README.md
index 220f766..7dba6bb 100644
--- a/README.md
+++ b/README.md
@@ -224,6 +224,9 @@ redline is built around the full prompt-regression loop:
- `redline watch`: collect prompt-response observations from logs, Python
functions, OpenAI/Anthropic-compatible SDK calls, or ASGI apps, with
best-effort common secrets and PII redacted before write by default.
+- `redline import`: normalize exported team logs into redline JSONL, with the
+ same best-effort redaction enabled by default. Use `--no-redact` only for
+ reviewed local-only logs.
- `RedlineMiddleware`: capture bounded JSON FastAPI or ASGI request/response pairs locally, with optional skip diagnostics.
- `redline redact --check`: scan logs for common secrets and PII, then write a scrubbed copy when needed.
Redaction is best-effort pattern matching, not a privacy boundary; review sensitive logs before sharing.
@@ -477,6 +480,7 @@ bash scripts/release_check.sh
- [docs/launch.md](docs/launch.md): public alpha launch plan
- [docs/troubleshooting.md](docs/troubleshooting.md): first-run and CI failure recovery
- [docs/commands.md](docs/commands.md): compact CLI command reference
+- [docs/real-log-dogfood.md](docs/real-log-dogfood.md): redaction-first real-log test protocol
- [docs/dogfood.md](docs/dogfood.md): first-user dogfood protocol
- [docs/case-studies.md](docs/case-studies.md): reproducible dogfood case studies
- [docs/internet-dogfood-sources.md](docs/internet-dogfood-sources.md): public prompt-response datasets for dogfood sourcing
diff --git a/docs/commands.md b/docs/commands.md
index 58a23ce..01c1c1d 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -10,7 +10,7 @@ output and examples after installing.
| `redline init` | Write `redline.json`, runner files, and optional CI workflow. | `--runner`, `--copy-runner`, `--github-action`, `--force` |
| `redline doctor` | Check config, suite, replay, reports, audit, and team workflow. | `--strict`, `--json` |
| `redline watch` | Collect prompt-response observations or print snippets. | `--log`, `--stats`, `--snippet`, `--follow` |
-| `redline import` | Normalize exported JSONL fields into redline `prompt`/`response` logs. | `--input-field`, `--output-field`, `--context-field`, `--metadata-field`, `--limit`, `--out` |
+| `redline import` | Normalize exported JSONL fields into redline `prompt`/`response` logs, with best-effort redaction on by default. | `--input-field`, `--output-field`, `--context-field`, `--metadata-field`, `--limit`, `--out`, `--no-redact` |
| `redline redact` | Check or write best-effort redacted logs. | `--check`, `--out`, `--json` |
| `redline cluster` | Inspect behavior groups before suite generation. | `--max-cases`, `--json` |
| `redline suite` | Generate a suite from baseline JSONL logs. | `--out`, `--max-cases`, `--all-cases`, `--owner` |
diff --git a/docs/real-log-dogfood.md b/docs/real-log-dogfood.md
new file mode 100644
index 0000000..5a4b385
--- /dev/null
+++ b/docs/real-log-dogfood.md
@@ -0,0 +1,112 @@
+# Real Log Dogfood
+
+Use this protocol when testing redline on real prompt/output logs from a team,
+agent, support workflow, extraction job, or public dataset. The goal is to learn
+whether redline catches useful regressions without leaking private data.
+
+## Rule Zero
+
+Do not send raw customer logs, secrets, API keys, or private prompts to another
+person. Keep the raw export local. Share only sanitized reports or short
+hand-redacted examples.
+
+## 1. Inspect the Export
+
+Open a few rows before importing:
+
+```bash
+python -m json.tool < first-row.json
+```
+
+Find the field that contains the user prompt or instruction, and the field that
+contains the model or agent response. Common examples:
+
+| Source shape | Input field | Output field |
+| --- | --- | --- |
+| redline JSONL | `prompt` | `response` |
+| Dolly-style public data | `instruction` | `response` |
+| OpenAI wrapper log | `request.messages` | `response.output_text` |
+| Langfuse/Helicone-style export | `input` | `output` |
+| Support traces | `ticket.text` | `assistant.reply` |
+
+## 2. Import With Redaction On
+
+`redline import` redacts common secrets and PII by default before writing the
+normalized baseline file.
+
+```bash
+redline import raw-export.jsonl \
+ --input-field instruction \
+ --output-field response \
+ --metadata-field category \
+ --out .redline/dogfood/baseline.jsonl
+```
+
+The command prints how many values were redacted. If the count is non-zero,
+inspect the normalized output before generating a suite.
+
+Use `--no-redact` only for local-only test fixtures that have already been
+reviewed.
+
+## 3. Generate a Suite
+
+```bash
+redline suite .redline/dogfood/baseline.jsonl \
+ --out .redline/dogfood/suite.json
+```
+
+Then inspect the selected cases:
+
+```bash
+redline cases .redline/dogfood/suite.json
+redline summary .redline/dogfood/suite.json
+```
+
+## 4. Compare a Candidate
+
+If you already have candidate outputs:
+
+```bash
+redline diff .redline/dogfood/suite.json .redline/dogfood/candidate.jsonl \
+ --compact \
+ --out-json .redline/dogfood/diff.json \
+ --out-html .redline/dogfood/diff.html \
+ --fail-on none
+```
+
+If you have a runner configured:
+
+```bash
+redline eval .redline/dogfood/suite.json \
+ --prompt prompts/v2.txt \
+ --compact \
+ --fail-on none
+```
+
+## 5. Report Back
+
+For early dogfood, collect this information:
+
+- Source type: public dataset, app logs, trace export, SDK capture, or manual JSONL.
+- Rows imported and redaction count.
+- Suite cases generated and behavior groups covered.
+- Blocking regressions found, if any.
+- False positives that felt noisy.
+- First command or error message that caused confusion.
+- Whether the result would have changed a shipping decision.
+
+Do not share raw logs. Share the redline report only after reviewing it for
+private content.
+
+## What Good Looks Like
+
+A useful dogfood run does not need to prove semantic correctness. It should show
+whether redline caught concrete structural drift:
+
+- JSON keys disappeared.
+- A table became prose.
+- A safe task became a refusal.
+- Required numbers, URLs, owners, or action items were dropped.
+- A pinned requirement failed.
+
+Those are the regressions redline is designed to catch quickly and locally.
diff --git a/redline/cli.py b/redline/cli.py
index ce803f5..cbbb3e3 100644
--- a/redline/cli.py
+++ b/redline/cli.py
@@ -251,6 +251,8 @@ def build_parser() -> argparse.ArgumentParser:
)
import_parser.add_argument("--limit", type=int, help="maximum records to import")
import_parser.add_argument("--out", required=True, help="redline JSONL output path")
+ import_parser.add_argument("--no-redact", action="store_true", help="write raw values without import redaction")
+ import_parser.add_argument("--redaction-placeholder", default=DEFAULT_PLACEHOLDER, help="replacement text for import redaction")
import_parser.add_argument("--json", action="store_true", help="print machine-readable JSON")
import_parser.set_defaults(func=cmd_import)
@@ -738,6 +740,8 @@ def cmd_import(args: argparse.Namespace) -> int:
id_field=args.id_field,
metadata_fields=args.metadata_field,
limit=args.limit,
+ redact=not args.no_redact,
+ placeholder=args.redaction_placeholder,
)
if args.json:
print(json.dumps(report, indent=2, sort_keys=True))
@@ -749,6 +753,16 @@ def cmd_import(args: argparse.Namespace) -> int:
print(f"Appended context: {report['context_field']}")
if report["metadata_fields"]:
print(f"Copied metadata: {', '.join(report['metadata_fields'])}")
+ if report["redacted"]:
+ print(
+ "Redaction: "
+ f"best-effort common secrets/PII scanned; {report['redactions']} value(s) redacted"
+ )
+ patterns = report.get("redaction_patterns")
+ if isinstance(patterns, dict) and patterns:
+ print(f"Redaction hits: {', '.join(f'{key}={value}' for key, value in patterns.items())}")
+ else:
+ print("Redaction: disabled; review sensitive logs before sharing or committing")
print(f"Wrote {Path(str(report['output']))}.")
print()
print("Next:")
diff --git a/redline/import_logs.py b/redline/import_logs.py
index 2baa500..bbaba31 100644
--- a/redline/import_logs.py
+++ b/redline/import_logs.py
@@ -5,6 +5,7 @@
from typing import Any
from .io import iter_jsonl, write_jsonl
+from .redact import DEFAULT_PLACEHOLDER, redact_object
_MISSING = object()
@@ -20,15 +21,20 @@ def import_jsonl_log(
id_field: str | None = None,
metadata_fields: list[str] | None = None,
limit: int | None = None,
+ redact: bool = True,
+ placeholder: str = DEFAULT_PLACEHOLDER,
) -> dict[str, Any]:
if limit is not None and limit < 1:
raise ValueError("--limit must be 1 or greater")
metadata_paths = metadata_fields or []
rows: list[dict[str, Any]] = []
+ redaction_counts: dict[str, int] = {}
source = Path(path)
for line_number, row in iter_jsonl(source):
if limit is not None and len(rows) >= limit:
break
+ if redact:
+ row = redact_object(row, placeholder=placeholder, counts=redaction_counts)
prompt = _required_field(source, line_number, row, input_field, "input")
response = _required_field(source, line_number, row, output_field, "output")
imported: dict[str, Any] = {
@@ -50,6 +56,7 @@ def import_jsonl_log(
if not rows:
raise ValueError(f"{path} contains no JSONL records")
write_jsonl(output, rows)
+ redactions = sum(redaction_counts.values())
return {
"source": str(path),
"output": str(output),
@@ -59,6 +66,9 @@ def import_jsonl_log(
"context_field": context_field or "",
"id_field": id_field or "",
"metadata_fields": metadata_paths,
+ "redacted": redact,
+ "redactions": redactions,
+ "redaction_patterns": dict(sorted(redaction_counts.items())),
}
diff --git a/redline/mcp.py b/redline/mcp.py
index 93e89fb..ed70db8 100644
--- a/redline/mcp.py
+++ b/redline/mcp.py
@@ -541,6 +541,8 @@ def _tools() -> list[ToolSpec]:
"description": "Source field paths copied into metadata.",
},
"limit": _integer("Maximum records to import."),
+ "no_redact": _boolean("Write raw values without import redaction."),
+ "redaction_placeholder": _string("Replacement text for import redaction."),
"json": _boolean("Print machine-readable JSON."),
},
required=("path", "out"),
@@ -890,6 +892,8 @@ def _build_import(arguments: dict[str, Any]) -> list[str]:
_add_option(args, "--id-field", arguments.get("id_field"))
_add_repeated_options(args, "--metadata-field", arguments.get("metadata_fields"))
_add_option(args, "--limit", arguments.get("limit"))
+ _add_flag(args, "--no-redact", arguments.get("no_redact"))
+ _add_option(args, "--redaction-placeholder", arguments.get("redaction_placeholder"))
_add_flag(args, "--json", arguments.get("json"))
return args
diff --git a/tests/test_cli_config.py b/tests/test_cli_config.py
index 3d2097b..54763dd 100644
--- a/tests/test_cli_config.py
+++ b/tests/test_cli_config.py
@@ -2081,6 +2081,59 @@ def test_watch_reports_default_redaction_and_supports_raw_opt_out(self) -> None:
finally:
os.chdir(previous)
+ def test_import_reports_default_redaction_and_supports_raw_opt_out(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ previous = Path.cwd()
+ os.chdir(root)
+ try:
+ Path("downloaded.jsonl").write_text(
+ '{"instruction": "Email ada@example.com", "response": "ok"}\n',
+ encoding="utf-8",
+ )
+
+ output = io.StringIO()
+ with contextlib.redirect_stdout(output):
+ self.assertEqual(
+ main(
+ [
+ "import",
+ "downloaded.jsonl",
+ "--input-field",
+ "instruction",
+ "--output-field",
+ "response",
+ "--out",
+ "baseline.jsonl",
+ ]
+ ),
+ 0,
+ )
+
+ self.assertIn("1 value(s) redacted", output.getvalue())
+ self.assertNotIn("ada@example.com", Path("baseline.jsonl").read_text(encoding="utf-8"))
+
+ with contextlib.redirect_stdout(io.StringIO()):
+ self.assertEqual(
+ main(
+ [
+ "import",
+ "downloaded.jsonl",
+ "--input-field",
+ "instruction",
+ "--output-field",
+ "response",
+ "--out",
+ "raw.jsonl",
+ "--no-redact",
+ ]
+ ),
+ 0,
+ )
+ self.assertIn("ada@example.com", Path("raw.jsonl").read_text(encoding="utf-8"))
+ finally:
+ os.chdir(previous)
+
def test_eval_uses_configured_replay_command(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
diff --git a/tests/test_import_logs.py b/tests/test_import_logs.py
index b9e08d2..9f6172e 100644
--- a/tests/test_import_logs.py
+++ b/tests/test_import_logs.py
@@ -68,6 +68,61 @@ def test_import_jsonl_log_limits_records(self) -> None:
self.assertEqual(report["records"], 1)
self.assertEqual(len(read_jsonl_records(output, "prompt", "response")), 1)
+ def test_import_jsonl_log_redacts_common_secrets_by_default(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ source = root / "downloaded.jsonl"
+ output = root / "baseline.jsonl"
+ source.write_text(
+ json.dumps(
+ {
+ "instruction": "Email ada@example.com with the result.",
+ "response": "Used key sk-ant-" + ("a" * 24),
+ "metadata": {"api_key": "secret-value"},
+ }
+ )
+ + "\n",
+ encoding="utf-8",
+ )
+
+ report = import_jsonl_log(
+ source,
+ output=output,
+ input_field="instruction",
+ output_field="response",
+ metadata_fields=["metadata"],
+ )
+
+ self.assertTrue(report["redacted"])
+ self.assertEqual(report["redactions"], 3)
+ records = read_jsonl_records(output, "prompt", "response")
+ self.assertNotIn("ada@example.com", records[0].prompt)
+ self.assertNotIn("sk-ant-", records[0].response)
+ self.assertEqual(records[0].raw["metadata"]["metadata"]["api_key"], "[REDACTED]")
+
+ def test_import_jsonl_log_can_disable_redaction_for_local_only_logs(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ source = root / "downloaded.jsonl"
+ output = root / "baseline.jsonl"
+ source.write_text(
+ '{"instruction": "Email ada@example.com", "response": "ok"}\n',
+ encoding="utf-8",
+ )
+
+ report = import_jsonl_log(
+ source,
+ output=output,
+ input_field="instruction",
+ output_field="response",
+ redact=False,
+ )
+
+ self.assertFalse(report["redacted"])
+ self.assertEqual(report["redactions"], 0)
+ records = read_jsonl_records(output, "prompt", "response")
+ self.assertIn("ada@example.com", records[0].prompt)
+
def test_import_jsonl_log_reports_missing_fields(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
diff --git a/tests/test_mcp.py b/tests/test_mcp.py
index 5e2c8af..385b2fa 100644
--- a/tests/test_mcp.py
+++ b/tests/test_mcp.py
@@ -77,6 +77,7 @@ def test_import_tool_normalizes_external_jsonl(self) -> None:
self.assertFalse(result["isError"])
self.assertEqual(result["structuredContent"]["exit_code"], 0)
self.assertEqual(result["structuredContent"]["json"]["records"], 1)
+ self.assertTrue(result["structuredContent"]["json"]["redacted"])
self.assertTrue(wrote_output)
def test_eval_and_diff_tools_do_not_accept_dynamic_commands(self) -> None:
diff --git a/tests/test_packaging.py b/tests/test_packaging.py
index 72c270c..eb1a454 100644
--- a/tests/test_packaging.py
+++ b/tests/test_packaging.py
@@ -238,6 +238,7 @@ def test_readme_marks_repo_only_script_commands(self) -> None:
self.assertIn("zero runtime", readme)
self.assertIn("docs/troubleshooting.md", readme)
self.assertIn("docs/commands.md", readme)
+ self.assertIn("docs/real-log-dogfood.md", readme)
self.assertIn("redline judges", readme)
self.assertIn("feature-level", readme)
self.assertIn("From a repo checkout, record the public demo", readme)
@@ -284,6 +285,7 @@ def test_troubleshooting_and_command_reference_are_public_docs(self) -> None:
self.assertIn("Neutral does not mean safe", troubleshooting)
self.assertIn("| `redline eval` |", commands)
self.assertIn("| `redline import` |", commands)
+ self.assertIn("--no-redact", commands)
self.assertIn("| `redline dashboard` |", commands)
self.assertIn("| `redline-mcp` |", commands)
self.assertIn("Default fail-on statuses", commands)
From 4383556d3bc44400b4c87a1c1dc2c4d16946e3e5 Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 31 May 2026 22:31:58 -0600
Subject: [PATCH 02/55] Clarify behavior grouping language
---
README.md | 13 +++++++------
docs/ai-session-dogfood-prompts.jsonl | 2 +-
docs/commands.md | 2 +-
docs/launch.md | 4 ++--
docs/runners.md | 2 +-
redline/benchmark.py | 4 ++--
redline/cli.py | 4 ++--
redline/clusters.py | 8 ++++----
redline/dashboard.py | 2 +-
redline/doctor.py | 4 ++--
redline/history.py | 4 ++--
redline/mcp.py | 4 ++--
redline/summary.py | 22 +++++++++++-----------
tests/test_clusters.py | 6 +++---
tests/test_dashboard.py | 2 +-
tests/test_doctor.py | 2 +-
tests/test_history.py | 2 +-
tests/test_summary.py | 10 +++++-----
18 files changed, 49 insertions(+), 48 deletions(-)
diff --git a/README.md b/README.md
index 7dba6bb..a031aa1 100644
--- a/README.md
+++ b/README.md
@@ -200,10 +200,11 @@ redline is deterministic and local-first by default. Optional judge commands are
available for ambiguous `changed` cases, but redline does not call a cloud model
unless you explicitly configure that command.
-Suite generation groups logs by deterministic behavior signatures, not opaque
-embedding clusters. It picks one representative per group first, then adds
-high-variance edges and evenly spread prompt-diverse samples from large clusters
-when the case budget allows.
+Suite generation does not run statistical or embedding clustering by default.
+It groups logs by deterministic behavior signatures, such as prompt intent,
+response shape, length bucket, and JSON schema. It picks one representative per
+group first, then adds high-variance edges and evenly spread prompt-diverse
+samples from large groups when the case budget allows.
## Trust Boundary
@@ -230,7 +231,7 @@ redline is built around the full prompt-regression loop:
- `RedlineMiddleware`: capture bounded JSON FastAPI or ASGI request/response pairs locally, with optional skip diagnostics.
- `redline redact --check`: scan logs for common secrets and PII, then write a scrubbed copy when needed.
Redaction is best-effort pattern matching, not a privacy boundary; review sensitive logs before sharing.
-- `redline cluster`: inspect behavior groups before suite generation.
+- `redline cluster`: inspect deterministic behavior-signature groups before suite generation.
- `redline suite`: generate a representative eval suite from baseline logs.
- `redline prompts`: scan many prompt files and write or check a versionable prompt-to-suite manifest.
Add `--check-suites` in CI when every prompt should already have a built and valid suite.
@@ -427,7 +428,7 @@ redline summary redline-suite.json
`doctor` shows whether the suite has explicit requirements or recorded
judgments before you rely on structural checks in CI.
-`summary` reports cluster/case coverage, owner coverage, accepted baseline
+`summary` reports behavior-group/case coverage, owner coverage, accepted baseline
history, approver coverage, and explicit guard coverage for cases with
requirements or recorded judgments so teams can review suite readiness before CI.
`dashboard` also shows audit checkpoint evidence when `.redline/audit-checkpoint.json`
diff --git a/docs/ai-session-dogfood-prompts.jsonl b/docs/ai-session-dogfood-prompts.jsonl
index e75ab61..ad0534b 100644
--- a/docs/ai-session-dogfood-prompts.jsonl
+++ b/docs/ai-session-dogfood-prompts.jsonl
@@ -2,7 +2,7 @@
{"case_id":"task_02","prompt":"Design a first-run experience for redline. A new user cloned the repo and has five minutes. What exact commands should they run, what should they see, and where are they most likely to get confused?"}
{"case_id":"task_03","prompt":"Write a concise config reference for redline.json. Include fields for suite path, input/output fields, max cases, replay command, judge command, reports, logs, and fail-on behavior."}
{"case_id":"task_04","prompt":"Create a runner adapter guide for a team using an HTTP API. Their endpoint accepts {\"prompt\":\"...\"} and returns {\"response\":\"...\"}. Show the script, environment variables, and redline command."}
-{"case_id":"task_05","prompt":"Review this CLI output and tell me whether it is clear enough for a first-time user. Suggest exact wording improvements:\nGenerated 5 cases from 5 records.\nDetected 5 behavioral clusters.\nWrote redline-suite.json."}
+{"case_id":"task_05","prompt":"Review this CLI output and tell me whether it is clear enough for a first-time user. Suggest exact wording improvements:\nGenerated 5 cases from 5 records.\nDetected 5 behavior-signature groups.\nWrote redline-suite.json."}
{"case_id":"task_06","prompt":"I changed an LLM system prompt and now responses are shorter. Give me a realistic before/after prompt-output log with 5 JSONL rows where the shorter candidate silently loses important behavior."}
{"case_id":"task_07","prompt":"Act as a code reviewer for a Python CLI eval tool. What are the top 10 bugs or product risks you would look for before calling it public alpha?"}
{"case_id":"task_08","prompt":"Write a GitHub Actions workflow for redline. It should install the package, run doctor, validate the suite, run eval, upload reports, and fail on regressions."}
diff --git a/docs/commands.md b/docs/commands.md
index 01c1c1d..25d7e19 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -12,7 +12,7 @@ output and examples after installing.
| `redline watch` | Collect prompt-response observations or print snippets. | `--log`, `--stats`, `--snippet`, `--follow` |
| `redline import` | Normalize exported JSONL fields into redline `prompt`/`response` logs, with best-effort redaction on by default. | `--input-field`, `--output-field`, `--context-field`, `--metadata-field`, `--limit`, `--out`, `--no-redact` |
| `redline redact` | Check or write best-effort redacted logs. | `--check`, `--out`, `--json` |
-| `redline cluster` | Inspect behavior groups before suite generation. | `--max-cases`, `--json` |
+| `redline cluster` | Inspect deterministic behavior-signature groups before suite generation. | `--max-cases`, `--json` |
| `redline suite` | Generate a suite from baseline JSONL logs. | `--out`, `--max-cases`, `--all-cases`, `--owner` |
| `redline suite add` | Pin a hand-picked edge case. | `--prompt`, `--response`, `--include`, `--exclude`, `--owner` |
| `redline cases` | List generated suite case IDs and coverage. | `--json` |
diff --git a/docs/launch.md b/docs/launch.md
index b8da4dc..8b7bbd4 100644
--- a/docs/launch.md
+++ b/docs/launch.md
@@ -101,7 +101,7 @@ Short post:
I built redline, a local-first tool that turns prompt-response logs into eval
suites automatically.
-You point it at existing JSONL logs, it clusters behavior, picks representative
+You point it at existing JSONL logs, groups behavior by deterministic signatures, picks representative
cases, and catches regressions when a prompt change drops structure, numbers,
URLs, entities, refusals, tables, code blocks, or required fields.
@@ -123,7 +123,7 @@ Longer post:
Most eval tools start with "write test cases." redline starts from the logs you
already have.
-It watches or imports prompt-response JSONL, clusters observed behavior, builds
+It watches or imports prompt-response JSONL, groups observed behavior by deterministic signatures, builds
a representative suite, and compares new prompt runs against the accepted
baseline. The default checks are deterministic and local: JSON validity, missing
keys, empty outputs, refusals, URLs, numbers, entities, code blocks, tables,
diff --git a/docs/runners.md b/docs/runners.md
index cb30b24..a6187a2 100644
--- a/docs/runners.md
+++ b/docs/runners.md
@@ -247,7 +247,7 @@ python runners/braintrust_suite_export.py redline-suite.json \
```
What it does: writes one JSONL row per redline case with `input`, `expected`,
-and redline metadata such as case ID, cluster, owner, and source line. Import
+and redline metadata such as case ID, behavior signature, owner, and source line. Import
that JSONL into Braintrust when you want Braintrust to mirror the same dataset
redline gates locally.
diff --git a/redline/benchmark.py b/redline/benchmark.py
index e479a93..1d7b9bd 100644
--- a/redline/benchmark.py
+++ b/redline/benchmark.py
@@ -186,7 +186,7 @@ def format_benchmark_report(report: dict[str, Any], *, command_name: str = "benc
"Mode: static estimate; no replay commands are executed",
f"{suite_label + ':':<22}{report['suite']}",
f"Cases: {report['cases']}",
- f"Behavioral clusters: {report['clusters']}",
+ f"Behavior groups: {report['clusters']}",
f"Records seen: {report['records_seen']}",
f"Workers: {report['workers']}",
f"Timeout per case: {_seconds(report['timeout_seconds'])}",
@@ -250,7 +250,7 @@ def format_benchmark_markdown(report: dict[str, Any], *, command_name: str = "be
"| --- | --- |",
f"| {'Prompt manifest' if report.get('is_prompt_manifest') else 'Suite'} | `{report['suite']}` |",
f"| Cases | {report['cases']} |",
- f"| Behavioral clusters | {report['clusters']} |",
+ f"| Behavior groups | {report['clusters']} |",
f"| Workers | {report['workers']} |",
f"| Timeout per case | {_seconds(report['timeout_seconds'])} |",
f"| Worst-case eval budget | {_duration(report['worst_case_seconds'])} |",
diff --git a/redline/cli.py b/redline/cli.py
index cbbb3e3..26e7ff9 100644
--- a/redline/cli.py
+++ b/redline/cli.py
@@ -322,7 +322,7 @@ def build_parser() -> argparse.ArgumentParser:
audit_parser.add_argument("--json", action="store_true", help="print machine-readable JSON")
audit_parser.set_defaults(func=cmd_audit)
- cluster_parser = subparsers.add_parser("cluster", help="analyze behavioral clusters in a log")
+ cluster_parser = subparsers.add_parser("cluster", help="inspect deterministic behavior-signature groups in a log")
cluster_parser.add_argument("log", nargs="?", help="JSONL prompt-response log; defaults to watched log")
cluster_parser.add_argument("--config", default=DEFAULT_CONFIG_PATH, help="config path to read")
cluster_parser.add_argument("--input-field", help="JSONL input field")
@@ -1026,7 +1026,7 @@ def cmd_suite(args: argparse.Namespace) -> int:
print(f"Generated {summary['cases']} cases from {summary['records_seen']} records.")
if summary.get("duplicate_prompt_response_pairs"):
print(f"Skipped {summary['duplicate_prompt_response_pairs']} duplicate prompt-response pairs.")
- print(f"Detected {summary['clusters']} behavioral clusters.")
+ print(f"Detected {summary['clusters']} behavior-signature groups.")
if summary.get("selection") != "all":
unique_pairs = int(summary.get("unique_prompt_response_pairs", summary["records_seen"]))
cases = int(summary["cases"])
diff --git a/redline/clusters.py b/redline/clusters.py
index e3caa31..8335481 100644
--- a/redline/clusters.py
+++ b/redline/clusters.py
@@ -46,11 +46,11 @@ def format_cluster_report(suite: dict[str, Any]) -> str:
lines = [
"redline cluster",
"",
- f"Identified {report['clusters']} behavioral clusters from {report['unique_prompt_response_pairs']} unique pairs.",
+ f"Identified {report['clusters']} behavior-signature groups from {report['unique_prompt_response_pairs']} unique pairs.",
f"Records seen: {report['records_seen']} Duplicate pairs: {report['duplicate_prompt_response_pairs']}",
- f"High-risk clusters: {report['high_risk_clusters']}",
- f"High-variance clusters: {report['high_variance_clusters']}",
- f"Failure-pattern clusters: {report['failure_pattern_clusters']}",
+ f"High-risk groups: {report['high_risk_clusters']}",
+ f"High-variance groups: {report['high_variance_clusters']}",
+ f"Failure-pattern groups: {report['failure_pattern_clusters']}",
f"Suggested eval suite: {report['suggested_cases']} representative cases.",
f"Case coverage: {report['suggested_cases']}/{report['unique_prompt_response_pairs']} ({_percent(report['case_coverage'])})",
"",
diff --git a/redline/dashboard.py b/redline/dashboard.py
index d9e673f..e5731fd 100644
--- a/redline/dashboard.py
+++ b/redline/dashboard.py
@@ -882,7 +882,7 @@ def _trend_cluster_diagnosis(value: Any) -> str:
)
if not rows:
return ""
- return "
Cluster diagnosis
" + "".join(rows) + "
"
+ return "
Behavior group diagnosis
" + "".join(rows) + "
"
def _trend_class(direction: str) -> str:
diff --git a/redline/doctor.py b/redline/doctor.py
index e9004a5..71fce81 100644
--- a/redline/doctor.py
+++ b/redline/doctor.py
@@ -235,7 +235,7 @@ def _coverage_check(
non_ascii_records = _manifest_int(manifest_summary, "non_ascii_records")
judge_configured = "judge" in config
message += (
- f"; high-risk clusters={high_risk_clusters}; "
+ f"; high-risk groups={high_risk_clusters}; "
f"requirements={requirements_count}; "
f"judge={'yes' if judge_configured else 'no'}; "
f"explicit guards={explicit_guard_cases}/{cases_count}"
@@ -261,7 +261,7 @@ def _coverage_check(
non_ascii_records = int(summary_report.get("non_ascii_records") or 0)
judge_configured = "judge" in config
message += (
- f"; high-risk clusters={high_risk_clusters}; "
+ f"; high-risk groups={high_risk_clusters}; "
f"requirements={requirements_count}; "
f"judge={'yes' if judge_configured else 'no'}; "
f"explicit guards={explicit_guard_cases}/{cases_count}"
diff --git a/redline/history.py b/redline/history.py
index c3ff184..b2032dd 100644
--- a/redline/history.py
+++ b/redline/history.py
@@ -156,9 +156,9 @@ def format_markdown_history(entries: list[dict[str, Any]], *, limit: int | None
if cluster_rows:
lines.extend(
[
- "## Cluster Diagnosis",
+ "## Behavior Group Diagnosis",
"",
- "| Cluster | Blocking Delta | Latest Blocking | Changed Delta |",
+ "| Behavior Group | Blocking Delta | Latest Blocking | Changed Delta |",
"| --- | ---: | ---: | ---: |",
]
)
diff --git a/redline/mcp.py b/redline/mcp.py
index ed70db8..50d0190 100644
--- a/redline/mcp.py
+++ b/redline/mcp.py
@@ -418,7 +418,7 @@ def _build_suite_from_logs_prompt(arguments: dict[str, Any]) -> str:
"Use this workflow:\n"
"1. Call `redline_suite` for the baseline log and write the suite output.\n"
"2. Call `redline_validate` on the generated suite.\n"
- "3. Call `redline_summary` so I can see coverage, clusters, pinned cases, and next steps.\n"
+ "3. Call `redline_summary` so I can see coverage, behavior groups, pinned cases, and next steps.\n"
"4. If coverage is low or cases look unclear, call `redline_cases` and `redline_case` to inspect representative cases before recommending pins.\n"
"5. Call `redline_budget` so I can see expected CI runtime before enabling a gate.\n"
"6. Explain what behavior redline can catch, what still needs human review, and the exact `redline suite add` command I can run for must-cover edge cases.\n"
@@ -640,7 +640,7 @@ def _tools() -> list[ToolSpec]:
),
ToolSpec(
"redline_summary",
- "Summarize suite or prompt-manifest provenance, coverage, clusters, owners, requirements, and next steps.",
+ "Summarize suite or prompt-manifest provenance, coverage, behavior groups, owners, requirements, and next steps.",
_schema(
{
"suite_path": _string("Suite or prompt manifest JSON path. Defaults to config."),
diff --git a/redline/summary.py b/redline/summary.py
index e20774f..adbdfad 100644
--- a/redline/summary.py
+++ b/redline/summary.py
@@ -329,8 +329,8 @@ def format_suite_summary(suite: dict[str, Any], *, suite_path: str | None = None
f"Records seen: {summary['records_seen']}",
f"Unique pairs: {summary['unique_prompt_response_pairs']}",
f"Duplicate pairs: {summary['duplicate_prompt_response_pairs']}",
- f"Behavioral clusters: {summary['clusters']}",
- f"Cluster coverage: {summary['covered_clusters']}/{summary['clusters']} ({_percent(summary['cluster_coverage'])})",
+ f"Behavior groups: {summary['clusters']}",
+ f"Group coverage: {summary['covered_clusters']}/{summary['clusters']} ({_percent(summary['cluster_coverage'])})",
f"Representative cases: {summary['cases']}",
f"Case coverage: {summary['cases']}/{summary['unique_prompt_response_pairs']} ({_percent(summary['case_coverage'])})",
f"Pinned cases: {summary['pinned_cases']}",
@@ -340,10 +340,10 @@ def format_suite_summary(suite: dict[str, Any], *, suite_path: str | None = None
f"Approved baselines: {summary['approved_baselines']}/{summary['accepted_baselines']}",
f"Explicit guard coverage: {summary['explicit_guard_cases']}/{summary['cases']} ({_percent(summary['explicit_guard_coverage'])})",
f"Max cases: {summary['max_cases']}",
- f"High-risk clusters: {summary['high_risk_clusters']}",
- f"Medium-risk clusters: {summary['medium_risk_clusters']}",
- f"High-variance clusters: {summary['high_variance_clusters']}",
- f"Failure-pattern clusters: {summary['failure_pattern_clusters']:>2}",
+ f"High-risk groups: {summary['high_risk_clusters']}",
+ f"Medium-risk groups: {summary['medium_risk_clusters']}",
+ f"High-variance groups: {summary['high_variance_clusters']}",
+ f"Failure-pattern groups: {summary['failure_pattern_clusters']:>2}",
f"Non-ASCII records: {summary['non_ascii_records']}",
f"Cases with requirements: {summary['requirements']:>2}",
"",
@@ -365,7 +365,7 @@ def format_suite_summary(suite: dict[str, Any], *, suite_path: str | None = None
top_clusters = summary["top_clusters"]
if top_clusters:
- lines.append("Top clusters:")
+ lines.append("Top groups:")
for cluster in top_clusters:
marker = " high-variance" if cluster["high_variance"] else ""
flags = cluster["failure_patterns"]
@@ -398,8 +398,8 @@ def format_prompt_manifest_summary(report: dict[str, Any]) -> str:
f"Invalid suites: {report['invalid_suite_count']}",
f"Records seen: {report['records_seen']}",
f"Unique pairs: {report['unique_prompt_response_pairs']}",
- f"Behavioral clusters: {report['clusters']}",
- f"Cluster coverage: {report['covered_clusters']}/{report['clusters']} ({_percent(report['cluster_coverage'])})",
+ f"Behavior groups: {report['clusters']}",
+ f"Group coverage: {report['covered_clusters']}/{report['clusters']} ({_percent(report['cluster_coverage'])})",
f"Representative cases: {report['cases']}",
f"Case coverage: {report['cases']}/{report['unique_prompt_response_pairs']} ({_percent(report['case_coverage'])})",
f"Pinned cases: {report['pinned_cases']}",
@@ -408,8 +408,8 @@ def format_prompt_manifest_summary(report: dict[str, Any]) -> str:
f"Accepted baselines: {report['accepted_baselines']}",
f"Approved baselines: {report['approved_baselines']}/{report['accepted_baselines']}",
f"Explicit guard coverage: {report['explicit_guard_cases']}/{report['cases']} ({_percent(report['explicit_guard_coverage'])})",
- f"High-risk clusters: {report['high_risk_clusters']}",
- f"Medium-risk clusters: {report['medium_risk_clusters']}",
+ f"High-risk groups: {report['high_risk_clusters']}",
+ f"Medium-risk groups: {report['medium_risk_clusters']}",
f"Non-ASCII records: {report['non_ascii_records']}",
f"Cases with requirements: {report['requirements']:>2}",
"",
diff --git a/tests/test_clusters.py b/tests/test_clusters.py
index b0e8ef7..5ae4f50 100644
--- a/tests/test_clusters.py
+++ b/tests/test_clusters.py
@@ -69,10 +69,10 @@ def test_format_cluster_report_is_readable(self) -> None:
output = format_cluster_report(suite)
self.assertIn("redline cluster", output)
- self.assertIn("Identified 1 behavioral clusters from 1 unique pairs", output)
+ self.assertIn("Identified 1 behavior-signature groups from 1 unique pairs", output)
self.assertIn("Records seen: 1 Duplicate pairs: 0", output)
- self.assertIn("High-risk clusters: 0", output)
- self.assertIn("Failure-pattern clusters: 0", output)
+ self.assertIn("High-risk groups: 0", output)
+ self.assertIn("Failure-pattern groups: 0", output)
self.assertIn("Suggested eval suite: 1 representative cases.", output)
self.assertIn("Case coverage: 1/1 (100.0%)", output)
self.assertIn("RISK", output)
diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py
index ad90aeb..5e7dfd5 100644
--- a/tests/test_dashboard.py
+++ b/tests/test_dashboard.py
@@ -326,7 +326,7 @@ def test_dashboard_trend_shows_cluster_diagnosis(self) -> None:
html = format_dashboard_html(dashboard)
- self.assertIn("
Cluster diagnosis
", html)
+ self.assertIn("
Behavior group diagnosis
", html)
self.assertIn("structured JSON prompt -> JSON response (short)", html)
self.assertIn("blocking +1", html)
self.assertIn("changed -1", html)
diff --git a/tests/test_doctor.py b/tests/test_doctor.py
index 8c2de55..237dac6 100644
--- a/tests/test_doctor.py
+++ b/tests/test_doctor.py
@@ -352,7 +352,7 @@ def test_doctor_calibrates_coverage_for_high_risk_suite_without_semantic_checks(
coverage = next(check for check in report["checks"] if check["name"] == "coverage")
self.assertEqual(coverage["status"], "ok")
- self.assertIn("high-risk clusters=1", coverage["message"])
+ self.assertIn("high-risk groups=1", coverage["message"])
self.assertIn("requirements=0; judge=no", coverage["message"])
self.assertIn("explicit guards=0/1", coverage["message"])
self.assertIn("add requirements or a judge", coverage["message"])
diff --git a/tests/test_history.py b/tests/test_history.py
index fc364f4..7bc1755 100644
--- a/tests/test_history.py
+++ b/tests/test_history.py
@@ -211,7 +211,7 @@ def test_format_markdown_history_includes_cluster_diagnosis(self) -> None:
]
)
- self.assertIn("## Cluster Diagnosis", text)
+ self.assertIn("## Behavior Group Diagnosis", text)
self.assertIn("structured JSON prompt -> JSON response (short)", text)
self.assertIn("| structured JSON prompt -> JSON response (short) | +1 | 1 | 0 |", text)
diff --git a/tests/test_summary.py b/tests/test_summary.py
index 530ea02..09c696e 100644
--- a/tests/test_summary.py
+++ b/tests/test_summary.py
@@ -92,7 +92,7 @@ def test_format_suite_summary_is_readable(self) -> None:
self.assertIn("Records seen:", output)
self.assertIn("Unique pairs:", output)
self.assertIn("Duplicate pairs:", output)
- self.assertIn("Cluster coverage:", output)
+ self.assertIn("Group coverage:", output)
self.assertIn("Case coverage:", output)
self.assertIn("Pinned cases:", output)
self.assertIn("Owned cases:", output)
@@ -100,9 +100,9 @@ def test_format_suite_summary_is_readable(self) -> None:
self.assertIn("Accepted baselines:", output)
self.assertIn("Approved baselines:", output)
self.assertIn("Explicit guard coverage:", output)
- self.assertIn("High-risk clusters:", output)
- self.assertIn("Failure-pattern clusters:", output)
- self.assertIn("Top clusters:", output)
+ self.assertIn("High-risk groups:", output)
+ self.assertIn("Failure-pattern groups:", output)
+ self.assertIn("Top groups:", output)
self.assertIn("structured JSON prompt -> JSON response", output)
def test_suite_summary_surfaces_non_ascii_calibration(self) -> None:
@@ -273,7 +273,7 @@ def test_suite_summary_recommends_more_coverage_when_budget_is_tight(self) -> No
self.assertEqual(summary["cluster_coverage"], 0.5)
self.assertIn("Increase --max-cases", summary["next_steps"][0])
self.assertIn("redline suite add redline-suite.json --prompt-file", output)
- self.assertIn("Cluster coverage: 1/2 (50.0%)", output)
+ self.assertIn("Group coverage: 1/2 (50.0%)", output)
self.assertIn("Next:", output)
From dda6a0b193f16eccd0e661a3b86b5773d1635b9d Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 31 May 2026 22:35:14 -0600
Subject: [PATCH 03/55] Add suite readiness scoring
---
README.md | 7 ++--
docs/commands.md | 2 +-
redline/summary.py | 92 +++++++++++++++++++++++++++++++++++++++++++
tests/test_summary.py | 9 +++++
4 files changed, 106 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index a031aa1..e9962e6 100644
--- a/README.md
+++ b/README.md
@@ -428,9 +428,10 @@ redline summary redline-suite.json
`doctor` shows whether the suite has explicit requirements or recorded
judgments before you rely on structural checks in CI.
-`summary` reports behavior-group/case coverage, owner coverage, accepted baseline
-history, approver coverage, and explicit guard coverage for cases with
-requirements or recorded judgments so teams can review suite readiness before CI.
+`summary` reports a suite readiness score, behavior-group/case coverage, owner
+coverage, accepted baseline history, approver coverage, and explicit guard
+coverage for cases with requirements or recorded judgments so teams can review
+suite readiness before CI.
`dashboard` also shows audit checkpoint evidence when `.redline/audit-checkpoint.json`
is present.
diff --git a/docs/commands.md b/docs/commands.md
index 25d7e19..c4cf8eb 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -19,7 +19,7 @@ output and examples after installing.
| `redline case` | Show one full suite case. | `--json` |
| `redline require` | Add deterministic include/exclude requirements. | `--include`, `--exclude`, `--owner` |
| `redline prompts` | Build or check a prompt-to-suite manifest. | `--suite-dir`, `--out`, `--check`, `--check-suites` |
-| `redline summary` | Summarize suite or manifest readiness. | `--json` |
+| `redline summary` | Summarize suite or manifest readiness, including suite score and coverage gaps. | `--json` |
| `redline validate` | Validate suite or manifest structure and freshness. | `--strict`, `--json` |
| `redline budget` | Estimate CI runtime without replaying prompts. | `--workers`, `--timeout`, `--max-seconds`, `--measure-local` |
| `redline benchmark` | Compatibility alias for `redline budget`. | Same as `budget` |
diff --git a/redline/summary.py b/redline/summary.py
index adbdfad..bb00e92 100644
--- a/redline/summary.py
+++ b/redline/summary.py
@@ -154,6 +154,7 @@ def suite_summary(suite: dict[str, Any]) -> dict[str, Any]:
"requirements": requirements_count,
"top_clusters": top_clusters,
}
+ result["suite_readiness"] = _suite_readiness(result)
result["next_steps"] = _summary_next_steps(result)
return result
@@ -333,6 +334,7 @@ def format_suite_summary(suite: dict[str, Any], *, suite_path: str | None = None
f"Group coverage: {summary['covered_clusters']}/{summary['clusters']} ({_percent(summary['cluster_coverage'])})",
f"Representative cases: {summary['cases']}",
f"Case coverage: {summary['cases']}/{summary['unique_prompt_response_pairs']} ({_percent(summary['case_coverage'])})",
+ f"Suite readiness: {_format_readiness(summary['suite_readiness'])}",
f"Pinned cases: {summary['pinned_cases']}",
f"Owned cases: {summary['owned_cases']}/{summary['cases']}",
f"Owner rule coverage: {summary['owner_rule_cases']}/{summary['owned_cases']} ({_percent(summary['owner_rule_coverage'])})",
@@ -374,6 +376,15 @@ def format_suite_summary(suite: dict[str, Any], *, suite_path: str | None = None
lines.append(f" {cluster['size']:>4} {cluster['behavior']}{marker}")
lines.append("")
+ readiness = summary.get("suite_readiness")
+ if isinstance(readiness, dict):
+ reasons = readiness.get("reasons")
+ if isinstance(reasons, list) and reasons:
+ lines.append("Readiness signals:")
+ for reason in reasons:
+ lines.append(f"- {reason}")
+ lines.append("")
+
next_steps = summary["next_steps"]
if next_steps:
lines.append("Next:")
@@ -484,6 +495,87 @@ def _summary_next_steps(summary: dict[str, Any], *, suite_path: str | None = Non
return steps
+def _suite_readiness(summary: dict[str, Any]) -> dict[str, Any]:
+ cases = int(summary.get("cases") or 0)
+ if cases == 0:
+ return {
+ "score": 0,
+ "label": "empty",
+ "reasons": ["no suite cases are available yet"],
+ }
+
+ score = 0.0
+ score += 35.0 * float(summary.get("cluster_coverage") or 0.0)
+ score += 20.0 * float(summary.get("case_coverage") or 0.0)
+ score += 20.0 * float(summary.get("explicit_guard_coverage") or 0.0)
+ owner_coverage = _ratio(int(summary.get("owned_cases") or 0), cases) or 0.0
+ score += 10.0 * owner_coverage
+ if int(summary.get("high_risk_clusters") or 0) == 0 or int(summary.get("requirements") or 0):
+ score += 10.0
+ if int(summary.get("non_ascii_records") or 0) == 0:
+ score += 5.0
+
+ rounded = int(round(min(100.0, max(0.0, score))))
+ if rounded >= 80:
+ label = "strong"
+ elif rounded >= 55:
+ label = "usable"
+ else:
+ label = "needs_work"
+
+ return {
+ "score": rounded,
+ "label": label,
+ "reasons": _readiness_reasons(summary, owner_coverage=owner_coverage),
+ }
+
+
+def _readiness_reasons(summary: dict[str, Any], *, owner_coverage: float) -> list[str]:
+ reasons = []
+ cluster_coverage = float(summary.get("cluster_coverage") or 0.0)
+ case_coverage = float(summary.get("case_coverage") or 0.0)
+ explicit_guard_coverage = float(summary.get("explicit_guard_coverage") or 0.0)
+
+ if cluster_coverage >= 1.0:
+ reasons.append("all detected behavior-signature groups have at least one selected case")
+ else:
+ reasons.append("some behavior-signature groups are not represented in the suite")
+
+ if case_coverage >= 0.8:
+ reasons.append("case budget covers most unique prompt-response pairs")
+ else:
+ reasons.append("case budget is sampling a small share of unique prompt-response pairs")
+
+ if explicit_guard_coverage >= 0.5:
+ reasons.append("many cases have requirements or recorded judgments")
+ elif explicit_guard_coverage > 0:
+ reasons.append("some cases have requirements or recorded judgments")
+ else:
+ reasons.append("no cases have explicit requirements or recorded judgments yet")
+
+ if owner_coverage >= 1.0:
+ reasons.append("all cases have owners")
+ elif owner_coverage > 0:
+ reasons.append("some cases still need owners")
+ else:
+ reasons.append("no cases have owners yet")
+
+ if int(summary.get("high_risk_clusters") or 0) and int(summary.get("requirements") or 0) == 0:
+ reasons.append("high-risk groups need explicit requirements or a judge before CI gating")
+ if int(summary.get("non_ascii_records") or 0):
+ reasons.append("non-ASCII records need extra review because entity/refusal heuristics are English-oriented")
+
+ return reasons
+
+
+def _format_readiness(value: object) -> str:
+ if not isinstance(value, dict):
+ return ""
+ score = int(value.get("score") or 0)
+ label = str(value.get("label") or "unknown").replace("_", " ")
+ return f"{score}/100 ({label})"
+
+
def _manifest_summary_status(summary: dict[str, Any]) -> str:
if int(summary["invalid_suite_count"]):
return "invalid"
diff --git a/tests/test_summary.py b/tests/test_summary.py
index 09c696e..bc2536a 100644
--- a/tests/test_summary.py
+++ b/tests/test_summary.py
@@ -54,6 +54,12 @@ def test_suite_summary_counts_cases_clusters_and_judgments(self) -> None:
self.assertEqual(summary["judgment_cases"], 1)
self.assertEqual(summary["explicit_guard_cases"], 1)
self.assertEqual(summary["explicit_guard_coverage"], 0.5)
+ self.assertEqual(summary["suite_readiness"]["score"], 80)
+ self.assertEqual(summary["suite_readiness"]["label"], "strong")
+ self.assertIn(
+ "many cases have requirements or recorded judgments",
+ summary["suite_readiness"]["reasons"],
+ )
self.assertEqual(summary["judgments"], {"expected": 1})
self.assertEqual(summary["requirements"], 1)
self.assertEqual(summary["failure_pattern_clusters"], 0)
@@ -94,6 +100,7 @@ def test_format_suite_summary_is_readable(self) -> None:
self.assertIn("Duplicate pairs:", output)
self.assertIn("Group coverage:", output)
self.assertIn("Case coverage:", output)
+ self.assertIn("Suite readiness:", output)
self.assertIn("Pinned cases:", output)
self.assertIn("Owned cases:", output)
self.assertIn("Owner rule coverage:", output)
@@ -103,6 +110,7 @@ def test_format_suite_summary_is_readable(self) -> None:
self.assertIn("High-risk groups:", output)
self.assertIn("Failure-pattern groups:", output)
self.assertIn("Top groups:", output)
+ self.assertIn("Readiness signals:", output)
self.assertIn("structured JSON prompt -> JSON response", output)
def test_suite_summary_surfaces_non_ascii_calibration(self) -> None:
@@ -271,6 +279,7 @@ def test_suite_summary_recommends_more_coverage_when_budget_is_tight(self) -> No
self.assertEqual(summary["clusters"], 2)
self.assertEqual(summary["case_coverage"], 0.5)
self.assertEqual(summary["cluster_coverage"], 0.5)
+ self.assertEqual(summary["suite_readiness"]["label"], "needs_work")
self.assertIn("Increase --max-cases", summary["next_steps"][0])
self.assertIn("redline suite add redline-suite.json --prompt-file", output)
self.assertIn("Group coverage: 1/2 (50.0%)", output)
From 12062cb20cc32fe29c0b4fd5e3b761f145d351ab Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 31 May 2026 22:37:08 -0600
Subject: [PATCH 04/55] Document redline methodology
---
README.md | 3 ++
docs/methodology.md | 104 ++++++++++++++++++++++++++++++++++++++++
tests/test_packaging.py | 10 ++++
3 files changed, 117 insertions(+)
create mode 100644 docs/methodology.md
diff --git a/README.md b/README.md
index e9962e6..e824620 100644
--- a/README.md
+++ b/README.md
@@ -200,6 +200,8 @@ redline is deterministic and local-first by default. Optional judge commands are
available for ambiguous `changed` cases, but redline does not call a cloud model
unless you explicitly configure that command.
+Methodology details live in [docs/methodology.md](docs/methodology.md).
+
Suite generation does not run statistical or embedding clustering by default.
It groups logs by deterministic behavior signatures, such as prompt intent,
response shape, length bucket, and JSON schema. It picks one representative per
@@ -481,6 +483,7 @@ bash scripts/release_check.sh
- [docs/release.md](docs/release.md): package, tag, PyPI, and MCP Registry release flow
- [docs/launch.md](docs/launch.md): public alpha launch plan
- [docs/troubleshooting.md](docs/troubleshooting.md): first-run and CI failure recovery
+- [docs/methodology.md](docs/methodology.md): behavior grouping, case selection, scoring, and trust boundaries
- [docs/commands.md](docs/commands.md): compact CLI command reference
- [docs/real-log-dogfood.md](docs/real-log-dogfood.md): redaction-first real-log test protocol
- [docs/dogfood.md](docs/dogfood.md): first-user dogfood protocol
diff --git a/docs/methodology.md b/docs/methodology.md
new file mode 100644
index 0000000..262b05f
--- /dev/null
+++ b/docs/methodology.md
@@ -0,0 +1,104 @@
+# Methodology
+
+redline is a deterministic regression detector for prompt-response systems. It
+does not claim that a green run proves semantic safety. It turns logs into a
+reviewable suite, checks high-signal behavioral changes, and leaves factual,
+tone, policy, hallucination, and subtle reasoning judgment to explicit
+requirements, optional judges, or humans.
+
+## Input Model
+
+redline expects prompt-response observations as JSONL. The canonical fields are
+`prompt` and `response`, but `redline import` can normalize nested exports from
+application logs, observability tools, or model gateways.
+
+Import is redaction-first by default. The importer redacts common secrets and
+PII before extracting fields so copied metadata is treated with the same local
+privacy boundary as prompt and response text. Use `--no-redact` only for local
+fixtures you have already inspected.
+
+## Behavior Grouping
+
+redline does not run statistical, embedding, or semantic clustering by default.
+It groups observations by deterministic behavior signatures:
+
+- prompt intent, such as classify, summarize, transform, generate, or answer
+- response shape, such as JSON, table, code block, bullet list, numbered list,
+ refusal, empty output, or prose
+- response length bucket
+- JSON schema shape when the response is valid JSON
+
+The CLI command is named `redline cluster` for compatibility, but the public
+model is behavior-signature grouping. These groups are explainable and stable;
+they are not a claim that two prompts are semantically equivalent.
+
+## Case Selection
+
+Suite generation chooses cases in this order:
+
+1. One representative from each behavior group.
+2. High-risk groups with obvious failure patterns, such as invalid JSON,
+ malformed tables, empty outputs, or refusals.
+3. High-variance edges where same-shape outputs vary substantially in length.
+4. Prompt-diverse samples from large groups when the case budget allows.
+5. User-pinned cases from `redline suite add`.
+
+This strategy optimizes for useful regression coverage from existing logs, not
+perfect coverage of every product risk. If a scenario matters, pin it.
+
+## Regression Signals
+
+The deterministic diff engine checks for high-signal changes:
+
+- lost JSON validity or missing JSON keys
+- lost table, list, or code-block structure
+- missing numbers, URLs, ticket IDs, and likely entities
+- empty outputs
+- newly refused safe-looking tasks
+- allow/deny polarity flips
+- large same-shape content drift
+- explicit include/exclude requirements
+
+These checks are intentionally conservative. They catch common prompt
+regressions quickly in CI, but they do not replace domain review.
+
+## Suite Readiness Score
+
+`redline summary` reports a suite readiness score from 0 to 100. The score is a
+suite-health signal, not a model-quality score. It combines:
+
+- behavior-group coverage
+- unique prompt-response pair coverage
+- explicit requirements or recorded judgments
+- owner coverage
+- high-risk group calibration
+- non-English calibration warnings
+
+Use the score to decide what to improve before CI gating. A high score means the
+suite is better prepared for review and automation. It does not mean the
+candidate prompt is safe to ship.
+
+## Judges
+
+Optional judges are for ambiguous `changed` cases and semantic risks that
+deterministic checks cannot prove. A judge can be a local script, a local model,
+or a cloud LLM command you control. redline routes only the configured review
+surface and records judge output as evidence.
+
+Default redline behavior is local and deterministic. No cloud judge is called
+unless you configure one.
+
+## Calibration
+
+Use redline as a safety loop:
+
+1. Import or watch real logs.
+2. Generate a suite.
+3. Inspect `redline summary` and `redline cases`.
+4. Pin must-cover edge cases.
+5. Add requirements for details that must not disappear.
+6. Run `redline eval` or `redline diff` before prompt changes ship.
+7. Mark expected changes and accept new baselines after review.
+
+The strongest evidence for redline is not a score. It is a concrete report that
+says which production behavior changed and why it matters.
diff --git a/tests/test_packaging.py b/tests/test_packaging.py
index eb1a454..6b303cf 100644
--- a/tests/test_packaging.py
+++ b/tests/test_packaging.py
@@ -103,6 +103,16 @@ def test_demo_gif_script_records_or_writes_transcript(self) -> None:
self.assertIn("redline-demo-transcript.txt", text)
self.assertIn("redline demo --public --compact", text)
+ def test_methodology_doc_calibrates_behavior_grouping_and_score(self) -> None:
+ text = Path("docs/methodology.md").read_text(encoding="utf-8")
+ readme = Path("README.md").read_text(encoding="utf-8")
+
+ self.assertIn("does not run statistical, embedding, or semantic clustering", text)
+ self.assertIn("behavior-signature grouping", text)
+ self.assertIn("suite readiness score", text)
+ self.assertIn("not a model-quality score", text)
+ self.assertIn("docs/methodology.md", readme)
+
def test_release_check_builds_and_smokes_installed_wheel(self) -> None:
script = Path("scripts/release_check.sh").read_text(encoding="utf-8")
From bfd4c41620998b7b9581b234c35af198fa32e44d Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 31 May 2026 22:39:32 -0600
Subject: [PATCH 05/55] Explain why report findings matter
---
redline/reports.py | 43 ++++++++++++++++++++++++++++++++++++++++++-
tests/test_reports.py | 6 ++++++
2 files changed, 48 insertions(+), 1 deletion(-)
diff --git a/redline/reports.py b/redline/reports.py
index 6cb1b48..27042b7 100644
--- a/redline/reports.py
+++ b/redline/reports.py
@@ -132,6 +132,10 @@ def format_markdown_report(result: dict[str, Any], *, title: str = "redline diff
if metadata:
lines.extend(metadata)
lines.append("")
+ impact = _why_this_matters(item)
+ if impact:
+ lines.append(f"Why this matters: {impact}")
+ lines.append("")
for reason in item["reasons"]:
lines.append(f"- {reason}")
lines.append("")
@@ -482,6 +486,35 @@ def _metadata_lines(item: dict[str, Any]) -> list[str]:
return lines
+def _why_this_matters(item: dict[str, Any]) -> str:
+ status = str(item.get("status") or "").lower()
+ reasons = item.get("reasons")
+ text = " ".join(str(reason).lower() for reason in reasons) if isinstance(reasons, list) else ""
+ if status == "missing":
+ return "Replay did not produce a candidate output, so this baseline behavior is untested."
+ if "valid json" in text or "json keys" in text:
+ return "Downstream code may fail if consumers expect parseable JSON or required fields."
+ if "markdown table" in text or "table structure" in text:
+ return "A consumer expecting rows and columns may receive unstructured prose instead."
+ if "code block" in text:
+ return "A developer or tool expecting copyable code may lose the executable structure."
+ if "bullet list" in text or "numbered list" in text:
+ return "A workflow expecting ordered or scannable steps may become harder to review."
+ if "missing numbers" in text or "missing urls" in text or "missing entities" in text:
+ return "Concrete details used for decisions, routing, or compliance may have been dropped."
+ if "newly refuses" in text or "refusal" in text:
+ return "A previously supported safe workflow may now be blocked."
+ if "became empty" in text:
+ return "The user may receive no usable answer."
+ if "content changed substantially" in text or "much shorter" in text:
+ return "Meaning may have changed enough to require human review before acceptance."
+ if status == "regression":
+ return "This case changed in a way redline treats as blocking before shipping."
+ if status == "changed":
+ return "Review whether this behavioral change is intentional before accepting it."
+ return ""
+
+
def _result_warnings(result: dict[str, Any]) -> list[str]:
warnings = result.get("warnings")
if not isinstance(warnings, list):
@@ -1054,7 +1087,12 @@ def _annotation_message(item: dict[str, Any]) -> str:
.changed { background: var(--changed); }
.improved { background: var(--improved); }
.accepted, .ignored, .neutral { background: var(--neutral); }
-.meta, .prompt { color: var(--muted); font-size: 13px; }
+.meta, .prompt, .impact { color: var(--muted); font-size: 13px; }
+.impact {
+ border-left: 3px solid var(--changed);
+ margin: 10px 0;
+ padding-left: 10px;
+}
.reasons { margin: 10px 0 14px 20px; padding: 0; }
.responses {
display: grid;
@@ -1344,6 +1382,9 @@ def _html_case(item: dict[str, Any]) -> str:
lines.append(f'
{_h(metadata)}
')
if prompt:
lines.append(f'
Prompt: {_h(prompt)}
')
+ impact = _why_this_matters(item)
+ if impact:
+ lines.append(f'
Why this matters: {_h(impact)}
')
if isinstance(reasons, list) and reasons:
lines.append('
')
for reason in reasons:
diff --git a/tests/test_reports.py b/tests/test_reports.py
index 8fa1f7b..23511cf 100644
--- a/tests/test_reports.py
+++ b/tests/test_reports.py
@@ -112,6 +112,10 @@ def test_markdown_report_includes_summary_and_reasons(self) -> None:
self.assertIn("Owner: `@platform-team`", report)
self.assertIn("Confidence: `high`", report)
self.assertIn("Signal: `structural`", report)
+ self.assertIn(
+ "Why this matters: Downstream code may fail if consumers expect parseable JSON or required fields.",
+ report,
+ )
self.assertIn("Baseline:", report)
self.assertIn('{"ok": true}', report)
self.assertIn("Candidate:", report)
@@ -226,6 +230,8 @@ def test_html_report_includes_side_by_side_case_detail(self) -> None:
self.assertIn("Owner: @platform-team", report)
self.assertIn("Behavior: structured JSON prompt -> JSON response (short)", report)
self.assertIn("Confidence: high | Signal: structural", report)
+ self.assertIn("Why this matters:", report)
+ self.assertIn("Downstream code may fail if consumers expect parseable JSON or required fields.", report)
self.assertIn("fix blocking cases before shipping", report)
self.assertIn("structural checks only", report)
self.assertIn("Candidate lost required structure; fix blocking cases before shipping.", report)
From 2394ad4e7be149b31262976561b6a878a31ef3b1 Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 31 May 2026 22:45:02 -0600
Subject: [PATCH 06/55] Add log import presets
---
README.md | 1 +
docs/commands.md | 2 +-
docs/real-log-dogfood.md | 9 ++++++++
redline/cli.py | 42 ++++++++++++++++++++++++++++++--------
redline/import_logs.py | 43 +++++++++++++++++++++++++++++++++++++++
redline/mcp.py | 2 ++
tests/test_cli_config.py | 33 ++++++++++++++++++++++++++++++
tests/test_import_logs.py | 40 ++++++++++++++++++++++++++++++++++++
tests/test_mcp.py | 2 ++
9 files changed, 165 insertions(+), 9 deletions(-)
diff --git a/README.md b/README.md
index e824620..de3ead1 100644
--- a/README.md
+++ b/README.md
@@ -119,6 +119,7 @@ bounded FastAPI/ASGI middleware.
```bash
redline import downloaded.jsonl --input-field instruction --output-field response --out logs/baseline.jsonl
+redline import langfuse-export.jsonl --preset langfuse --out logs/baseline.jsonl
redline suite logs/baseline.jsonl --out redline-suite.json
redline cases redline-suite.json
```
diff --git a/docs/commands.md b/docs/commands.md
index c4cf8eb..8b9f466 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -10,7 +10,7 @@ output and examples after installing.
| `redline init` | Write `redline.json`, runner files, and optional CI workflow. | `--runner`, `--copy-runner`, `--github-action`, `--force` |
| `redline doctor` | Check config, suite, replay, reports, audit, and team workflow. | `--strict`, `--json` |
| `redline watch` | Collect prompt-response observations or print snippets. | `--log`, `--stats`, `--snippet`, `--follow` |
-| `redline import` | Normalize exported JSONL fields into redline `prompt`/`response` logs, with best-effort redaction on by default. | `--input-field`, `--output-field`, `--context-field`, `--metadata-field`, `--limit`, `--out`, `--no-redact` |
+| `redline import` | Normalize exported JSONL fields into redline `prompt`/`response` logs, with best-effort redaction on by default. | `--preset`, `--input-field`, `--output-field`, `--context-field`, `--metadata-field`, `--limit`, `--out`, `--no-redact` |
| `redline redact` | Check or write best-effort redacted logs. | `--check`, `--out`, `--json` |
| `redline cluster` | Inspect deterministic behavior-signature groups before suite generation. | `--max-cases`, `--json` |
| `redline suite` | Generate a suite from baseline JSONL logs. | `--out`, `--max-cases`, `--all-cases`, `--owner` |
diff --git a/docs/real-log-dogfood.md b/docs/real-log-dogfood.md
index 5a4b385..85e9b6d 100644
--- a/docs/real-log-dogfood.md
+++ b/docs/real-log-dogfood.md
@@ -29,6 +29,15 @@ contains the model or agent response. Common examples:
| Langfuse/Helicone-style export | `input` | `output` |
| Support traces | `ticket.text` | `assistant.reply` |
+For common exports, start with a preset and override fields only when your file
+differs:
+
+```bash
+redline import raw-export.jsonl --preset langfuse --out .redline/dogfood/baseline.jsonl
+redline import raw-export.jsonl --preset helicone --out .redline/dogfood/baseline.jsonl
+redline import raw-export.jsonl --preset openai-chat --out .redline/dogfood/baseline.jsonl
+```
+
## 2. Import With Redaction On
`redline import` redacts common secrets and PII by default before writing the
diff --git a/redline/cli.py b/redline/cli.py
index 26e7ff9..6f3ce08 100644
--- a/redline/cli.py
+++ b/redline/cli.py
@@ -63,7 +63,7 @@
read_history,
should_fail_history,
)
-from .import_logs import import_jsonl_log
+from .import_logs import IMPORT_PRESETS, import_jsonl_log, import_preset
from .io import append_jsonl, append_text, read_json, read_jsonl_records, write_json, write_jsonl, write_text
from .judge import apply_judge
from .judge_templates import (
@@ -239,8 +239,9 @@ def build_parser() -> argparse.ArgumentParser:
import_parser = subparsers.add_parser("import", help="normalize exported JSONL logs into redline format")
import_parser.add_argument("path", help="source JSONL file to normalize")
- import_parser.add_argument("--input-field", default="prompt", help="source field path containing prompt text")
- import_parser.add_argument("--output-field", default="response", help="source field path containing response text")
+ import_parser.add_argument("--preset", choices=sorted(IMPORT_PRESETS), help="field mapping preset for common log exports")
+ import_parser.add_argument("--input-field", help="source field path containing prompt text")
+ import_parser.add_argument("--output-field", help="source field path containing response text")
import_parser.add_argument("--context-field", help="optional source field path appended to the prompt as Context")
import_parser.add_argument("--id-field", help="optional source field path copied to the redline id field")
import_parser.add_argument(
@@ -731,22 +732,32 @@ def cmd_judges(args: argparse.Namespace) -> int:
def cmd_import(args: argparse.Namespace) -> int:
+ preset = import_preset(args.preset) if args.preset else {}
+ input_field = args.input_field or str(preset.get("input_field") or "prompt")
+ output_field = args.output_field or str(preset.get("output_field") or "response")
+ context_field = args.context_field if args.context_field is not None else _optional_preset_string(preset, "context_field")
+ id_field = args.id_field if args.id_field is not None else _optional_preset_string(preset, "id_field")
+ metadata_fields = _preset_string_list(preset, "metadata_fields")
+ metadata_fields.extend(args.metadata_field)
report = import_jsonl_log(
args.path,
output=args.out,
- input_field=args.input_field,
- output_field=args.output_field,
- context_field=args.context_field,
- id_field=args.id_field,
- metadata_fields=args.metadata_field,
+ input_field=input_field,
+ output_field=output_field,
+ context_field=context_field,
+ id_field=id_field,
+ metadata_fields=metadata_fields,
limit=args.limit,
redact=not args.no_redact,
placeholder=args.redaction_placeholder,
)
+ report["preset"] = args.preset or ""
if args.json:
print(json.dumps(report, indent=2, sort_keys=True))
else:
print(f"Imported {report['records']} prompt-response pairs from {Path(str(report['source']))}.")
+ if report["preset"]:
+ print(f"Preset: {report['preset']}")
print(f"Mapped prompt: {report['input_field']}")
print(f"Mapped response: {report['output_field']}")
if report["context_field"]:
@@ -772,6 +783,21 @@ def cmd_import(args: argparse.Namespace) -> int:
return 0
+def _optional_preset_string(preset: Mapping[str, object], key: str) -> str | None:
+ value = preset.get(key)
+ if value is None:
+ return None
+ text = str(value).strip()
+ return text or None
+
+
+def _preset_string_list(preset: Mapping[str, object], key: str) -> list[str]:
+ value = preset.get(key)
+ if not isinstance(value, list):
+ return []
+ return [str(item) for item in value if str(item).strip()]
+
+
def cmd_watch(args: argparse.Namespace) -> int:
config = load_config(args.config)
input_field = str(_config_value(args.input_field, config, "input_field", "prompt"))
diff --git a/redline/import_logs.py b/redline/import_logs.py
index bbaba31..1cf512e 100644
--- a/redline/import_logs.py
+++ b/redline/import_logs.py
@@ -10,6 +10,43 @@
_MISSING = object()
+IMPORT_PRESETS: dict[str, dict[str, object]] = {
+ "datadog": {
+ "input_field": "attributes.input",
+ "output_field": "attributes.output",
+ "metadata_fields": ["service", "trace_id", "attributes.model"],
+ },
+ "dolly": {
+ "input_field": "instruction",
+ "output_field": "response",
+ "context_field": "context",
+ "metadata_fields": ["category"],
+ },
+ "helicone": {
+ "input_field": "request.prompt",
+ "output_field": "response.text",
+ "metadata_fields": ["request.model", "response.model", "user_id"],
+ },
+ "langfuse": {
+ "input_field": "input",
+ "output_field": "output",
+ "metadata_fields": ["name", "traceId", "userId"],
+ },
+ "openai-chat": {
+ "input_field": "request.messages",
+ "output_field": "response.choices.0.message.content",
+ "metadata_fields": ["request.model", "response.model"],
+ },
+}
+
+
+def import_preset(name: str) -> dict[str, object]:
+ try:
+ return dict(IMPORT_PRESETS[name])
+ except KeyError as exc:
+ choices = ", ".join(sorted(IMPORT_PRESETS))
+ raise ValueError(f"unknown import preset: {name}; choose one of: {choices}") from exc
+
def import_jsonl_log(
path: str | Path,
@@ -115,6 +152,12 @@ def _get_field(row: dict[str, Any], path: str) -> Any:
return row[path]
current: Any = row
for part in path.split("."):
+ if isinstance(current, list) and part.isdigit():
+ index = int(part)
+ if index >= len(current):
+ return _MISSING
+ current = current[index]
+ continue
if not isinstance(current, dict) or part not in current:
return _MISSING
current = current[part]
diff --git a/redline/mcp.py b/redline/mcp.py
index 50d0190..107a87e 100644
--- a/redline/mcp.py
+++ b/redline/mcp.py
@@ -531,6 +531,7 @@ def _tools() -> list[ToolSpec]:
{
"path": _string("Source JSONL file to normalize."),
"out": _string("Redline JSONL output path."),
+ "preset": _string("Optional import preset such as langfuse, helicone, datadog, dolly, or openai-chat."),
"input_field": _string("Source field path containing prompt text."),
"output_field": _string("Source field path containing response text."),
"context_field": _string("Optional source field path appended to the prompt as Context."),
@@ -886,6 +887,7 @@ def _build_import(arguments: dict[str, Any]) -> list[str]:
args = ["import"]
_add_positional(args, _required_string(arguments, "path"))
_add_option(args, "--out", _required_string(arguments, "out"))
+ _add_option(args, "--preset", arguments.get("preset"))
_add_option(args, "--input-field", arguments.get("input_field"))
_add_option(args, "--output-field", arguments.get("output_field"))
_add_option(args, "--context-field", arguments.get("context_field"))
diff --git a/tests/test_cli_config.py b/tests/test_cli_config.py
index 54763dd..13590b6 100644
--- a/tests/test_cli_config.py
+++ b/tests/test_cli_config.py
@@ -2131,6 +2131,39 @@ def test_import_reports_default_redaction_and_supports_raw_opt_out(self) -> None
0,
)
self.assertIn("ada@example.com", Path("raw.jsonl").read_text(encoding="utf-8"))
+
+ Path("langfuse.jsonl").write_text(
+ json.dumps(
+ {
+ "input": "Summarize refund policy",
+ "output": "Refunds are available for 30 days.",
+ "traceId": "trace-123",
+ }
+ )
+ + "\n",
+ encoding="utf-8",
+ )
+ output = io.StringIO()
+ with contextlib.redirect_stdout(output):
+ self.assertEqual(
+ main(
+ [
+ "import",
+ "langfuse.jsonl",
+ "--preset",
+ "langfuse",
+ "--out",
+ "langfuse-baseline.jsonl",
+ ]
+ ),
+ 0,
+ )
+ text = output.getvalue()
+ self.assertIn("Preset: langfuse", text)
+ self.assertIn("Mapped prompt: input", text)
+ imported_row = json.loads(Path("langfuse-baseline.jsonl").read_text(encoding="utf-8"))
+ self.assertEqual(imported_row["prompt"], "Summarize refund policy")
+ self.assertEqual(imported_row["metadata"], {"traceId": "trace-123"})
finally:
os.chdir(previous)
diff --git a/tests/test_import_logs.py b/tests/test_import_logs.py
index 9f6172e..ee70019 100644
--- a/tests/test_import_logs.py
+++ b/tests/test_import_logs.py
@@ -68,6 +68,46 @@ def test_import_jsonl_log_limits_records(self) -> None:
self.assertEqual(report["records"], 1)
self.assertEqual(len(read_jsonl_records(output, "prompt", "response")), 1)
+ def test_import_jsonl_log_reads_nested_list_paths_for_chat_exports(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ source = root / "openai.jsonl"
+ output = root / "baseline.jsonl"
+ source.write_text(
+ json.dumps(
+ {
+ "request": {
+ "messages": [
+ {"role": "system", "content": "Use JSON."},
+ {"role": "user", "content": "Classify ticket INV-1042."},
+ ],
+ "model": "gpt-4o-mini",
+ },
+ "response": {
+ "choices": [
+ {"message": {"content": '{"owner": "billing"}'}},
+ ]
+ },
+ }
+ )
+ + "\n",
+ encoding="utf-8",
+ )
+
+ report = import_jsonl_log(
+ source,
+ output=output,
+ input_field="request.messages",
+ output_field="response.choices.0.message.content",
+ metadata_fields=["request.model"],
+ )
+
+ self.assertEqual(report["records"], 1)
+ records = read_jsonl_records(output, "prompt", "response")
+ self.assertIn("Classify ticket INV-1042", records[0].prompt)
+ self.assertEqual(records[0].response, '{"owner": "billing"}')
+ self.assertEqual(records[0].raw["metadata"], {"request.model": "gpt-4o-mini"})
+
def test_import_jsonl_log_redacts_common_secrets_by_default(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
diff --git a/tests/test_mcp.py b/tests/test_mcp.py
index 385b2fa..bab84e3 100644
--- a/tests/test_mcp.py
+++ b/tests/test_mcp.py
@@ -65,6 +65,7 @@ def test_import_tool_normalizes_external_jsonl(self) -> None:
"cwd": directory,
"path": str(source),
"out": str(output),
+ "preset": "dolly",
"input_field": "instruction",
"output_field": "response",
"context_field": "context",
@@ -77,6 +78,7 @@ def test_import_tool_normalizes_external_jsonl(self) -> None:
self.assertFalse(result["isError"])
self.assertEqual(result["structuredContent"]["exit_code"], 0)
self.assertEqual(result["structuredContent"]["json"]["records"], 1)
+ self.assertEqual(result["structuredContent"]["json"]["preset"], "dolly")
self.assertTrue(result["structuredContent"]["json"]["redacted"])
self.assertTrue(wrote_output)
From 3f553c28c790db0d2090b6865e1b86c661d310ed Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 31 May 2026 22:52:03 -0600
Subject: [PATCH 07/55] Add calibration examples
---
README.md | 1 +
docs/calibration.md | 40 ++++++++++++++++++++++++++++
docs/methodology.md | 2 ++
examples/calibration_baseline.jsonl | 4 +++
examples/calibration_candidate.jsonl | 4 +++
tests/test_calibration_examples.py | 31 +++++++++++++++++++++
tests/test_packaging.py | 11 ++++++++
7 files changed, 93 insertions(+)
create mode 100644 docs/calibration.md
create mode 100644 examples/calibration_baseline.jsonl
create mode 100644 examples/calibration_candidate.jsonl
create mode 100644 tests/test_calibration_examples.py
diff --git a/README.md b/README.md
index de3ead1..8891955 100644
--- a/README.md
+++ b/README.md
@@ -485,6 +485,7 @@ bash scripts/release_check.sh
- [docs/launch.md](docs/launch.md): public alpha launch plan
- [docs/troubleshooting.md](docs/troubleshooting.md): first-run and CI failure recovery
- [docs/methodology.md](docs/methodology.md): behavior grouping, case selection, scoring, and trust boundaries
+- [docs/calibration.md](docs/calibration.md): tiny fixture showing regressions, changed cases, and neutral cases
- [docs/commands.md](docs/commands.md): compact CLI command reference
- [docs/real-log-dogfood.md](docs/real-log-dogfood.md): redaction-first real-log test protocol
- [docs/dogfood.md](docs/dogfood.md): first-user dogfood protocol
diff --git a/docs/calibration.md b/docs/calibration.md
new file mode 100644
index 0000000..23676f0
--- /dev/null
+++ b/docs/calibration.md
@@ -0,0 +1,40 @@
+# Calibration Examples
+
+Use this fixture when you want to see redline's trust boundary in a tiny,
+inspectable dataset. It is not a benchmark. It is a calibration exercise that
+shows what deterministic checks catch, what they mark for review, and what a
+green case means.
+
+```bash
+redline suite examples/calibration_baseline.jsonl \
+ --out /tmp/redline-calibration-suite.json \
+ --all-cases
+
+redline diff /tmp/redline-calibration-suite.json \
+ examples/calibration_candidate.jsonl \
+ --fail-on none
+```
+
+Expected result:
+
+- Two regressions: lost JSON structure, and dropped refund details.
+- One changed case: the reply added apologetic tone.
+- One neutral case: the response is unchanged.
+
+The neutral case is deliberately boring. It means redline found no configured
+behavioral change in that case. It does not prove factual correctness,
+hallucination safety, policy compliance, or subtle reasoning quality.
+
+## Why This Matters
+
+The fixture demonstrates the intended operating model:
+
+- structural losses should block a prompt release;
+- tone or wording shifts should be reviewed before acceptance;
+- unchanged or neutral cases still inherit the global trust boundary;
+- must-cover product requirements should be pinned with `redline suite add` or
+ `redline require`.
+
+For semantic risks, add requirements or configure an optional judge. Keep the
+default deterministic checks as the fast local gate, and use judges where the
+product risk is not visible from structure or concrete detail loss.
diff --git a/docs/methodology.md b/docs/methodology.md
index 262b05f..6ea7304 100644
--- a/docs/methodology.md
+++ b/docs/methodology.md
@@ -102,3 +102,5 @@ Use redline as a safety loop:
The strongest evidence for redline is not a score. It is a concrete report that
says which production behavior changed and why it matters.
+
+For a tiny runnable boundary check, see [docs/calibration.md](calibration.md).
diff --git a/examples/calibration_baseline.jsonl b/examples/calibration_baseline.jsonl
new file mode 100644
index 0000000..4a2c9f4
--- /dev/null
+++ b/examples/calibration_baseline.jsonl
@@ -0,0 +1,4 @@
+{"prompt":"Return JSON with keys owner and priority for ticket INV-1042.","response":"{\"owner\":\"billing\",\"priority\":\"high\"}"}
+{"prompt":"Answer a customer asking about refunds. Include the 30 day window and policy URL https://docs.example.test/refunds.","response":"Refunds are available within 30 days. See https://docs.example.test/refunds for the full policy."}
+{"prompt":"Tell Sam that the export is ready.","response":"Sam, the export is ready in the dashboard."}
+{"prompt":"Confirm the workspace status in one sentence.","response":"The workspace remains available."}
diff --git a/examples/calibration_candidate.jsonl b/examples/calibration_candidate.jsonl
new file mode 100644
index 0000000..7461a9b
--- /dev/null
+++ b/examples/calibration_candidate.jsonl
@@ -0,0 +1,4 @@
+{"prompt":"Return JSON with keys owner and priority for ticket INV-1042.","response":"owner: billing, priority: high"}
+{"prompt":"Answer a customer asking about refunds. Include the 30 day window and policy URL https://docs.example.test/refunds.","response":"Refunds are available. See the policy page."}
+{"prompt":"Tell Sam that the export is ready.","response":"Sorry Sam, unfortunately the export is ready in the dashboard."}
+{"prompt":"Confirm the workspace status in one sentence.","response":"The workspace remains available."}
diff --git a/tests/test_calibration_examples.py b/tests/test_calibration_examples.py
new file mode 100644
index 0000000..16fdc19
--- /dev/null
+++ b/tests/test_calibration_examples.py
@@ -0,0 +1,31 @@
+import unittest
+
+from redline.diff import compare_suite_to_candidate
+from redline.io import read_jsonl_records
+from redline.suite import build_suite
+
+
+class CalibrationExampleTests(unittest.TestCase):
+ def test_calibration_fixture_shows_regression_changed_and_neutral(self) -> None:
+ baseline = read_jsonl_records("examples/calibration_baseline.jsonl", "prompt", "response")
+ candidate = read_jsonl_records("examples/calibration_candidate.jsonl", "prompt", "response")
+ suite = build_suite(
+ baseline,
+ source="examples/calibration_baseline.jsonl",
+ input_field="prompt",
+ output_field="response",
+ max_cases=10,
+ all_cases=True,
+ )
+
+ result = compare_suite_to_candidate(suite, candidate)
+ statuses = [item["status"] for item in result["diffs"]]
+ reasons = "\n".join(reason for item in result["diffs"] for reason in item["reasons"])
+
+ self.assertEqual(result["summary"]["regression"], 2)
+ self.assertEqual(result["summary"]["changed"], 1)
+ self.assertEqual(result["summary"]["neutral"], 1)
+ self.assertEqual(statuses, ["regression", "regression", "changed", "neutral"])
+ self.assertIn("candidate lost valid JSON format", reasons)
+ self.assertIn("candidate missing URLs", reasons)
+ self.assertIn("tone changed", reasons)
diff --git a/tests/test_packaging.py b/tests/test_packaging.py
index 6b303cf..1d7fb02 100644
--- a/tests/test_packaging.py
+++ b/tests/test_packaging.py
@@ -113,6 +113,17 @@ def test_methodology_doc_calibrates_behavior_grouping_and_score(self) -> None:
self.assertIn("not a model-quality score", text)
self.assertIn("docs/methodology.md", readme)
+ def test_calibration_doc_links_runnable_fixture(self) -> None:
+ text = Path("docs/calibration.md").read_text(encoding="utf-8")
+ readme = Path("README.md").read_text(encoding="utf-8")
+
+ self.assertIn("examples/calibration_baseline.jsonl", text)
+ self.assertIn("examples/calibration_candidate.jsonl", text)
+ self.assertIn("Two regressions", text)
+ self.assertIn("One changed case", text)
+ self.assertIn("One neutral case", text)
+ self.assertIn("docs/calibration.md", readme)
+
def test_release_check_builds_and_smokes_installed_wheel(self) -> None:
script = Path("scripts/release_check.sh").read_text(encoding="utf-8")
From 82995e53297b06cb09f8c9f0da309d9956b6fa95 Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 31 May 2026 22:54:43 -0600
Subject: [PATCH 08/55] Surface diff profile calibration
---
docs/calibration.md | 8 ++++++++
docs/commands.md | 9 +++++++++
redline/diff.py | 15 +++++++++++++++
tests/test_diff.py | 6 ++++++
4 files changed, 38 insertions(+)
diff --git a/docs/calibration.md b/docs/calibration.md
index 23676f0..6b91980 100644
--- a/docs/calibration.md
+++ b/docs/calibration.md
@@ -38,3 +38,11 @@ The fixture demonstrates the intended operating model:
For semantic risks, add requirements or configure an optional judge. Keep the
default deterministic checks as the fast local gate, and use judges where the
product risk is not visible from structure or concrete detail loss.
+
+For exploratory data with noisy entity extraction, compare strict and review
+profiles:
+
+```bash
+redline diff /tmp/redline-calibration-suite.json examples/calibration_candidate.jsonl --profile strict --fail-on none
+redline diff /tmp/redline-calibration-suite.json examples/calibration_candidate.jsonl --profile review --fail-on none
+```
diff --git a/docs/commands.md b/docs/commands.md
index 8b9f466..ff202e5 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -51,3 +51,12 @@ output and examples after installing.
Structural checks are deterministic and local. Green or neutral does not prove
semantic equivalence. Use `redline require`, `redline mark`, and optional judge
templates for factual, tone, hallucination, policy, or reasoning risks.
+
+## Diff profiles
+
+- `--profile strict` is the default CI-oriented mode. Missing concrete details
+ such as numbers and likely entities are blocking regressions.
+- `--profile review` is a softer calibration mode for noisy exploratory logs.
+ Missing numbers and likely entities become reviewable `changed` cases while
+ hard structural losses, such as invalid JSON, missing JSON keys, empty
+ outputs, refusals, tables, lists, code blocks, and URLs, stay blocking.
diff --git a/redline/diff.py b/redline/diff.py
index bf19d1d..2e994aa 100644
--- a/redline/diff.py
+++ b/redline/diff.py
@@ -544,6 +544,10 @@ def format_report(result: dict[str, Any], *, title: str = "redline diff") -> str
f" MISSING {summary['missing']:>3}",
"",
]
+ profile = str(result.get("profile") or "")
+ if profile:
+ lines.append(f"Profile: {profile} ({_profile_description(profile)})")
+ lines.append("")
decision = result.get("decision")
if isinstance(decision, dict):
confidence = str(decision.get("confidence") or "").upper()
@@ -596,6 +600,9 @@ def format_compact_report(result: dict[str, Any], *, title: str = "redline diff"
f"neutral={summary['neutral']}"
)
]
+ profile = str(result.get("profile") or "")
+ if profile:
+ lines.append(f"Profile: {profile} ({_profile_description(profile)})")
decision = result.get("decision")
if isinstance(decision, dict):
confidence = str(decision.get("confidence") or "").upper()
@@ -671,6 +678,14 @@ def _prompt_eval_lines(value: Any) -> list[str]:
return rows
+def _profile_description(profile: str) -> str:
+ if profile == "review":
+ return "detail/entity loss becomes reviewable changed signal"
+ if profile == "strict":
+ return "detail/entity loss is blocking"
+ return "custom"
+
+
def _prompt_eval_status(summary: dict[str, Any]) -> str:
if _summary_count(summary, "regression") or _summary_count(summary, "missing"):
return "REGRESSION"
diff --git a/tests/test_diff.py b/tests/test_diff.py
index 5cdc3ee..6c832df 100644
--- a/tests/test_diff.py
+++ b/tests/test_diff.py
@@ -446,11 +446,14 @@ def test_format_report_includes_decision(self) -> None:
"diagnosis": "No structural blockers were detected; still review semantic risks.",
},
"warnings": ["prompt file prompts/v2.txt is newer than suite"],
+ "profile": "review",
"diffs": [],
}
report = format_report(result)
+ self.assertIn("Profile: review", report)
+ self.assertIn("detail/entity loss becomes reviewable changed signal", report)
self.assertIn("Confidence: MEDIUM", report)
self.assertIn("Recommended action: no structural blockers detected; review semantic risks before shipping", report)
self.assertIn("Scope: structural checks only", report)
@@ -476,6 +479,7 @@ def test_format_compact_report_outputs_one_line_per_actionable_case(self) -> Non
"scope": "structural checks only; review semantic risks separately",
"diagnosis": "Candidate lost required structure; fix blocking cases before shipping.",
},
+ "profile": "strict",
"prompt_evals": [
{
"id": "support/triage",
@@ -517,6 +521,8 @@ def test_format_compact_report_outputs_one_line_per_actionable_case(self) -> Non
report = format_compact_report(result, title="redline eval")
self.assertIn("redline eval: cases=2 regression=1 changed=1", report)
+ self.assertIn("Profile: strict", report)
+ self.assertIn("detail/entity loss is blocking", report)
self.assertIn("Confidence: HIGH | fix blocking cases before shipping", report)
self.assertIn("Scope: structural checks only", report)
self.assertIn("Diagnosis: Candidate lost required structure; fix blocking cases before shipping.", report)
From d57a55953148558eb6090f5444018c129e415574 Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 31 May 2026 22:57:15 -0600
Subject: [PATCH 09/55] Clarify suite readiness scope
---
redline/summary.py | 1 +
tests/test_summary.py | 1 +
2 files changed, 2 insertions(+)
diff --git a/redline/summary.py b/redline/summary.py
index bb00e92..f35ff55 100644
--- a/redline/summary.py
+++ b/redline/summary.py
@@ -335,6 +335,7 @@ def format_suite_summary(suite: dict[str, Any], *, suite_path: str | None = None
f"Representative cases: {summary['cases']}",
f"Case coverage: {summary['cases']}/{summary['unique_prompt_response_pairs']} ({_percent(summary['case_coverage'])})",
f"Suite readiness: {_format_readiness(summary['suite_readiness'])}",
+ "Readiness scope: suite health, not model quality or candidate safety",
f"Pinned cases: {summary['pinned_cases']}",
f"Owned cases: {summary['owned_cases']}/{summary['cases']}",
f"Owner rule coverage: {summary['owner_rule_cases']}/{summary['owned_cases']} ({_percent(summary['owner_rule_coverage'])})",
diff --git a/tests/test_summary.py b/tests/test_summary.py
index bc2536a..bf04081 100644
--- a/tests/test_summary.py
+++ b/tests/test_summary.py
@@ -101,6 +101,7 @@ def test_format_suite_summary_is_readable(self) -> None:
self.assertIn("Group coverage:", output)
self.assertIn("Case coverage:", output)
self.assertIn("Suite readiness:", output)
+ self.assertIn("Readiness scope: suite health, not model quality or candidate safety", output)
self.assertIn("Pinned cases:", output)
self.assertIn("Owned cases:", output)
self.assertIn("Owner rule coverage:", output)
From 2e0e8b8e6b43519c3cd7be9ead68264569c672f6 Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 31 May 2026 23:03:42 -0600
Subject: [PATCH 10/55] Expose import preset discovery
---
docs/commands.md | 2 +-
docs/mcp.md | 1 +
docs/real-log-dogfood.md | 1 +
redline/cli.py | 23 ++++++++++++++++++---
redline/import_logs.py | 43 +++++++++++++++++++++++++++++++++++++++
redline/mcp.py | 12 +++++++++++
tests/test_cli_config.py | 17 ++++++++++++++++
tests/test_import_logs.py | 13 +++++++++++-
tests/test_mcp.py | 10 +++++++++
9 files changed, 117 insertions(+), 5 deletions(-)
diff --git a/docs/commands.md b/docs/commands.md
index ff202e5..05056d4 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -10,7 +10,7 @@ output and examples after installing.
| `redline init` | Write `redline.json`, runner files, and optional CI workflow. | `--runner`, `--copy-runner`, `--github-action`, `--force` |
| `redline doctor` | Check config, suite, replay, reports, audit, and team workflow. | `--strict`, `--json` |
| `redline watch` | Collect prompt-response observations or print snippets. | `--log`, `--stats`, `--snippet`, `--follow` |
-| `redline import` | Normalize exported JSONL fields into redline `prompt`/`response` logs, with best-effort redaction on by default. | `--preset`, `--input-field`, `--output-field`, `--context-field`, `--metadata-field`, `--limit`, `--out`, `--no-redact` |
+| `redline import` | Normalize exported JSONL fields into redline `prompt`/`response` logs, with best-effort redaction on by default. | `--list-presets`, `--preset`, `--input-field`, `--output-field`, `--context-field`, `--metadata-field`, `--limit`, `--out`, `--no-redact` |
| `redline redact` | Check or write best-effort redacted logs. | `--check`, `--out`, `--json` |
| `redline cluster` | Inspect deterministic behavior-signature groups before suite generation. | `--max-cases`, `--json` |
| `redline suite` | Generate a suite from baseline JSONL logs. | `--out`, `--max-cases`, `--all-cases`, `--owner` |
diff --git a/docs/mcp.md b/docs/mcp.md
index 1350bea..7316130 100644
--- a/docs/mcp.md
+++ b/docs/mcp.md
@@ -67,6 +67,7 @@ Available tools:
- `redline_suite`
- `redline_redact`
- `redline_import`
+- `redline_import_presets`
- `redline_watch_stats`
- `redline_watch_snippets`
- `redline_prompts`
diff --git a/docs/real-log-dogfood.md b/docs/real-log-dogfood.md
index 85e9b6d..a313f0e 100644
--- a/docs/real-log-dogfood.md
+++ b/docs/real-log-dogfood.md
@@ -33,6 +33,7 @@ For common exports, start with a preset and override fields only when your file
differs:
```bash
+redline import --list-presets
redline import raw-export.jsonl --preset langfuse --out .redline/dogfood/baseline.jsonl
redline import raw-export.jsonl --preset helicone --out .redline/dogfood/baseline.jsonl
redline import raw-export.jsonl --preset openai-chat --out .redline/dogfood/baseline.jsonl
diff --git a/redline/cli.py b/redline/cli.py
index 6f3ce08..be15dfa 100644
--- a/redline/cli.py
+++ b/redline/cli.py
@@ -63,7 +63,13 @@
read_history,
should_fail_history,
)
-from .import_logs import IMPORT_PRESETS, import_jsonl_log, import_preset
+from .import_logs import (
+ IMPORT_PRESETS,
+ format_import_presets,
+ import_jsonl_log,
+ import_preset,
+ import_preset_rows,
+)
from .io import append_jsonl, append_text, read_json, read_jsonl_records, write_json, write_jsonl, write_text
from .judge import apply_judge
from .judge_templates import (
@@ -238,7 +244,8 @@ def build_parser() -> argparse.ArgumentParser:
runners_parser.set_defaults(func=cmd_runners)
import_parser = subparsers.add_parser("import", help="normalize exported JSONL logs into redline format")
- import_parser.add_argument("path", help="source JSONL file to normalize")
+ import_parser.add_argument("path", nargs="?", help="source JSONL file to normalize")
+ import_parser.add_argument("--list-presets", action="store_true", help="list built-in import presets")
import_parser.add_argument("--preset", choices=sorted(IMPORT_PRESETS), help="field mapping preset for common log exports")
import_parser.add_argument("--input-field", help="source field path containing prompt text")
import_parser.add_argument("--output-field", help="source field path containing response text")
@@ -251,7 +258,7 @@ def build_parser() -> argparse.ArgumentParser:
help="source field path copied into metadata; repeat for multiple fields",
)
import_parser.add_argument("--limit", type=int, help="maximum records to import")
- import_parser.add_argument("--out", required=True, help="redline JSONL output path")
+ import_parser.add_argument("--out", help="redline JSONL output path")
import_parser.add_argument("--no-redact", action="store_true", help="write raw values without import redaction")
import_parser.add_argument("--redaction-placeholder", default=DEFAULT_PLACEHOLDER, help="replacement text for import redaction")
import_parser.add_argument("--json", action="store_true", help="print machine-readable JSON")
@@ -732,6 +739,16 @@ def cmd_judges(args: argparse.Namespace) -> int:
def cmd_import(args: argparse.Namespace) -> int:
+ if args.list_presets:
+ if args.json:
+ print(json.dumps({"presets": import_preset_rows()}, indent=2, sort_keys=True))
+ else:
+ print(format_import_presets(), end="")
+ return 0
+ if not args.path:
+ raise ValueError("import path is required unless --list-presets is used")
+ if not args.out:
+ raise ValueError("--out is required unless --list-presets is used")
preset = import_preset(args.preset) if args.preset else {}
input_field = args.input_field or str(preset.get("input_field") or "prompt")
output_field = args.output_field or str(preset.get("output_field") or "response")
diff --git a/redline/import_logs.py b/redline/import_logs.py
index 1cf512e..807ed38 100644
--- a/redline/import_logs.py
+++ b/redline/import_logs.py
@@ -48,6 +48,43 @@ def import_preset(name: str) -> dict[str, object]:
raise ValueError(f"unknown import preset: {name}; choose one of: {choices}") from exc
+def import_preset_rows() -> list[dict[str, Any]]:
+ rows = []
+ for name, preset in sorted(IMPORT_PRESETS.items()):
+ rows.append(
+ {
+ "id": name,
+ "input_field": str(preset.get("input_field") or ""),
+ "output_field": str(preset.get("output_field") or ""),
+ "context_field": str(preset.get("context_field") or ""),
+ "metadata_fields": [str(value) for value in _preset_list(preset.get("metadata_fields"))],
+ }
+ )
+ return rows
+
+
+def format_import_presets() -> str:
+ lines = [
+ "redline import presets",
+ "",
+ f"{'PRESET':<12} {'PROMPT FIELD':<28} {'RESPONSE FIELD':<38} METADATA",
+ f"{'-' * 12} {'-' * 28} {'-' * 38} {'-' * 24}",
+ ]
+ for row in import_preset_rows():
+ metadata = ", ".join(row["metadata_fields"])
+ lines.append(
+ f"{row['id']:<12} {row['input_field']:<28} {row['output_field']:<38} {metadata}"
+ )
+ lines.extend(
+ [
+ "",
+ "Use: redline import raw.jsonl --preset langfuse --out .redline/logs/prompts.jsonl",
+ "Override any preset field with --input-field, --output-field, --context-field, or --metadata-field.",
+ ]
+ )
+ return "\n".join(lines).rstrip() + "\n"
+
+
def import_jsonl_log(
path: str | Path,
*,
@@ -174,3 +211,9 @@ def _stringify_response(value: Any) -> str:
if isinstance(value, str):
return value.strip()
return _stringify(value)
+
+
+def _preset_list(value: object) -> list[object]:
+ if not isinstance(value, list):
+ return []
+ return value
diff --git a/redline/mcp.py b/redline/mcp.py
index 107a87e..b8133c3 100644
--- a/redline/mcp.py
+++ b/redline/mcp.py
@@ -613,6 +613,12 @@ def _tools() -> list[ToolSpec]:
),
_build_judges,
),
+ ToolSpec(
+ "redline_import_presets",
+ "List built-in log import presets and their field mappings.",
+ _schema({"json": _boolean("Print machine-readable JSON.")}),
+ _build_import_presets,
+ ),
ToolSpec(
"redline_runners",
"List or copy runner adapters for replay commands, log import, and SDK capture.",
@@ -936,6 +942,12 @@ def _build_judges(arguments: dict[str, Any]) -> list[str]:
return args
+def _build_import_presets(arguments: dict[str, Any]) -> list[str]:
+ args = ["import", "--list-presets"]
+ _add_flag(args, "--json", arguments.get("json"))
+ return args
+
+
def _build_runners(arguments: dict[str, Any]) -> list[str]:
args = ["runners"]
_add_option(args, "--copy", arguments.get("copy"))
diff --git a/tests/test_cli_config.py b/tests/test_cli_config.py
index 13590b6..6b5ff9f 100644
--- a/tests/test_cli_config.py
+++ b/tests/test_cli_config.py
@@ -220,6 +220,23 @@ def test_runners_command_refuses_out_with_copy_all(self) -> None:
finally:
os.chdir(previous)
+ def test_import_command_lists_presets(self) -> None:
+ output = io.StringIO()
+
+ with contextlib.redirect_stdout(output):
+ self.assertEqual(main(["import", "--list-presets"]), 0)
+
+ text = output.getvalue()
+ self.assertIn("redline import presets", text)
+ self.assertIn("langfuse", text)
+ self.assertIn("openai-chat", text)
+
+ json_output = io.StringIO()
+ with contextlib.redirect_stdout(json_output):
+ self.assertEqual(main(["import", "--list-presets", "--json"]), 0)
+ payload = json.loads(json_output.getvalue())
+ self.assertTrue(any(row["id"] == "helicone" for row in payload["presets"]))
+
def test_judges_command_lists_templates(self) -> None:
output = io.StringIO()
diff --git a/tests/test_import_logs.py b/tests/test_import_logs.py
index ee70019..b8125d6 100644
--- a/tests/test_import_logs.py
+++ b/tests/test_import_logs.py
@@ -5,11 +5,22 @@
import unittest
from pathlib import Path
-from redline.import_logs import import_jsonl_log
+from redline.import_logs import format_import_presets, import_jsonl_log, import_preset_rows
from redline.io import read_jsonl_records
class ImportLogTests(unittest.TestCase):
+ def test_import_preset_rows_are_human_discoverable(self) -> None:
+ rows = import_preset_rows()
+ output = format_import_presets()
+
+ self.assertTrue(any(row["id"] == "langfuse" for row in rows))
+ self.assertTrue(any(row["id"] == "openai-chat" for row in rows))
+ self.assertIn("redline import presets", output)
+ self.assertIn("langfuse", output)
+ self.assertIn("openai-chat", output)
+ self.assertIn("redline import raw.jsonl --preset langfuse", output)
+
def test_import_jsonl_log_maps_external_fields_and_context(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
diff --git a/tests/test_mcp.py b/tests/test_mcp.py
index bab84e3..1bae668 100644
--- a/tests/test_mcp.py
+++ b/tests/test_mcp.py
@@ -32,6 +32,7 @@ def test_tools_list_exposes_redline_tools_with_guarded_writes(self) -> None:
self.assertIn("redline_suite", names)
self.assertIn("redline_redact", names)
self.assertIn("redline_import", names)
+ self.assertIn("redline_import_presets", names)
self.assertIn("redline_watch_stats", names)
self.assertIn("redline_watch_snippets", names)
self.assertIn("redline_prompts", names)
@@ -82,6 +83,15 @@ def test_import_tool_normalizes_external_jsonl(self) -> None:
self.assertTrue(result["structuredContent"]["json"]["redacted"])
self.assertTrue(wrote_output)
+ def test_import_presets_tool_lists_mappings(self) -> None:
+ result = call_tool("redline_import_presets", {"json": True})
+
+ self.assertFalse(result["isError"])
+ self.assertEqual(result["structuredContent"]["exit_code"], 0)
+ presets = result["structuredContent"]["json"]["presets"]
+ self.assertTrue(any(row["id"] == "langfuse" for row in presets))
+ self.assertTrue(any(row["id"] == "openai-chat" for row in presets))
+
def test_eval_and_diff_tools_do_not_accept_dynamic_commands(self) -> None:
response = handle_jsonrpc_line(
json.dumps({"jsonrpc": "2.0", "id": 20, "method": "tools/list"})
From 21a7c60efaaeec63d3c82784d143e3dfff43711f Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 31 May 2026 23:06:51 -0600
Subject: [PATCH 11/55] Record suite selection methodology
---
docs/methodology.md | 4 ++++
redline-suite.schema.json | 11 +++++++++++
redline/suite.py | 13 +++++++++++++
redline/summary.py | 15 +++++++++++++++
tests/test_suite.py | 6 +++++-
tests/test_summary.py | 4 ++++
6 files changed, 52 insertions(+), 1 deletion(-)
diff --git a/docs/methodology.md b/docs/methodology.md
index 6ea7304..298dd9a 100644
--- a/docs/methodology.md
+++ b/docs/methodology.md
@@ -46,6 +46,10 @@ Suite generation chooses cases in this order:
This strategy optimizes for useful regression coverage from existing logs, not
perfect coverage of every product risk. If a scenario matters, pin it.
+Generated suites include a `methodology` block with the deterministic
+case-selection method and version, currently `behavior-signature-v1`, so
+reports can be audited against the algorithm that selected the suite.
+
## Regression Signals
The deterministic diff engine checks for high-signal changes:
diff --git a/redline-suite.schema.json b/redline-suite.schema.json
index e786323..cd1b20f 100644
--- a/redline-suite.schema.json
+++ b/redline-suite.schema.json
@@ -31,6 +31,17 @@
"type": "string",
"description": "JSONL field path used as the baseline response/output."
},
+ "methodology": {
+ "type": "object",
+ "description": "Deterministic grouping and case-selection method used to generate this suite.",
+ "properties": {
+ "name": {"type": "string"},
+ "version": {"type": "string"},
+ "trust_scope": {"type": "string"},
+ "case_selection": {"type": "array", "items": {"type": "string"}}
+ },
+ "additionalProperties": true
+ },
"summary": {
"type": "object",
"required": ["records_seen", "unique_prompt_response_pairs", "clusters", "cases", "selection"],
diff --git a/redline/suite.py b/redline/suite.py
index f27851e..9cd21a5 100644
--- a/redline/suite.py
+++ b/redline/suite.py
@@ -16,6 +16,18 @@
ClusterInfo = dict[str, Any]
SUITE_SCHEMA_URL = "https://raw.githubusercontent.com/gowtham0992/redline/main/redline-suite.schema.json"
PROMPT_DIVERSITY_EDGE_TARGET = 8
+SELECTION_METHODOLOGY_VERSION = "behavior-signature-v1"
+SELECTION_METHODOLOGY = {
+ "name": "deterministic behavior-signature grouping",
+ "version": SELECTION_METHODOLOGY_VERSION,
+ "trust_scope": "structural checks only; review factual, tone, hallucination, policy, and reasoning risks separately",
+ "case_selection": [
+ "one representative per behavior-signature group",
+ "high-risk groups first when case budget is tight",
+ "high-variance edge cases when budget remains",
+ "prompt-diverse samples from large groups when budget remains",
+ ],
+}
def build_suite(
@@ -99,6 +111,7 @@ def build_suite(
"source": str(source),
"input_field": input_field,
"output_field": output_field,
+ "methodology": dict(SELECTION_METHODOLOGY),
"summary": {
"records_seen": len(records),
"unique_prompt_response_pairs": len(unique_records),
diff --git a/redline/summary.py b/redline/summary.py
index f35ff55..b284a29 100644
--- a/redline/summary.py
+++ b/redline/summary.py
@@ -59,6 +59,8 @@ def suite_summary(suite: dict[str, Any]) -> dict[str, Any]:
summary = suite.get("summary", {})
if not isinstance(summary, dict):
summary = {}
+ methodology = suite.get("methodology")
+ methodology = methodology if isinstance(methodology, dict) else {}
cases = suite.get("cases", [])
if not isinstance(cases, list):
cases = []
@@ -121,6 +123,8 @@ def suite_summary(suite: dict[str, Any]) -> dict[str, Any]:
"source": str(suite.get("source") or ""),
"created_at": str(suite.get("created_at") or ""),
"selection": str(summary.get("selection") or ""),
+ "methodology_version": str(methodology.get("version") or ""),
+ "methodology_name": str(methodology.get("name") or ""),
"records_seen": records_seen,
"unique_prompt_response_pairs": unique_pairs,
"duplicate_prompt_response_pairs": int(summary.get("duplicate_prompt_response_pairs", 0)),
@@ -327,6 +331,7 @@ def format_suite_summary(suite: dict[str, Any], *, suite_path: str | None = None
f"Source: {summary['source'] or ''}",
f"Created: {summary['created_at'] or ''}",
f"Selection: {summary['selection'] or ''}",
+ f"Methodology: {_methodology_label(summary)}",
f"Records seen: {summary['records_seen']}",
f"Unique pairs: {summary['unique_prompt_response_pairs']}",
f"Duplicate pairs: {summary['duplicate_prompt_response_pairs']}",
@@ -577,6 +582,16 @@ def _format_readiness(value: object) -> str:
return f"{score}/100 ({label})"
+def _methodology_label(summary: dict[str, Any]) -> str:
+ name = str(summary.get("methodology_name") or "").strip()
+ version = str(summary.get("methodology_version") or "").strip()
+ if name and version:
+ return f"{name} ({version})"
+ if version:
+ return version
+ return ""
+
+
def _manifest_summary_status(summary: dict[str, Any]) -> str:
if int(summary["invalid_suite_count"]):
return "invalid"
diff --git a/tests/test_suite.py b/tests/test_suite.py
index ee5d9d7..ee867fb 100644
--- a/tests/test_suite.py
+++ b/tests/test_suite.py
@@ -5,7 +5,7 @@
from redline.features import extract_features
from redline.io import LogRecord
-from redline.suite import SUITE_SCHEMA_URL, add_suite_case, build_suite
+from redline.suite import SELECTION_METHODOLOGY_VERSION, SUITE_SCHEMA_URL, add_suite_case, build_suite
class SuiteTests(unittest.TestCase):
@@ -26,6 +26,9 @@ def test_build_suite_groups_behavioral_clusters(self) -> None:
self.assertEqual(suite["summary"]["records_seen"], 3)
self.assertEqual(suite["$schema"], SUITE_SCHEMA_URL)
+ self.assertEqual(suite["methodology"]["version"], SELECTION_METHODOLOGY_VERSION)
+ self.assertIn("behavior-signature", suite["methodology"]["name"])
+ self.assertIn("case_selection", suite["methodology"])
self.assertEqual(suite["summary"]["unique_prompt_response_pairs"], 3)
self.assertEqual(suite["summary"]["duplicate_prompt_response_pairs"], 0)
self.assertEqual(suite["summary"]["cases"], 2)
@@ -57,6 +60,7 @@ def test_suite_schema_documents_generated_suite_fields(self) -> None:
self.assertIn("Portable prompt-response regression suite", schema["description"])
for key in ("summary", "clusters", "cases"):
self.assertIn(key, schema["properties"])
+ self.assertIn("methodology", schema["properties"])
case_properties = schema["properties"]["cases"]["items"]["properties"]
self.assertIn("selection_reason", case_properties)
self.assertIn("cluster_risk", case_properties)
diff --git a/tests/test_summary.py b/tests/test_summary.py
index bf04081..1dbe426 100644
--- a/tests/test_summary.py
+++ b/tests/test_summary.py
@@ -33,6 +33,8 @@ def test_suite_summary_counts_cases_clusters_and_judgments(self) -> None:
self.assertEqual(summary["source"], "logs/baseline.jsonl")
self.assertEqual(summary["selection"], "representative")
+ self.assertEqual(summary["methodology_version"], "behavior-signature-v1")
+ self.assertEqual(summary["methodology_name"], "deterministic behavior-signature grouping")
self.assertEqual(summary["records_seen"], 2)
self.assertEqual(summary["unique_prompt_response_pairs"], 2)
self.assertEqual(summary["duplicate_prompt_response_pairs"], 0)
@@ -95,6 +97,8 @@ def test_format_suite_summary_is_readable(self) -> None:
self.assertIn("redline summary", output)
self.assertIn("Source:", output)
self.assertIn("Selection:", output)
+ self.assertIn("Methodology:", output)
+ self.assertIn("behavior-signature-v1", output)
self.assertIn("Records seen:", output)
self.assertIn("Unique pairs:", output)
self.assertIn("Duplicate pairs:", output)
From 79a58f834b9bdd6462060b8abdb84f2b297ac473 Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 31 May 2026 23:09:26 -0600
Subject: [PATCH 12/55] Improve log field diagnostics
---
redline/import_logs.py | 7 ++++++-
redline/io.py | 17 +++++++++++++----
tests/test_import_logs.py | 2 +-
tests/test_io.py | 8 ++++++++
4 files changed, 28 insertions(+), 6 deletions(-)
diff --git a/redline/import_logs.py b/redline/import_logs.py
index 807ed38..ed14b1e 100644
--- a/redline/import_logs.py
+++ b/redline/import_logs.py
@@ -155,7 +155,12 @@ def _required_field(
) -> Any:
value = _get_field(row, path)
if value is _MISSING:
- raise ValueError(f"{source}:{line_number} missing {label} field: {path}")
+ fields = ", ".join(sorted(str(key) for key in row)[:8]) or ""
+ raise ValueError(
+ f"{source}:{line_number} missing {label} field: {path}. "
+ f"Available top-level fields: {fields}. "
+ "Run `redline import --list-presets` or override the field path."
+ )
return value
diff --git a/redline/io.py b/redline/io.py
index 6a0ba9a..c45a9d1 100644
--- a/redline/io.py
+++ b/redline/io.py
@@ -19,8 +19,7 @@ def read_jsonl_records(path: str | Path, input_field: str, output_field: str) ->
for line_number, obj in iter_jsonl(path):
missing = [field for field in (input_field, output_field) if _get_field(obj, field) is _MISSING]
if missing:
- joined = ", ".join(missing)
- raise ValueError(f"{path}:{line_number} missing required field(s): {joined}")
+ raise ValueError(_missing_fields_message(path, line_number, obj, missing))
prompt = _get_field(obj, input_field)
response = _get_field(obj, output_field)
records.append(
@@ -63,8 +62,7 @@ def read_jsonl_records_from_offset(
obj = _parse_jsonl_object(path, line_number, stripped)
missing = [field for field in (input_field, output_field) if _get_field(obj, field) is _MISSING]
if missing:
- joined = ", ".join(missing)
- raise ValueError(f"{path}:{line_number} missing required field(s): {joined}")
+ raise ValueError(_missing_fields_message(path, line_number, obj, missing))
prompt = _get_field(obj, input_field)
response = _get_field(obj, output_field)
records.append(
@@ -183,3 +181,14 @@ def _get_field(obj: dict[str, Any], field: str) -> Any:
return _MISSING
current = current[part]
return current
+
+
+def _missing_fields_message(path: str | Path, line_number: int, obj: dict[str, Any], missing: list[str]) -> str:
+ joined = ", ".join(missing)
+ fields = ", ".join(sorted(str(key) for key in obj)[:8]) or ""
+ return (
+ f"{path}:{line_number} missing required field(s): {joined}. "
+ f"Available top-level fields: {fields}. "
+ "If this is an exported provider log, run "
+ "`redline import --list-presets` or pass --input-field/--output-field."
+ )
diff --git a/tests/test_import_logs.py b/tests/test_import_logs.py
index b8125d6..d2eb0bb 100644
--- a/tests/test_import_logs.py
+++ b/tests/test_import_logs.py
@@ -180,5 +180,5 @@ def test_import_jsonl_log_reports_missing_fields(self) -> None:
source = root / "downloaded.jsonl"
source.write_text('{"instruction": "missing response"}\n', encoding="utf-8")
- with self.assertRaisesRegex(ValueError, "missing output field: response"):
+ with self.assertRaisesRegex(ValueError, "redline import --list-presets"):
import_jsonl_log(source, output=root / "baseline.jsonl", input_field="instruction")
diff --git a/tests/test_io.py b/tests/test_io.py
index 3a763b0..4bddd58 100644
--- a/tests/test_io.py
+++ b/tests/test_io.py
@@ -22,6 +22,14 @@ def test_missing_jsonl_file_raises_value_error(self) -> None:
with self.assertRaisesRegex(ValueError, "not found"):
read_jsonl_records("missing.jsonl", "prompt", "response")
+ def test_missing_jsonl_fields_suggest_import_presets(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ path = Path(directory) / "provider.jsonl"
+ path.write_text('{"instruction": "Summarize", "completion": "Done"}\n', encoding="utf-8")
+
+ with self.assertRaisesRegex(ValueError, "redline import --list-presets"):
+ read_jsonl_records(path, "prompt", "response")
+
def test_read_json_gives_jsonl_next_step_for_extra_data(self) -> None:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "baseline.jsonl"
From f32a10ff19ed97b034f3d0506809a0dbe76e755e Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 31 May 2026 23:17:07 -0600
Subject: [PATCH 13/55] Carry methodology into reports
---
docs/methodology.md | 4 +++-
redline-report.schema.json | 10 ++++++++++
redline/dashboard.py | 19 +++++++++++++++++++
redline/diff.py | 12 ++++++++++++
redline/reports.py | 27 +++++++++++++++++++++++++++
tests/test_dashboard.py | 10 ++++++++++
tests/test_diff.py | 16 ++++++++++++++++
tests/test_reports.py | 11 +++++++++++
8 files changed, 108 insertions(+), 1 deletion(-)
diff --git a/docs/methodology.md b/docs/methodology.md
index 298dd9a..689c614 100644
--- a/docs/methodology.md
+++ b/docs/methodology.md
@@ -48,7 +48,9 @@ perfect coverage of every product risk. If a scenario matters, pin it.
Generated suites include a `methodology` block with the deterministic
case-selection method and version, currently `behavior-signature-v1`, so
-reports can be audited against the algorithm that selected the suite.
+reports can be audited against the algorithm that selected the suite. Diff
+and eval reports copy that block into JSON, Markdown, HTML, and dashboard
+surfaces so reviewers can see the trust method beside the result.
## Regression Signals
diff --git a/redline-report.schema.json b/redline-report.schema.json
index 6c70357..d163da6 100644
--- a/redline-report.schema.json
+++ b/redline-report.schema.json
@@ -19,6 +19,16 @@
"enum": ["strict", "review"],
"description": "Diff profile used to classify missing numbers and entities."
},
+ "methodology": {
+ "type": "object",
+ "description": "Suite selection methodology copied from the suite, when available.",
+ "properties": {
+ "name": {"type": "string"},
+ "version": {"type": "string"},
+ "trust_scope": {"type": "string"}
+ },
+ "additionalProperties": true
+ },
"summary": {
"type": "object",
"required": ["cases", "regression", "changed", "improved", "accepted", "ignored", "neutral", "missing"],
diff --git a/redline/dashboard.py b/redline/dashboard.py
index e5731fd..01eeea3 100644
--- a/redline/dashboard.py
+++ b/redline/dashboard.py
@@ -152,6 +152,7 @@ def _collect_reports(reports_dir: Path, *, limit: int) -> tuple[list[dict[str, A
"kind": _report_kind(report, path),
"summary": _summary_counts(summary),
"decision": report.get("decision") if isinstance(report.get("decision"), dict) else {},
+ "methodology": report.get("methodology") if isinstance(report.get("methodology"), dict) else {},
"owners": _report_owner_review(report.get("diffs"), suite_path=str(report.get("suite") or "")),
"trust": _report_trust_summary(report.get("diffs")),
"review": _report_review_summary(report.get("diffs")),
@@ -523,6 +524,7 @@ def _dashboard_trust_summary(reports: list[dict[str, Any]]) -> dict[str, Any]:
cases = 0
confidence: dict[str, int] = {}
signal: dict[str, int] = {}
+ methodology: dict[str, int] = {}
for report in reports:
trust = report.get("trust")
if not isinstance(trust, dict):
@@ -530,10 +532,16 @@ def _dashboard_trust_summary(reports: list[dict[str, Any]]) -> dict[str, Any]:
cases += int(trust.get("cases") or 0)
_merge_counts(confidence, trust.get("confidence"))
_merge_counts(signal, trust.get("signal"))
+ method = report.get("methodology")
+ if isinstance(method, dict):
+ label = _methodology_label(method)
+ if label:
+ methodology[label] = methodology.get(label, 0) + 1
return {
"cases": cases,
"confidence": dict(sorted(confidence.items())),
"signal": dict(sorted(signal.items())),
+ "methodology": dict(sorted(methodology.items())),
}
@@ -747,10 +755,12 @@ def _trust_panel(trust: dict[str, Any]) -> str:
cases = int(trust.get("cases") or 0)
confidence = trust.get("confidence")
signal = trust.get("signal")
+ methodology = trust.get("methodology")
if cases <= 0 or not isinstance(confidence, dict) or not isinstance(signal, dict):
return ""
confidence_pills = _count_pills(confidence)
signal_pills = _count_pills(signal)
+ methodology_pills = _count_pills(methodology) if isinstance(methodology, dict) else ""
if not confidence_pills and not signal_pills:
return ""
return (
@@ -759,6 +769,7 @@ def _trust_panel(trust: dict[str, Any]) -> str:
'
'
f'
Confidence
{confidence_pills or "-"}
'
f'
Signal
{signal_pills or "-"}
'
+ f'
Methodology
{methodology_pills or "-"}
'
"
"
""
)
@@ -798,6 +809,14 @@ def _count_pills(counts: dict[Any, Any]) -> str:
return "".join(rows)
+def _methodology_label(value: dict[str, Any]) -> str:
+ name = str(value.get("name") or "").strip()
+ version = str(value.get("version") or "").strip()
+ if name and version:
+ return f"{name} ({version})"
+ return version or name
+
+
def _owners_panel(owners: list[Any]) -> str:
if not owners:
return ""
diff --git a/redline/diff.py b/redline/diff.py
index 2e994aa..dd0263b 100644
--- a/redline/diff.py
+++ b/redline/diff.py
@@ -265,6 +265,7 @@ def compare_suite_to_candidate(
"$schema": REPORT_SCHEMA_URL,
"version": "0.1",
"profile": profile,
+ "methodology": _report_methodology(suite),
"summary": summary,
"decision": decision,
"diffs": [diff.to_dict() for diff in diffs],
@@ -409,6 +410,17 @@ def _diff_profile(value: str) -> str:
return value
+def _report_methodology(suite: dict[str, Any]) -> dict[str, Any]:
+ methodology = suite.get("methodology")
+ if not isinstance(methodology, dict):
+ return {}
+ return {
+ "name": str(methodology.get("name") or ""),
+ "version": str(methodology.get("version") or ""),
+ "trust_scope": str(methodology.get("trust_scope") or ""),
+ }
+
+
def _confidence_drift_reason(
baseline_text: str | None,
candidate_text: str | None,
diff --git a/redline/reports.py b/redline/reports.py
index 27042b7..88b0f3a 100644
--- a/redline/reports.py
+++ b/redline/reports.py
@@ -41,6 +41,10 @@ def format_markdown_report(result: dict[str, Any], *, title: str = "redline diff
if diagnosis:
lines.append(f"**Diagnosis:** {diagnosis}")
lines.append("")
+ methodology = _methodology_label(result.get("methodology"))
+ if methodology:
+ lines.append(f"**Methodology:** {methodology}")
+ lines.append("")
warnings = _result_warnings(result)
if warnings:
@@ -356,6 +360,7 @@ def format_html_report(result: dict[str, Any], *, title: str = "redline diff") -
"",
_html_summary(summary),
_html_decision(decision),
+ _html_methodology(result.get("methodology")),
_html_warnings(result),
_html_artifacts(result),
_html_owner_review(diffs),
@@ -486,6 +491,16 @@ def _metadata_lines(item: dict[str, Any]) -> list[str]:
return lines
+def _methodology_label(value: object) -> str:
+ if not isinstance(value, dict):
+ return ""
+ name = str(value.get("name") or "").strip()
+ version = str(value.get("version") or "").strip()
+ if name and version:
+ return f"{name} ({version})"
+ return version or name
+
+
def _why_this_matters(item: dict[str, Any]) -> str:
status = str(item.get("status") or "").lower()
reasons = item.get("reasons")
@@ -1174,6 +1189,18 @@ def _html_decision(decision: dict[str, Any]) -> str:
return "".join(lines)
+def _html_methodology(value: object) -> str:
+ label = _methodology_label(value)
+ if not label:
+ return ""
+ return (
+ ''
+ "
You already have the eval data. redline turns it into a gate.
Deterministic by default
-
No cloud account is required for the core loop; CI-friendly checks run locally, cost nothing, and do not depend on hosted judges.
+
No cloud account is required. The default gate is fast, local, reproducible, and cheap. It catches production-breaking structural regressions without depending on hosted judges.
Reports people can review
@@ -213,6 +213,11 @@
What redline does not pretend
tells you when a run has no structural blockers; it does not
pretend that means the answer is perfect.
+
+ This is intentional: deterministic checks are the merge gate.
+ Judges are review evidence for ambiguous or domain-specific
+ behavior, not the only line of defense.
+
How to close the gap
From f014d08abdf96490c502fedf97d24eba3d2f8f26 Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 14 Jun 2026 11:58:46 -0600
Subject: [PATCH 25/55] Add import source guides
---
README.md | 1 +
docs/commands.md | 6 +++
docs/import-guides.md | 118 ++++++++++++++++++++++++++++++++++++++++++
3 files changed, 125 insertions(+)
create mode 100644 docs/import-guides.md
diff --git a/README.md b/README.md
index c6629cc..758ae63 100644
--- a/README.md
+++ b/README.md
@@ -507,6 +507,7 @@ bash scripts/release_check.sh
- [docs/release.md](docs/release.md): package, tag, PyPI, and MCP Registry release flow
- [docs/launch.md](docs/launch.md): public alpha launch plan
- [docs/troubleshooting.md](docs/troubleshooting.md): first-run and CI failure recovery
+- [docs/import-guides.md](docs/import-guides.md): Langfuse, Helicone, OpenAI chat, Datadog, and custom log import recipes
- [docs/methodology.md](docs/methodology.md): behavior grouping, case selection, scoring, and trust boundaries
- [docs/calibration.md](docs/calibration.md): tiny fixture showing regressions, changed cases, and neutral cases
- [docs/commands.md](docs/commands.md): compact CLI command reference
diff --git a/docs/commands.md b/docs/commands.md
index 9c6f1f8..e8ad649 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -53,6 +53,12 @@ Structural checks are deterministic and local. Green or neutral does not prove
semantic equivalence. Use `redline require`, `redline mark`, and optional judge
templates for factual, tone, hallucination, policy, or reasoning risks.
+## Import Help
+
+Use `redline import --detect` when you do not know an export's field names, then
+`--preview 3` before writing normalized logs. Source-specific recipes live in
+[docs/import-guides.md](import-guides.md).
+
## Diff profiles
- `--profile strict` is the default CI-oriented mode. Missing concrete details
diff --git a/docs/import-guides.md b/docs/import-guides.md
new file mode 100644
index 0000000..ad13280
--- /dev/null
+++ b/docs/import-guides.md
@@ -0,0 +1,118 @@
+# Import Guides
+
+redline only needs prompt-response JSONL. Your production system probably calls
+those fields something else. Start with detection, preview the mapping, then
+write the normalized baseline.
+
+```bash
+redline import raw-export.jsonl --detect
+redline import raw-export.jsonl --input-field request.prompt --output-field response.text --preview 3
+redline import raw-export.jsonl --input-field request.prompt --output-field response.text --out logs/baseline.jsonl
+```
+
+Redaction is enabled by default during import. It is best-effort pattern
+matching, not a privacy boundary; review normalized files before sharing or
+committing them.
+
+## Langfuse-Style Exports
+
+Use the built-in preset when your export has `input` and `output` fields.
+
+```bash
+redline import langfuse-export.jsonl --preset langfuse --preview 3
+redline import langfuse-export.jsonl --preset langfuse --out logs/baseline.jsonl
+```
+
+If your export nests generations under a trace object, detect first:
+
+```bash
+redline import langfuse-export.jsonl --detect
+```
+
+Then rerun import with the suggested field paths.
+
+## Helicone-Style Exports
+
+The preset expects `request.prompt` and `response.text`.
+
+```bash
+redline import helicone-export.jsonl --preset helicone --preview 3
+redline import helicone-export.jsonl --preset helicone --out logs/baseline.jsonl
+```
+
+For chat payloads, the prompt may be an array of messages. That is acceptable;
+redline stringifies structured prompts deterministically.
+
+## OpenAI Chat Traces
+
+The `openai-chat` preset maps chat messages to the prompt and the first choice
+message to the response.
+
+```bash
+redline import openai-chat.jsonl --preset openai-chat --preview 3
+redline import openai-chat.jsonl --preset openai-chat --out logs/baseline.jsonl
+```
+
+If your app stores `response.output_text`, override the response path:
+
+```bash
+redline import openai-chat.jsonl \
+ --input-field request.messages \
+ --output-field response.output_text \
+ --preview 3
+```
+
+## Datadog Or App Logs
+
+The `datadog` preset expects `attributes.input` and `attributes.output`.
+
+```bash
+redline import datadog.jsonl --preset datadog --preview 3
+redline import datadog.jsonl --preset datadog --out logs/baseline.jsonl
+```
+
+For custom app logs, common mappings look like:
+
+```bash
+redline import app.jsonl --input-field ticket.text --output-field assistant.reply --preview 3
+redline import app.jsonl --input-field prompt --output-field completion --preview 3
+redline import app.jsonl --input-field payload.user_question --output-field result.assistant_answer --preview 3
+```
+
+## Database Exports
+
+Export the rows you want as JSONL, one object per line:
+
+```json
+{"prompt":"Classify ticket INV-1042","response":"{\"owner\":\"billing\"}","created_at":"2026-06-14T10:00:00Z"}
+```
+
+Then import:
+
+```bash
+redline import db-export.jsonl \
+ --input-field prompt \
+ --output-field response \
+ --metadata-field created_at \
+ --preview 3
+
+redline import db-export.jsonl \
+ --input-field prompt \
+ --output-field response \
+ --metadata-field created_at \
+ --out logs/baseline.jsonl
+```
+
+## After Import
+
+```bash
+redline suite logs/baseline.jsonl --out redline-suite.json
+redline summary redline-suite.json
+redline cases redline-suite.json
+```
+
+If the suite looks thin, add pinned cases:
+
+```bash
+redline suite add redline-suite.json --prompt "..." --response "..."
+```
From 25295b7dd756fb2cc72a99dbd48dafd5cb608532 Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 14 Jun 2026 15:57:45 -0600
Subject: [PATCH 26/55] Expose quick-check through MCP
---
README.md | 2 +-
docs/mcp.md | 7 ++++---
redline/mcp.py | 39 ++++++++++++++++++++++++++++++++++++++-
tests/test_mcp.py | 39 +++++++++++++++++++++++++++++++++++++++
4 files changed, 82 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 758ae63..60e262b 100644
--- a/README.md
+++ b/README.md
@@ -341,7 +341,7 @@ redline-mcp
Use [docs/mcp.md](docs/mcp.md) to wire redline into an MCP client. The MCP
surface exposes safe capture-readiness, privacy, audit, scale, read,
-case-inspection, eval, and report tools plus workflow prompts like
+quick-check, case-inspection, eval, and report tools plus workflow prompts like
`setup_redline_project`, `check_prompt_change`, `build_suite_from_logs`, and
`review_candidate_outputs`.
It can also list or copy runner adapters and optional judge templates during setup.
diff --git a/docs/mcp.md b/docs/mcp.md
index 3dceceb..c720f2f 100644
--- a/docs/mcp.md
+++ b/docs/mcp.md
@@ -65,6 +65,7 @@ Available tools:
- `redline_doctor`
- `redline_suite`
+- `redline_quick_check`
- `redline_redact`
- `redline_import` (supports `detect` and `preview` before writing)
- `redline_import_presets`
@@ -87,9 +88,9 @@ Available tools:
- `redline_audit` (including `verify` and checkpoint output for the local audit hash chain)
- `redline_sbom`
-`redline_diff` and `redline_eval` return the underlying redline exit code as
-structured data. Exit code `1` means redline found blocking regressions or
-missing outputs; the MCP tool still returns successfully because that is a
+`redline_quick_check`, `redline_diff`, and `redline_eval` return the underlying
+redline exit code as structured data. Exit code `1` means redline found blocking regressions
+or missing outputs; the MCP tool still returns successfully because that is a
product finding, not a protocol failure. Exit code `2` and above indicates a
setup or command error.
diff --git a/redline/mcp.py b/redline/mcp.py
index c8bd506..e9b2e16 100644
--- a/redline/mcp.py
+++ b/redline/mcp.py
@@ -108,7 +108,7 @@ def main(argv: Sequence[str] | None = None) -> int:
"redline-mcp\n\n"
"Local MCP stdio server for redline.\n\n"
"Run this command from an MCP client. It exposes redline doctor, suite,\n"
- "watch stats, prompts, runners, judges, redact, audit, SBOM, benchmark, validate, summary, diff, eval, history, dashboard, cases, case, and guarded mark tools.\n",
+ "quick-check, watch stats, prompts, runners, judges, redact, audit, SBOM, benchmark, validate, summary, diff, eval, history, dashboard, cases, case, and guarded mark tools.\n",
end="",
)
return 0
@@ -508,6 +508,27 @@ def _tools() -> list[ToolSpec]:
),
_build_suite,
),
+ ToolSpec(
+ "redline_quick_check",
+ "Generate a temporary suite from baseline JSONL, diff candidate JSONL, and write local reports.",
+ _schema(
+ {
+ "baseline_path": _string("Baseline prompt-response JSONL path."),
+ "candidate_path": _string("Candidate prompt-response JSONL path."),
+ "input_field": _string("JSONL prompt field path."),
+ "output_field": _string("JSONL response field path."),
+ "out_dir": _string("Directory for quick-check suite and reports. Defaults to .redline/quick-check."),
+ "max_cases": _integer("Maximum representative cases."),
+ "all_cases": _boolean("Include every unique record instead of sampling representative cases."),
+ "profile": _string("Diff profile: strict or review."),
+ "compact": _boolean("Print compact one-line-per-case output."),
+ "json": _boolean("Print machine-readable JSON."),
+ "fail_on": _string("Comma-separated statuses that produce exit code 1; use none for report-only."),
+ },
+ required=("baseline_path", "candidate_path"),
+ ),
+ _build_quick_check,
+ ),
ToolSpec(
"redline_redact",
"Scan or redact common secrets and PII from JSONL prompt-response logs.",
@@ -880,6 +901,22 @@ def _build_suite(arguments: dict[str, Any]) -> list[str]:
return args
+def _build_quick_check(arguments: dict[str, Any]) -> list[str]:
+ args = ["quick-check"]
+ _add_positional(args, _required_string(arguments, "baseline_path"))
+ _add_positional(args, _required_string(arguments, "candidate_path"))
+ _add_option(args, "--input-field", arguments.get("input_field"))
+ _add_option(args, "--output-field", arguments.get("output_field"))
+ _add_option(args, "--out-dir", arguments.get("out_dir"))
+ _add_option(args, "--max-cases", arguments.get("max_cases"))
+ _add_flag(args, "--all-cases", arguments.get("all_cases"))
+ _add_option(args, "--profile", arguments.get("profile"))
+ _add_flag(args, "--compact", arguments.get("compact"))
+ _add_flag(args, "--json", arguments.get("json"))
+ _add_option(args, "--fail-on", arguments.get("fail_on"))
+ return args
+
+
def _build_redact(arguments: dict[str, Any]) -> list[str]:
args = ["redact"]
_add_positional(args, _required_string(arguments, "log_path"))
diff --git a/tests/test_mcp.py b/tests/test_mcp.py
index f92cdac..462afb9 100644
--- a/tests/test_mcp.py
+++ b/tests/test_mcp.py
@@ -30,6 +30,7 @@ def test_tools_list_exposes_redline_tools_with_guarded_writes(self) -> None:
names = {tool["name"] for tool in response["result"]["tools"]}
self.assertIn("redline_doctor", names)
self.assertIn("redline_suite", names)
+ self.assertIn("redline_quick_check", names)
self.assertIn("redline_redact", names)
self.assertIn("redline_import", names)
self.assertIn("redline_import_presets", names)
@@ -50,6 +51,44 @@ def test_tools_list_exposes_redline_tools_with_guarded_writes(self) -> None:
self.assertNotIn("redline_accept", names)
self.assertNotIn("redline_require", names)
+ def test_quick_check_tool_generates_suite_and_reports(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ baseline = root / "baseline.jsonl"
+ candidate = root / "candidate.jsonl"
+ baseline.write_text(
+ '{"prompt": "Return JSON with owner and priority.", "response": "{\\"owner\\": \\"Support\\", \\"priority\\": \\"high\\"}"}\n',
+ encoding="utf-8",
+ )
+ candidate.write_text(
+ '{"prompt": "Return JSON with owner and priority.", "response": "Support should handle this."}\n',
+ encoding="utf-8",
+ )
+
+ result = call_tool(
+ "redline_quick_check",
+ {
+ "cwd": directory,
+ "baseline_path": "baseline.jsonl",
+ "candidate_path": "candidate.jsonl",
+ "fail_on": "none",
+ "json": True,
+ },
+ )
+ report_dir = root / ".redline" / "quick-check"
+ suite_exists = (report_dir / "suite.json").exists()
+ json_exists = (report_dir / "diff.json").exists()
+ html_exists = (report_dir / "diff.html").exists()
+
+ self.assertFalse(result["isError"])
+ self.assertEqual(result["structuredContent"]["exit_code"], 0)
+ payload = result["structuredContent"]["json"]
+ self.assertEqual(payload["summary"]["regression"], 1)
+ self.assertEqual(payload["artifacts"]["html"], ".redline/quick-check/diff.html")
+ self.assertTrue(suite_exists)
+ self.assertTrue(json_exists)
+ self.assertTrue(html_exists)
+
def test_import_tool_normalizes_external_jsonl(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
From afe5d15ebfc499ef37244450207de0c1f4f8c34d Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 14 Jun 2026 15:59:53 -0600
Subject: [PATCH 27/55] Route MCP reviews through quick-check
---
docs/mcp.md | 7 ++++++-
redline/mcp.py | 16 ++++++++++------
tests/test_mcp.py | 26 ++++++++++++++++++++++++++
3 files changed, 42 insertions(+), 7 deletions(-)
diff --git a/docs/mcp.md b/docs/mcp.md
index c720f2f..d694b64 100644
--- a/docs/mcp.md
+++ b/docs/mcp.md
@@ -105,7 +105,7 @@ reach for most often:
- `check_prompt_change`: run doctor, then eval a changed prompt file.
- `build_suite_from_logs`: generate a suite, validate it, and summarize coverage.
-- `review_candidate_outputs`: diff candidate JSONL outputs and lead with blocking findings.
+- `review_candidate_outputs`: quick-check two JSONL logs or diff candidate outputs against a suite, then lead with blocking findings.
- `setup_redline_project`: guide first-time setup through runner selection,
prompt/log discovery, suite validation, CI scale checks, and optional judge setup.
@@ -132,6 +132,11 @@ Generate a redline suite from logs/baseline.jsonl, then diff it against
logs/candidate.jsonl. Summarize only regressions I should review.
```
+```text
+Quick-check logs/baseline.jsonl against logs/candidate.jsonl and tell me the
+blocking regressions before I wire a permanent suite.
+```
+
```text
Run redline eval with prompts/v2.txt and tell me whether this prompt change is
safe to ship. Do not accept or modify the baseline.
diff --git a/redline/mcp.py b/redline/mcp.py
index e9b2e16..ca46efa 100644
--- a/redline/mcp.py
+++ b/redline/mcp.py
@@ -361,9 +361,10 @@ def _prompts() -> list[PromptSpec]:
),
PromptSpec(
"review_candidate_outputs",
- "Compare candidate JSONL outputs against a suite and summarize risky behavior changes.",
+ "Quick-check or diff candidate JSONL outputs and summarize risky behavior changes.",
(
PromptArgument("cwd", "Project directory where redline should run."),
+ PromptArgument("baseline_path", "Baseline JSONL outputs to compare when no suite exists."),
PromptArgument("candidate_path", "Candidate JSONL outputs to compare.", required=True),
PromptArgument("suite_path", "Optional suite JSON path."),
),
@@ -428,18 +429,21 @@ def _build_suite_from_logs_prompt(arguments: dict[str, Any]) -> str:
def _build_review_candidate_outputs_prompt(arguments: dict[str, Any]) -> str:
candidate_path = _required_string(arguments, "candidate_path")
cwd = _optional_prompt_argument(arguments, "cwd", "the current project")
+ baseline_path = _optional_prompt_argument(arguments, "baseline_path", "not provided")
suite_path = _optional_prompt_argument(arguments, "suite_path", "the configured suite")
return (
"Review candidate prompt outputs with redline and tell me what changed.\n\n"
f"- Run in: {cwd}\n"
+ f"- Baseline outputs: {baseline_path}\n"
f"- Candidate outputs: {candidate_path}\n"
f"- Suite: {suite_path}\n\n"
"Use this workflow:\n"
- "1. Call `redline_diff` against the candidate outputs and suite when provided.\n"
- "2. Treat exit code 1 as a redline finding, not a tool failure.\n"
- "3. Lead with blocking regressions and missing outputs.\n"
- "4. Then summarize changed cases that need human review.\n"
- "5. Do not accept or modify the baseline.\n"
+ "1. If baseline outputs are provided and no suite exists yet, call `redline_quick_check` with the baseline and candidate logs.\n"
+ "2. Otherwise call `redline_diff` against the candidate outputs and suite when provided.\n"
+ "3. Treat exit code 1 as a redline finding, not a tool failure.\n"
+ "4. Lead with blocking regressions and missing outputs.\n"
+ "5. Then summarize changed cases that need human review.\n"
+ "6. Do not accept or modify the baseline.\n"
)
diff --git a/tests/test_mcp.py b/tests/test_mcp.py
index 462afb9..f94971f 100644
--- a/tests/test_mcp.py
+++ b/tests/test_mcp.py
@@ -256,6 +256,32 @@ def test_prompt_get_build_suite_workflow_includes_benchmark(self) -> None:
self.assertIn("redline suite add", text)
self.assertIn("redline_budget", text)
+ def test_prompt_get_review_candidate_outputs_uses_quick_check_when_baseline_is_available(self) -> None:
+ response = handle_jsonrpc_line(
+ json.dumps(
+ {
+ "jsonrpc": "2.0",
+ "id": 36,
+ "method": "prompts/get",
+ "params": {
+ "name": "review_candidate_outputs",
+ "arguments": {
+ "baseline_path": "logs/baseline.jsonl",
+ "candidate_path": "logs/candidate.jsonl",
+ },
+ },
+ }
+ )
+ )
+
+ assert response is not None
+ text = response["result"]["messages"][0]["content"]["text"]
+ self.assertIn("redline_quick_check", text)
+ self.assertIn("redline_diff", text)
+ self.assertIn("logs/baseline.jsonl", text)
+ self.assertIn("logs/candidate.jsonl", text)
+ self.assertIn("Do not accept or modify the baseline", text)
+
def test_prompt_get_builds_first_time_setup_workflow(self) -> None:
response = handle_jsonrpc_line(
json.dumps(
From 6557b6c9859ea4e6504ca72f1ff9019853d1188e Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 14 Jun 2026 16:05:34 -0600
Subject: [PATCH 28/55] Open quick-check reports from first run
---
README.md | 5 +++--
docs/commands.md | 2 +-
redline/cli.py | 7 ++++++-
tests/test_cli_config.py | 33 ++++++++++++++++++++-------------
4 files changed, 30 insertions(+), 17 deletions(-)
diff --git a/README.md b/README.md
index 60e262b..58330e7 100644
--- a/README.md
+++ b/README.md
@@ -114,11 +114,12 @@ redline gives you three primitives that cover the prompt-regression loop:
For a first pass on two local logs, use one command:
```bash
-redline quick-check logs/baseline.jsonl logs/candidate.jsonl
+redline quick-check logs/baseline.jsonl logs/candidate.jsonl --open
```
It generates a temporary suite, writes JSON/Markdown/HTML reports under
-`.redline/quick-check`, and prints the concrete behavioral diff.
+`.redline/quick-check`, opens the local HTML report, and prints the concrete
+behavioral diff.
### 1. Logs
diff --git a/docs/commands.md b/docs/commands.md
index e8ad649..1997d3e 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -21,7 +21,7 @@ output and examples after installing.
| `redline prompts` | Build or check a prompt-to-suite manifest. | `--suite-dir`, `--out`, `--check`, `--check-suites` |
| `redline summary` | Summarize suite or manifest readiness, including suite score and coverage gaps. | `--json` |
| `redline validate` | Validate suite or manifest structure and freshness. | `--strict`, `--json` |
-| `redline quick-check` | Generate a temporary suite from baseline JSONL, diff candidate JSONL, and write local reports in one first-run command. | `--input-field`, `--output-field`, `--out-dir`, `--max-cases`, `--all-cases`, `--profile`, `--fail-on` |
+| `redline quick-check` | Generate a temporary suite from baseline JSONL, diff candidate JSONL, and write local reports in one first-run command. | `--input-field`, `--output-field`, `--out-dir`, `--max-cases`, `--all-cases`, `--profile`, `--fail-on`, `--open` |
| `redline budget` | Estimate CI runtime without replaying prompts. | `--workers`, `--timeout`, `--max-seconds`, `--measure-local` |
| `redline benchmark` | Compatibility alias for `redline budget`. | Same as `budget` |
| `redline eval` | Replay suite cases through a configured runner. | `--prompt`, `--replay`, `--workers`, `--judge`, `--fail-on`, `--compact` |
diff --git a/redline/cli.py b/redline/cli.py
index f0a8150..70119f6 100644
--- a/redline/cli.py
+++ b/redline/cli.py
@@ -149,6 +149,7 @@ def _root_help() -> str:
Start here:
redline demo
+ redline quick-check path/to/baseline.jsonl path/to/candidate.jsonl --open
redline dashboard
redline init --runner stdio --copy-runner
redline runners
@@ -157,7 +158,6 @@ def _root_help() -> str:
redline sbom
Core loop:
- redline quick-check path/to/baseline.jsonl path/to/candidate.jsonl
redline suite path/to/baseline.jsonl --out redline-suite.json
redline import downloaded.jsonl --input-field instruction --output-field response --out baseline.jsonl
redline eval --prompt prompts/v2.txt
@@ -418,6 +418,7 @@ def build_parser() -> argparse.ArgumentParser:
quick_check_parser.add_argument("--profile", choices=DIFF_PROFILES, default="strict", help="diff signal profile")
quick_check_parser.add_argument("--compact", action="store_true", help="print compact one-line-per-case output")
quick_check_parser.add_argument("--json", action="store_true", help="print machine-readable JSON")
+ quick_check_parser.add_argument("--open", action="store_true", help="open the HTML report in the default browser")
quick_check_parser.add_argument(
"--fail-on",
default="regression,missing",
@@ -1386,6 +1387,8 @@ def cmd_quick_check(args: argparse.Namespace) -> int:
write_json(report_json, result)
write_text(report_md, format_markdown_report(result, title="redline quick-check"))
write_text(report_html, format_html_report(result, title="redline quick-check"))
+ if args.open:
+ webbrowser.open(report_html.resolve().as_uri())
fail_on = parse_fail_on(args.fail_on)
if args.json:
@@ -1412,6 +1415,8 @@ def cmd_quick_check(args: argparse.Namespace) -> int:
print(f"- Open HTML report: {report_html}")
print(f"- Inspect cases: redline cases {suite_path}")
print(f"- Make this persistent: redline suite {args.baseline} --out redline-suite.json")
+ if args.open:
+ print("- Opened HTML report in the default browser.")
return 1 if should_fail(result, fail_on) else 0
diff --git a/tests/test_cli_config.py b/tests/test_cli_config.py
index 6abb706..58adb00 100644
--- a/tests/test_cli_config.py
+++ b/tests/test_cli_config.py
@@ -6,6 +6,7 @@
import tempfile
import unittest
from pathlib import Path
+from unittest.mock import patch
from redline.cli import main
@@ -20,6 +21,7 @@ def test_bare_cli_prints_first_run_help(self) -> None:
text = output.getvalue()
self.assertIn("Start here:", text)
self.assertIn("redline demo", text)
+ self.assertIn("redline quick-check path/to/baseline.jsonl path/to/candidate.jsonl --open", text)
self.assertIn("redline init --runner stdio --copy-runner", text)
self.assertIn("Review loop:", text)
self.assertIn("redline suite add redline-suite.json", text)
@@ -35,6 +37,7 @@ def test_root_help_prints_first_run_help(self) -> None:
text = output.getvalue()
self.assertIn("Start here:", text)
self.assertIn("redline demo", text)
+ self.assertIn("redline quick-check path/to/baseline.jsonl path/to/candidate.jsonl --open", text)
self.assertNotIn("suite-add", text)
self.assertNotIn("==SUPPRESS==", text)
@@ -675,24 +678,27 @@ def test_quick_check_generates_suite_and_reports(self) -> None:
)
output = io.StringIO()
- with contextlib.redirect_stdout(output):
- self.assertEqual(
- main(
- [
- "quick-check",
- "baseline.jsonl",
- "candidate.jsonl",
- "--fail-on",
- "none",
- ]
- ),
- 0,
- )
+ with patch("redline.cli.webbrowser.open") as open_browser:
+ with contextlib.redirect_stdout(output):
+ self.assertEqual(
+ main(
+ [
+ "quick-check",
+ "baseline.jsonl",
+ "candidate.jsonl",
+ "--fail-on",
+ "none",
+ "--open",
+ ]
+ ),
+ 0,
+ )
text = output.getvalue()
self.assertIn("redline quick-check", text)
self.assertIn("candidate lost valid JSON format", text)
self.assertIn("Open HTML report", text)
+ self.assertIn("Opened HTML report in the default browser.", text)
self.assertTrue((root / ".redline" / "quick-check" / "suite.json").exists())
self.assertTrue((root / ".redline" / "quick-check" / "diff.json").exists())
self.assertTrue((root / ".redline" / "quick-check" / "diff.md").exists())
@@ -701,6 +707,7 @@ def test_quick_check_generates_suite_and_reports(self) -> None:
self.assertEqual(report["summary"]["regression"], 1)
self.assertEqual(report["artifacts"]["html"], ".redline/quick-check/diff.html")
self.assertEqual(report["suite"], ".redline/quick-check/suite.json")
+ open_browser.assert_called_once()
finally:
os.chdir(previous)
From e76989ccf0955a3c921774ce027c33da7ee4a4ee Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 14 Jun 2026 18:25:45 -0600
Subject: [PATCH 29/55] Validate suite schema compatibility
---
redline/validate.py | 35 ++++++++++++++++++++++++++++++++-
tests/test_validate.py | 44 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 78 insertions(+), 1 deletion(-)
diff --git a/redline/validate.py b/redline/validate.py
index 92afea5..582f261 100644
--- a/redline/validate.py
+++ b/redline/validate.py
@@ -26,10 +26,13 @@
"shape",
"length_bucket",
)
+_CURRENT_SUITE_VERSION = "0.1"
+_SUPPORTED_SUITE_VERSIONS = {_CURRENT_SUITE_VERSION}
def validate_suite(suite: dict[str, Any], *, suite_path: str = "") -> dict[str, Any]:
items: list[dict[str, str]] = []
+ _validate_suite_metadata(items, suite)
cases = suite.get("cases")
if not isinstance(cases, list):
_add(items, "error", "cases", "expected a list of suite cases")
@@ -93,7 +96,7 @@ def validate_suite(suite: dict[str, Any], *, suite_path: str = "") -> dict[str,
error_count = _count(items, "error")
warning_count = _count(items, "warning")
return {
- "version": "0.1",
+ "version": _CURRENT_SUITE_VERSION,
"suite": suite_path,
"valid": error_count == 0,
"errors": error_count,
@@ -107,6 +110,33 @@ def validate_suite(suite: dict[str, Any], *, suite_path: str = "") -> dict[str,
}
+def _validate_suite_metadata(items: list[dict[str, str]], suite: dict[str, Any]) -> None:
+ version = suite.get("version")
+ if not isinstance(version, str) or not version.strip():
+ _add(
+ items,
+ "warning",
+ "version",
+ "missing suite version; treating as legacy and recommending regeneration with current redline",
+ )
+ elif version.strip() not in _SUPPORTED_SUITE_VERSIONS:
+ supported = ", ".join(sorted(_SUPPORTED_SUITE_VERSIONS))
+ _add(
+ items,
+ "error",
+ "version",
+ f"unsupported suite version {version.strip()}; supported versions: {supported}",
+ )
+
+ schema = suite.get("$schema")
+ if not isinstance(schema, str) or not schema.strip():
+ _add(items, "warning", "$schema", "missing suite JSON schema reference")
+
+ methodology = suite.get("methodology")
+ if not isinstance(methodology, dict):
+ _add(items, "warning", "methodology", "missing suite selection methodology metadata")
+
+
def validate_prompt_manifest(
manifest: dict[str, Any],
*,
@@ -281,6 +311,8 @@ def _next_steps(items: list[dict[str, str]], *, suite_path: str, source: object)
steps.append(f"Use a supported judgment status, then rerun: redline validate {suite_arg}")
if any(item["level"] == "error" and item["path"] == "cases" for item in items):
steps.append(f"Fix suite JSON shape, then rerun: redline validate {suite_arg}")
+ if any(item["level"] == "error" and item["path"] == "version" for item in items):
+ steps.append(f"Regenerate or migrate unsupported suite schema: redline suite {source_arg} --out {suite_arg}")
if any(
item["level"] == "warning" and "duplicate prompt-response pair" in item["message"]
@@ -295,6 +327,7 @@ def _next_steps(items: list[dict[str, str]], *, suite_path: str, source: object)
item["path"].endswith(".content_hash")
or ".features." in item["path"]
or item["path"] == "summary"
+ or item["path"] in {"version", "$schema", "methodology"}
)
for item in items
):
diff --git a/tests/test_validate.py b/tests/test_validate.py
index 67a1ac2..2fbb318 100644
--- a/tests/test_validate.py
+++ b/tests/test_validate.py
@@ -24,6 +24,50 @@ def test_validate_suite_accepts_generated_suite(self) -> None:
self.assertEqual(report["warnings"], 0)
self.assertEqual(report["next_steps"], [])
+ def test_validate_suite_warns_for_legacy_missing_version_metadata(self) -> None:
+ suite = build_suite(
+ [LogRecord(1, "Return JSON", '{"ok": true}', {})],
+ source="logs/baseline.jsonl",
+ input_field="prompt",
+ output_field="response",
+ max_cases=10,
+ )
+ del suite["version"]
+
+ report = validate_suite(suite, suite_path="redline-suite.json")
+
+ self.assertTrue(report["valid"])
+ self.assertEqual(report["warnings"], 1)
+ self.assertTrue(
+ any(item["path"] == "version" and "missing suite version" in item["message"] for item in report["items"])
+ )
+ self.assertIn(
+ "Regenerate suite metadata from trusted logs: redline suite logs/baseline.jsonl --out redline-suite.json",
+ report["next_steps"],
+ )
+
+ def test_validate_suite_rejects_unsupported_future_version(self) -> None:
+ suite = build_suite(
+ [LogRecord(1, "Return JSON", '{"ok": true}', {})],
+ source="logs/baseline.jsonl",
+ input_field="prompt",
+ output_field="response",
+ max_cases=10,
+ )
+ suite["version"] = "99.0"
+
+ report = validate_suite(suite, suite_path="redline-suite.json")
+
+ self.assertFalse(report["valid"])
+ self.assertEqual(report["errors"], 1)
+ self.assertTrue(
+ any(item["path"] == "version" and "unsupported suite version 99.0" in item["message"] for item in report["items"])
+ )
+ self.assertIn(
+ "Regenerate or migrate unsupported suite schema: redline suite logs/baseline.jsonl --out redline-suite.json",
+ report["next_steps"],
+ )
+
def test_validate_suite_rejects_duplicate_case_ids(self) -> None:
suite = build_suite(
[
From fc8a349b39be1e9cd0269a886659a1077ca2876a Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 14 Jun 2026 18:37:27 -0600
Subject: [PATCH 30/55] Warn on stochastic baseline suites
---
docs/methodology.md | 8 ++++++++
redline-report.schema.json | 1 +
redline-suite.schema.json | 3 ++-
redline/diff.py | 7 +++++++
redline/suite.py | 9 +++++++++
redline/summary.py | 11 +++++++++++
tests/test_diff.py | 19 +++++++++++++++++++
tests/test_packaging.py | 1 +
tests/test_suite.py | 20 ++++++++++++++++++++
tests/test_summary.py | 20 ++++++++++++++++++++
10 files changed, 98 insertions(+), 1 deletion(-)
diff --git a/docs/methodology.md b/docs/methodology.md
index 78796aa..d02a7c3 100644
--- a/docs/methodology.md
+++ b/docs/methodology.md
@@ -61,6 +61,13 @@ The generated suite summary also carries `case_coverage` and
`cluster_coverage` so downstream tools can display basic suite-health context
without recomputing it.
+Suite summaries also count `stochastic_prompt_groups`: prompts that appear with
+more than one distinct baseline response. Those prompts may be genuinely
+multi-answer, or they may come from high-temperature sampling, model drift, or
+mixed production contexts. redline warns on them because natural baseline
+variance can look like a regression unless the suite is stabilized, switched to
+review mode, or guarded with explicit requirements.
+
## Regression Signals
The deterministic diff engine checks for high-signal changes:
@@ -88,6 +95,7 @@ suite-health signal, not a model-quality score. It combines:
- owner coverage
- high-risk group calibration
- non-English calibration warnings
+- stochastic baseline warnings
Use the score to decide what to improve before CI gating. A high score means the
suite is better prepared for review and automation. It does not mean the
diff --git a/redline-report.schema.json b/redline-report.schema.json
index c50877a..52c2e6f 100644
--- a/redline-report.schema.json
+++ b/redline-report.schema.json
@@ -39,6 +39,7 @@
"cases": {"type": "integer", "minimum": 0},
"case_coverage": {"type": ["number", "null"], "minimum": 0, "maximum": 1},
"cluster_coverage": {"type": ["number", "null"], "minimum": 0, "maximum": 1},
+ "stochastic_prompt_groups": {"type": "integer", "minimum": 0},
"selection": {"type": "string"}
},
"additionalProperties": true
diff --git a/redline-suite.schema.json b/redline-suite.schema.json
index f29cdd8..c815c79 100644
--- a/redline-suite.schema.json
+++ b/redline-suite.schema.json
@@ -62,7 +62,8 @@
"high_variance_clusters": {"type": "integer", "minimum": 0},
"failure_pattern_clusters": {"type": "integer", "minimum": 0},
"prompt_diversity_cases": {"type": "integer", "minimum": 0},
- "non_ascii_records": {"type": "integer", "minimum": 0}
+ "non_ascii_records": {"type": "integer", "minimum": 0},
+ "stochastic_prompt_groups": {"type": "integer", "minimum": 0}
},
"additionalProperties": true
},
diff --git a/redline/diff.py b/redline/diff.py
index cf7f143..b0005c8 100644
--- a/redline/diff.py
+++ b/redline/diff.py
@@ -439,6 +439,7 @@ def _report_suite_summary(suite: dict[str, Any]) -> dict[str, Any]:
"high_risk_clusters",
"medium_risk_clusters",
"non_ascii_records",
+ "stochastic_prompt_groups",
)
return {key: summary[key] for key in keys if key in summary}
@@ -454,6 +455,12 @@ def _report_warnings(suite: dict[str, Any]) -> list[str]:
f"suite contains {non_ascii_records} non-ASCII record(s); "
"English-centric entity, tone, refusal, and policy-polarity heuristics need manual or judge review"
)
+ stochastic_prompt_groups = int(summary.get("stochastic_prompt_groups") or 0)
+ if stochastic_prompt_groups:
+ warnings.append(
+ f"suite contains {stochastic_prompt_groups} prompt(s) with multiple distinct baseline responses; "
+ "separate natural sampling variance from prompt regressions before CI gating"
+ )
return warnings
diff --git a/redline/suite.py b/redline/suite.py
index 9286d5b..c5ec2fa 100644
--- a/redline/suite.py
+++ b/redline/suite.py
@@ -68,6 +68,7 @@ def build_suite(
)
selected_clusters = {signatures[id(record)] for record, _ in selected}
non_ascii_records = sum(1 for record in unique_records if _has_non_ascii(record.prompt) or _has_non_ascii(record.response))
+ stochastic_prompt_groups = _stochastic_prompt_groups(unique_records)
cases = []
for index, (record, selection_reason) in enumerate(selected, 1):
signature = signatures[id(record)]
@@ -135,6 +136,7 @@ def build_suite(
"failure_pattern_clusters": sum(1 for info in cluster_infos.values() if info["failure_patterns"]),
"prompt_diversity_cases": sum(1 for _, reason in selected if reason == "prompt_diversity_edge"),
"non_ascii_records": non_ascii_records,
+ "stochastic_prompt_groups": stochastic_prompt_groups,
"owned_cases": _owned_case_count(cases),
},
"clusters": clusters,
@@ -273,6 +275,13 @@ def _unique_prompt_response_records(records: list[LogRecord]) -> list[LogRecord]
return unique
+def _stochastic_prompt_groups(records: list[LogRecord]) -> int:
+ responses_by_prompt: dict[str, set[str]] = defaultdict(set)
+ for record in records:
+ responses_by_prompt[record.prompt].add(record.response)
+ return sum(1 for responses in responses_by_prompt.values() if len(responses) > 1)
+
+
def _has_non_ascii(value: str) -> bool:
return any(ord(char) > 127 for char in value)
diff --git a/redline/summary.py b/redline/summary.py
index b284a29..3f0e99e 100644
--- a/redline/summary.py
+++ b/redline/summary.py
@@ -154,6 +154,7 @@ def suite_summary(suite: dict[str, Any]) -> dict[str, Any]:
"high_variance_clusters": len(high_variance),
"failure_pattern_clusters": len(failure_patterns),
"non_ascii_records": int(summary.get("non_ascii_records", 0)),
+ "stochastic_prompt_groups": int(summary.get("stochastic_prompt_groups", 0)),
"judgments": dict(sorted(judgment_counts.items())),
"requirements": requirements_count,
"top_clusters": top_clusters,
@@ -245,6 +246,7 @@ def prompt_manifest_summary(
"high_variance_clusters",
"failure_pattern_clusters",
"non_ascii_records",
+ "stochastic_prompt_groups",
"requirements",
):
totals[key] += int(child_summary.get(key) or 0)
@@ -307,6 +309,7 @@ def prompt_manifest_summary(
"high_variance_clusters": totals["high_variance_clusters"],
"failure_pattern_clusters": totals["failure_pattern_clusters"],
"non_ascii_records": totals["non_ascii_records"],
+ "stochastic_prompt_groups": totals["stochastic_prompt_groups"],
"requirements": totals["requirements"],
"prompts": prompt_rows,
"missing_suites": missing_suites,
@@ -353,6 +356,7 @@ def format_suite_summary(suite: dict[str, Any], *, suite_path: str | None = None
f"High-variance groups: {summary['high_variance_clusters']}",
f"Failure-pattern groups: {summary['failure_pattern_clusters']:>2}",
f"Non-ASCII records: {summary['non_ascii_records']}",
+ f"Stochastic prompts: {summary['stochastic_prompt_groups']}",
f"Cases with requirements: {summary['requirements']:>2}",
"",
]
@@ -428,6 +432,7 @@ def format_prompt_manifest_summary(report: dict[str, Any]) -> str:
f"High-risk groups: {report['high_risk_clusters']}",
f"Medium-risk groups: {report['medium_risk_clusters']}",
f"Non-ASCII records: {report['non_ascii_records']}",
+ f"Stochastic prompts: {report['stochastic_prompt_groups']}",
f"Cases with requirements: {report['requirements']:>2}",
"",
]
@@ -494,6 +499,10 @@ def _summary_next_steps(summary: dict[str, Any], *, suite_path: str | None = Non
"Review non-English cases manually or with a domain judge; structural checks still work, "
"but entity/refusal heuristics are English-oriented."
)
+ if int(summary.get("stochastic_prompt_groups") or 0):
+ steps.append(
+ "Stabilize repeated-prompt baselines or use review mode; otherwise natural sampling variance can look like regressions."
+ )
if int(summary["cases"]) and not summary["judgments"]:
steps.append("After the first eval, mark expected or ignored changes to train the suite.")
if int(summary["cases"]) == 0:
@@ -570,6 +579,8 @@ def _readiness_reasons(summary: dict[str, Any], *, owner_coverage: float) -> lis
reasons.append("high-risk groups need explicit requirements or a judge before CI gating")
if int(summary.get("non_ascii_records") or 0):
reasons.append("non-ASCII records need extra review because entity/refusal heuristics are English-oriented")
+ if int(summary.get("stochastic_prompt_groups") or 0):
+ reasons.append("repeated prompts with multiple baseline responses need stochastic baseline review")
return reasons
diff --git a/tests/test_diff.py b/tests/test_diff.py
index cc3ae03..42b4834 100644
--- a/tests/test_diff.py
+++ b/tests/test_diff.py
@@ -26,6 +26,7 @@ def test_report_schema_documents_generated_report_fields(self) -> None:
self.assertIn("diagnosis", schema["properties"]["decision"]["properties"])
self.assertIn("methodology", schema["properties"])
self.assertIn("suite_summary", schema["properties"])
+ self.assertIn("stochastic_prompt_groups", schema["properties"]["suite_summary"]["properties"])
self.assertIn("suite", schema["properties"])
self.assertIn("candidate", schema["properties"])
self.assertIn("diffs", schema["properties"])
@@ -64,6 +65,24 @@ def test_report_warns_when_suite_contains_non_ascii_records(self) -> None:
self.assertTrue(any("English-centric" in warning for warning in result["warnings"]))
+ def test_report_warns_when_suite_contains_stochastic_baselines(self) -> None:
+ suite = build_suite(
+ [
+ LogRecord(1, "Classify ticket", "billing", {}),
+ LogRecord(2, "Classify ticket", "support", {}),
+ ],
+ source="memory",
+ input_field="prompt",
+ output_field="response",
+ max_cases=10,
+ )
+ candidate = [LogRecord(1, "Classify ticket", "billing", {})]
+
+ result = compare_suite_to_candidate(suite, candidate)
+
+ self.assertEqual(result["suite_summary"]["stochastic_prompt_groups"], 1)
+ self.assertTrue(any("multiple distinct baseline responses" in warning for warning in result["warnings"]))
+
def test_classify_json_regression(self) -> None:
baseline = extract_features('{"name":"Ada","status":"active"}').to_dict()
candidate = extract_features('{"name":"Ada"').to_dict()
diff --git a/tests/test_packaging.py b/tests/test_packaging.py
index 1d7fb02..c6f1565 100644
--- a/tests/test_packaging.py
+++ b/tests/test_packaging.py
@@ -111,6 +111,7 @@ def test_methodology_doc_calibrates_behavior_grouping_and_score(self) -> None:
self.assertIn("behavior-signature grouping", text)
self.assertIn("suite readiness score", text)
self.assertIn("not a model-quality score", text)
+ self.assertIn("stochastic_prompt_groups", text)
self.assertIn("docs/methodology.md", readme)
def test_calibration_doc_links_runnable_fixture(self) -> None:
diff --git a/tests/test_suite.py b/tests/test_suite.py
index cdb2a51..bc5827a 100644
--- a/tests/test_suite.py
+++ b/tests/test_suite.py
@@ -73,6 +73,7 @@ def test_suite_schema_documents_generated_suite_fields(self) -> None:
self.assertIn("cluster_coverage", schema["properties"]["summary"]["properties"])
self.assertIn("prompt_diversity_cases", schema["properties"]["summary"]["properties"])
self.assertIn("non_ascii_records", schema["properties"]["summary"]["properties"])
+ self.assertIn("stochastic_prompt_groups", schema["properties"]["summary"]["properties"])
def test_build_suite_assigns_case_owners_from_rules(self) -> None:
suite = build_suite(
@@ -303,6 +304,25 @@ def test_build_suite_counts_non_ascii_records(self) -> None:
self.assertEqual(suite["summary"]["non_ascii_records"], 1)
+ def test_build_suite_counts_repeated_prompts_with_distinct_responses(self) -> None:
+ suite = build_suite(
+ [
+ LogRecord(1, "Classify ticket", "billing", {}),
+ LogRecord(2, "Classify ticket", "support", {}),
+ LogRecord(3, "Classify ticket", "billing", {}),
+ LogRecord(4, "Summarize ticket", "Customer needs a refund.", {}),
+ ],
+ source="logs/baseline.jsonl",
+ input_field="prompt",
+ output_field="response",
+ max_cases=10,
+ )
+
+ self.assertEqual(suite["summary"]["records_seen"], 4)
+ self.assertEqual(suite["summary"]["unique_prompt_response_pairs"], 3)
+ self.assertEqual(suite["summary"]["duplicate_prompt_response_pairs"], 1)
+ self.assertEqual(suite["summary"]["stochastic_prompt_groups"], 1)
+
def test_add_suite_case_pins_manual_case(self) -> None:
suite = build_suite(
[LogRecord(1, "Return JSON", '{"ok": true}', {})],
diff --git a/tests/test_summary.py b/tests/test_summary.py
index 1dbe426..f1d3b19 100644
--- a/tests/test_summary.py
+++ b/tests/test_summary.py
@@ -134,6 +134,26 @@ def test_suite_summary_surfaces_non_ascii_calibration(self) -> None:
self.assertIn("Non-ASCII records: 1", output)
self.assertIn("entity/refusal heuristics are English-oriented", "\n".join(summary["next_steps"]))
+ def test_suite_summary_surfaces_stochastic_baseline_calibration(self) -> None:
+ suite = build_suite(
+ [
+ LogRecord(1, "Classify ticket", "billing", {}),
+ LogRecord(2, "Classify ticket", "support", {}),
+ ],
+ source="memory",
+ input_field="prompt",
+ output_field="response",
+ max_cases=10,
+ )
+
+ summary = suite_summary(suite)
+ output = format_suite_summary(suite)
+
+ self.assertEqual(summary["stochastic_prompt_groups"], 1)
+ self.assertIn("Stochastic prompts: 1", output)
+ self.assertIn("natural sampling variance can look like regressions", "\n".join(summary["next_steps"]))
+ self.assertTrue(any("stochastic baseline review" in reason for reason in summary["suite_readiness"]["reasons"]))
+
def test_suite_summary_counts_owners(self) -> None:
suite = build_suite(
[
From 6967374834c54d5cc0fdca8acdf335d129a6c30e Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 14 Jun 2026 21:30:20 -0600
Subject: [PATCH 31/55] Add app-style dashboard renderer
---
README.md | 4 +-
docs/commands.md | 2 +-
redline/cli.py | 3 +-
redline/dashboard.py | 667 +++++++++++++++++++++++++++++++++++++++
tests/test_dashboard.py | 90 ++++++
tests/test_quickstart.py | 2 +-
6 files changed, 763 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 58330e7..5ce387c 100644
--- a/README.md
+++ b/README.md
@@ -57,13 +57,13 @@ HTML reports under `.redline/demo`.
Open the local demo report index:
```bash
-redline dashboard --reports-dir .redline/demo/reports --open
+redline dashboard --reports-dir .redline/demo/reports --style app --open
```
On headless CI or remote shells, skip `--open` and use the printed HTML path:
```bash
-redline dashboard --reports-dir .redline/demo/reports --out .redline/dashboard.html
+redline dashboard --reports-dir .redline/demo/reports --style app --out .redline/dashboard.html
```
diff --git a/docs/commands.md b/docs/commands.md
index 1997d3e..ee5c797 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -30,7 +30,7 @@ output and examples after installing.
| `redline accept` | Promote reviewed candidate outputs into the suite baseline. | `--candidate`, `--all-expected`, `--approver`, `--note` |
| `redline history` | Append and summarize trend history. | `--label`, `--out`, `--out-md`, `--fail-on` |
| `redline compare` | Compare two redline reports. | `--fail-on`, `--out-json`, `--out-html` |
-| `redline dashboard` | Render a self-contained local HTML dashboard. | `--reports-dir`, `--history`, `--checkpoint`, `--out`, `--open` |
+| `redline dashboard` | Render a self-contained local HTML dashboard. | `--reports-dir`, `--history`, `--checkpoint`, `--out`, `--style classic\|app`, `--open` |
| `redline runners` | List or copy runner and log adapter templates. | `--copy`, `--out`, `--force` |
| `redline judges` | List or copy judge templates. | `--copy`, `--out`, `--force` |
| `redline audit` | Show and verify local audit events. | `--verify`, `--checkpoint`, `--out-checkpoint`, `--expect-last-hash` |
diff --git a/redline/cli.py b/redline/cli.py
index 70119f6..1213fc0 100644
--- a/redline/cli.py
+++ b/redline/cli.py
@@ -491,6 +491,7 @@ def build_parser() -> argparse.ArgumentParser:
dashboard_parser.add_argument("--checkpoint", default=".redline/audit-checkpoint.json", help="audit checkpoint JSON path")
dashboard_parser.add_argument("--out", default=".redline/dashboard.html", help="dashboard HTML output path")
dashboard_parser.add_argument("--limit", type=int, default=20, help="recent reports/history entries to include; use 0 for all")
+ dashboard_parser.add_argument("--style", choices=("classic", "app"), default="classic", help="dashboard visual style")
dashboard_parser.add_argument("--open", action="store_true", help="open the dashboard in the default browser")
dashboard_parser.add_argument("--json", action="store_true", help="print machine-readable dashboard metadata")
dashboard_parser.set_defaults(func=cmd_dashboard)
@@ -1514,7 +1515,7 @@ def cmd_dashboard(args: argparse.Namespace) -> int:
checkpoint_path=args.checkpoint,
limit=args.limit,
)
- write_text(args.out, format_dashboard_html(dashboard, output_path=args.out))
+ write_text(args.out, format_dashboard_html(dashboard, output_path=args.out, style=args.style))
if args.open:
webbrowser.open(Path(args.out).resolve().as_uri())
if args.json:
diff --git a/redline/dashboard.py b/redline/dashboard.py
index af0f8fa..2a15e7b 100644
--- a/redline/dashboard.py
+++ b/redline/dashboard.py
@@ -50,6 +50,7 @@ def format_dashboard_html(
*,
title: str = "redline dashboard",
output_path: str | Path | None = None,
+ style: str = "classic",
) -> str:
reports = dashboard.get("reports")
if not isinstance(reports, list):
@@ -78,6 +79,24 @@ def format_dashboard_html(
raw_trend = dashboard.get("trend")
trend = raw_trend if isinstance(raw_trend, dict) else history_trend(list(reversed(history)))
latest = reports[0] if reports and isinstance(reports[0], dict) else {}
+ if style == "app":
+ return _format_dashboard_app_html(
+ dashboard,
+ title=title,
+ output_path=output_path,
+ reports=reports,
+ history=history,
+ benchmarks=benchmarks,
+ errors=errors,
+ notices=notices,
+ owners=owners,
+ trust=trust,
+ checkpoint=checkpoint,
+ trend=trend,
+ latest=latest,
+ )
+ if style != "classic":
+ raise ValueError(f"unknown dashboard style: {style}")
return "\n".join(
[
"",
@@ -120,6 +139,373 @@ def format_dashboard_html(
)
+def _format_dashboard_app_html(
+ dashboard: dict[str, Any],
+ *,
+ title: str,
+ output_path: str | Path | None,
+ reports: list[Any],
+ history: list[Any],
+ benchmarks: list[Any],
+ errors: list[Any],
+ notices: list[Any],
+ owners: list[Any],
+ trust: dict[str, Any],
+ checkpoint: dict[str, Any],
+ trend: dict[str, Any],
+ latest: dict[str, Any],
+) -> str:
+ summary = _app_dict(latest.get("summary"))
+ suite_summary = _app_dict(latest.get("suite_summary"))
+ decision = _app_dict(latest.get("decision"))
+ active = _blocking_count(summary)
+ changed = _changed_count(summary)
+ status_class = "red" if active else "amber" if changed else "green"
+ status_text = "Regressions" if active else "Review" if changed else "Clear"
+ review_cases = _app_list(latest.get("review_cases"))
+ warnings = _app_list(latest.get("warnings"))
+ if notices:
+ warnings = [*(str(item.get("message") or "") for item in notices if isinstance(item, dict)), *warnings]
+ return "\n".join(
+ [
+ "",
+ '',
+ "",
+ '',
+ '',
+ f"{_h(title)}",
+ "",
+ "",
+ "",
+ '
"
+ )
+
+
+def _app_warning_banner(warnings: list[Any], *, status_class: str) -> str:
+ if warnings:
+ text = " ".join(_preview(str(warning), 160) for warning in warnings[:2])
+ return f'
Calibration warning. {_h(text)}
'
+ if status_class == "red":
+ return '
Blocking regressions detected. Fix or mark expected changes before shipping.
'
+ return '
No blocking structural regressions in latest report. Review semantic risks separately.
'
+
+
+def _app_review_rows(items: list[Any], *, empty: str = "No review cases found.") -> str:
+ rows = []
+ for item in items[:8]:
+ if not isinstance(item, dict):
+ continue
+ status = str(item.get("status") or item.get("kind") or "review")
+ case_id = str(item.get("case_id") or item.get("suite_case_id") or "-")
+ prompt = _preview(str(item.get("prompt") or item.get("reason") or "Review case"), 96)
+ reason = _preview(str(item.get("reason") or "; ".join(str(value) for value in item.get("reasons", []) if value) or ""), 120)
+ tone = "red" if status in {"regression", "missing"} else "amber" if status == "changed" else "green"
+ rows.append(
+ '
' for label, value in rows)
+
+
+def _app_owner_rows(owners: list[Any]) -> str:
+ rows = []
+ for owner in owners[:8]:
+ if not isinstance(owner, dict):
+ continue
+ rows.append(
+ '
'
+ f'{_h(str(owner.get("owner") or "unowned"))}'
+ f'{_safe_int(owner.get("reviewable"))}'
+ "
"
+ f'{_app_alert("blue", f"Trend: {direction}", str(trend.get("summary") or "Record history entries to see whether prompt quality is improving or regressing."))}'
+ f'
Blocking regressions detected. Fix or mark expected changes before shipping.
'
- return '
No blocking structural regressions in latest report. Review semantic risks separately.
'
+ return _app_alert("red", "Blocking regressions detected.", "Fix or mark expected changes before shipping.")
+ return _app_alert("green", "No blocking structural regressions in latest report.", "Review semantic risks separately.")
def _app_review_rows(items: list[Any], *, empty: str = "No review cases found.") -> str:
@@ -435,10 +518,10 @@ def _app_review_rows(items: list[Any], *, empty: str = "No review cases found.")
reason = _preview(str(item.get("reason") or "; ".join(str(value) for value in item.get("reasons", []) if value) or ""), 120)
tone = "red" if status in {"regression", "missing"} else "amber" if status == "changed" else "green"
rows.append(
- '
'
+ for label, value, tone in rows
+ )
+ return f'
B {_h(title)}
{body}
'
+
+
+def _app_fix_card(review_cases: list[Any]) -> str:
+ first = next((item for item in review_cases if isinstance(item, dict)), {})
+ case_id = str(first.get("case_id") or first.get("suite_case_id") or "case id")
+ command = f'redline case redline-suite.json {case_id}' if case_id != "case id" else "redline cases redline-suite.json"
+ body = (
+ '
'
+ 'Recommended local loop'
+ f'1. Inspect: {_h(command)}'
+ '2. Fix prompt or runner output'
+ '3. Mark expected only when the change is intentional'
+ '
'
+ )
+ return f'
F Suggested fix
{body}
'
+
+
+def _app_report_chart(reports: list[Any]) -> str:
+ points = []
+ for index, report in enumerate(reversed([item for item in reports[:12] if isinstance(item, dict)])):
+ summary = _app_dict(report.get("summary"))
+ cases = max(_safe_int(summary.get("cases")), 1)
+ blocking = _blocking_count(summary)
+ pass_rate = max(0.0, min(1.0, (cases - blocking) / cases))
+ x = 24 + index * 74
+ y = 132 - int(pass_rate * 96)
+ points.append((x, y))
+ if not points:
+ return '
No report chart yet. Run redline diff or eval to populate trend evidence.
'
+ path = " ".join(f"{x},{y}" for x, y in points)
+ dots = "".join(f'' for x, y in points)
+ return (
+ '"
+ '
"
f'{_app_alert("blue", f"Trend: {direction}", str(trend.get("summary") or "Record history entries to see whether prompt quality is improving or regressing."))}'
- f'
'
def _app_fix_card(review_cases: list[Any]) -> str:
first = next((item for item in review_cases if isinstance(item, dict)), {})
case_id = str(first.get("case_id") or first.get("suite_case_id") or "case id")
- command = f'redline case redline-suite.json {case_id}' if case_id != "case id" else "redline cases redline-suite.json"
+ suite = str(first.get("suite") or "redline-suite.json")
+ command = f"redline case {shell_quote(suite)} {shell_quote(case_id)}" if case_id != "case id" else "redline cases redline-suite.json"
body = (
'
'
'Recommended local loop'
@@ -612,7 +621,7 @@ def _app_fix_card(review_cases: list[Any]) -> str:
'3. Mark expected only when the change is intentional'
'
- Local-first prompt regression diffs for AI engineers. Point redline
- at real prompt-response logs, change your prompt, then catch broken
- JSON, missing URLs, lost tables, refusals, and other structural
- regressions before they ship.
+ Point redline at the prompt-response logs you already have, change
+ your prompt, then catch broken JSON, missing URLs, lost tables, and
+ silent refusals before your users do.
- The public demo is synthetic and safe to share, but it behaves like
- a real prompt change: the candidate gets shorter and silently drops
- required structure, dates, owners, URLs, and refusal behavior.
-
REGRESSION case_006: candidate newly refuses
REGRESSION case_010: candidate lost bullet list structure
-Next: inspect the HTML report, mark intentional changes, accept reviewed outputs.
+10 blocking regressions - fix before you ship.
+
+
+
+
+
First-run proof
+
One command, ten regressions, no account setup.
+
+ The public demo is synthetic and safe to share, but it behaves like
+ a real prompt change: the candidate gets shorter and silently drops
+ required structure, dates, owners, URLs, and refusal behavior.
+
reason: candidate lost valid JSON format; required keys are no longer parseable.
+
+
+
+
+
+
One command, ten regressions, no account setup.
width="1440"
height="1050"
loading="lazy"
+ decoding="async"
>
Local dashboard with ship readiness, history, benchmark evidence, and report links.
@@ -122,6 +168,7 @@
One command, ten regressions, no account setup.
width="1440"
height="1050"
loading="lazy"
+ decoding="async"
>
HTML report with concrete reasons and side-by-side baseline versus candidate output.
@@ -130,25 +177,25 @@
One command, ten regressions, no account setup.
-
Why it exists
+
Product promise
You already have the eval data. redline turns it into a gate.
- Zero manual test writing
-
Build suites from JSONL prompt-response logs instead of inventing cases by hand.
+ Zero manual cases to start
+
Build suites from JSONL prompt-response logs instead of inventing eval cases by hand.
- Deterministic by default
-
No cloud account is required. The default gate is fast, local, reproducible, and cheap. It catches production-breaking structural regressions without depending on hosted judges.
+ No cloud calls by default
+
The core checks run locally with zero runtime dependencies, no API keys, and reproducible CI output.
- Reports people can review
-
Write JSON, Markdown, JUnit, and self-contained HTML reports for local review, CI artifacts, and GitHub summaries.
+ Clear failure reasons
+
Reports name the broken behavior: lost JSON, missing keys, dropped URLs, new refusals, empty output, or structure drift.
Review loop included
-
Mark intentional changes, accept new baselines, and keep the suite moving with your prompt.
+
Mark intentional changes, accept reviewed baselines, and keep the suite evolving with your prompt.
@@ -163,6 +210,7 @@
From logs to a shipping gate in four commands.
01
Collect behavior
+
Watch an app, import JSONL, or adapt exports from your logging stack.
redline watch --log logs/prompts.jsonl
@@ -170,13 +218,15 @@
Collect behavior
02
Generate the suite
- redline suite .redline/logs/prompts.jsonl --out redline-suite.json
+
Select representative behavior from real prompt-output data.
+ redline suite logs/prompts.jsonl --out redline-suite.json
03
-
Evaluate a prompt change
+
Evaluate a change
+
Replay the suite against a new prompt file or candidate log.
redline eval --prompt prompts/v2.txt
@@ -184,6 +234,7 @@
Evaluate a prompt change
04
Review and accept
+
Pin requirements, mark expected changes, and promote reviewed outputs.
redline mark ...redline accept ...
@@ -213,11 +264,6 @@
What redline does not pretend
tells you when a run has no structural blockers; it does not
pretend that means the answer is perfect.
-
- This is intentional: deterministic checks are the merge gate.
- Judges are review evidence for ambiguous or domain-specific
- behavior, not the only line of defense.
-
How to close the gap
@@ -255,7 +301,7 @@
Replay any app
Local dashboard
-
Publish self-contained HTML reports as CI artifacts or browse them locally.
+
Publish self-contained HTML reports as CI artifacts or browse the ship-readiness dashboard locally.
diff --git a/tests/test_cli_config.py b/tests/test_cli_config.py
index 58adb00..f06cc76 100644
--- a/tests/test_cli_config.py
+++ b/tests/test_cli_config.py
@@ -642,7 +642,7 @@ def test_suite_and_diff_use_config_defaults(self) -> None:
text = output.getvalue()
self.assertIn("Open HTML report: .redline/reports/diff.html", text)
- self.assertIn("Open dashboard: redline dashboard --reports-dir .redline/reports --open", text)
+ self.assertIn("Open app: redline app --reports-dir .redline/reports", text)
self.assertTrue((root / ".redline" / "suite.json").exists())
self.assertTrue((root / ".redline" / "reports" / "diff.json").exists())
self.assertTrue((root / ".redline" / "reports" / "diff.md").exists())
diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py
index 32a4d60..21e24d8 100644
--- a/tests/test_dashboard.py
+++ b/tests/test_dashboard.py
@@ -4,6 +4,7 @@
import tempfile
import unittest
from pathlib import Path
+from unittest.mock import patch
from redline.cli import main
from redline.dashboard import build_dashboard, format_dashboard_html
@@ -321,6 +322,16 @@ def test_app_dashboard_renders_real_report_data(self) -> None:
self.assertIn("local dashboard", html)
self.assertIn('class="svg-ico"', html)
self.assertIn('button type="button" class="sb-item', html)
+ self.assertIn("Workflow", html)
+ self.assertIn('id="s-workflow"', html)
+ self.assertIn("Command center.", html)
+ self.assertIn("dashboard never runs shell commands", html)
+ self.assertIn('data-copy="redline demo --public --compact"', html)
+ self.assertIn("redline import path/to/export.jsonl --detect", html)
+ self.assertIn("redline suite .redline/logs/baseline.jsonl", html)
+ self.assertIn("redline eval --compact", html)
+ self.assertIn("redline history .redline/reports/eval.json", html)
+ self.assertIn("redline app --reports-dir .redline/reports", html)
self.assertIn("Active regressions", html)
self.assertIn("Regression trend", html)
self.assertIn("Alerts", html)
@@ -533,6 +544,73 @@ def test_cli_writes_app_dashboard_html(self) -> None:
self.assertIn("candidate lost valid JSON format", text)
self.assertIn("Reports: 1", stdout.getvalue())
+ def test_cli_app_opens_guided_local_app_by_default(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ reports = root / ".redline" / "reports"
+ write_json(
+ reports / "eval.json",
+ {
+ "summary": {"cases": 1, "regression": 1},
+ "decision": {"recommended_action": "fix blocking cases before shipping"},
+ "suite": "redline-suite.json",
+ "diffs": [
+ {
+ "case_id": "case_001",
+ "status": "regression",
+ "prompt": "Return JSON.",
+ "reasons": ["candidate lost valid JSON format"],
+ }
+ ],
+ },
+ )
+ output = root / ".redline" / "app.html"
+
+ current = Path.cwd()
+ stdout = io.StringIO()
+ try:
+ import os
+
+ os.chdir(root)
+ with patch("redline.cli.webbrowser.open") as open_browser:
+ with contextlib.redirect_stdout(stdout):
+ code = main(["app", "--out", str(output)])
+ finally:
+ os.chdir(current)
+
+ self.assertEqual(code, 0)
+ text = output.read_text(encoding="utf-8")
+ self.assertIn("redline app", text)
+ self.assertIn('data-redline-dashboard="app"', text)
+ self.assertIn('id="s-workflow"', text)
+ self.assertIn("redline import path/to/export.jsonl --detect", text)
+ self.assertIn("candidate lost valid JSON format", text)
+ self.assertIn("Opened redline app in the default browser.", stdout.getvalue())
+ self.assertIn("Next:", stdout.getvalue())
+ open_browser.assert_called_once()
+
+ def test_cli_app_can_write_without_opening_browser(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ output = root / ".redline" / "app.html"
+
+ current = Path.cwd()
+ stdout = io.StringIO()
+ try:
+ import os
+
+ os.chdir(root)
+ with patch("redline.cli.webbrowser.open") as open_browser:
+ with contextlib.redirect_stdout(stdout):
+ code = main(["app", "--no-open", "--out", str(output)])
+ finally:
+ os.chdir(current)
+
+ self.assertEqual(code, 0)
+ self.assertTrue(output.exists())
+ self.assertIn("Open:", stdout.getvalue())
+ open_browser.assert_not_called()
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_demo.py b/tests/test_demo.py
index 1b19ca1..15ba47f 100644
--- a/tests/test_demo.py
+++ b/tests/test_demo.py
@@ -46,7 +46,7 @@ def test_format_demo_includes_regression_report(self) -> None:
self.assertIn("--status expected", output)
self.assertIn("redline accept", output)
self.assertIn("--all-expected", output)
- self.assertIn("redline dashboard --reports-dir", output)
+ self.assertIn("redline app --reports-dir", output)
self.assertIn("redline init --runner stdio --copy-runner --github-action", output)
self.assertIn("redline runners --copy all", output)
self.assertIn("redline suite path/to/baseline.jsonl --out redline-suite.json", output)
diff --git a/tests/test_packaging.py b/tests/test_packaging.py
index c6f1565..79362ef 100644
--- a/tests/test_packaging.py
+++ b/tests/test_packaging.py
@@ -91,7 +91,7 @@ def test_demo_recording_script_runs_compact_demo(self) -> None:
self.assertIn("redline demo --public --compact", text)
self.assertIn("redline history", text)
self.assertIn("--out-md", text)
- self.assertIn("redline dashboard", text)
+ self.assertIn("redline app", text)
def test_demo_gif_script_records_or_writes_transcript(self) -> None:
script = Path("scripts/demo_gif.sh")
@@ -160,7 +160,7 @@ def test_release_check_builds_and_smokes_installed_wheel(self) -> None:
self.assertIn("--out-md history.md", script)
self.assertIn("redline compare .redline/demo/reports/diff.json", script)
self.assertIn("--out-html compare.html", script)
- self.assertIn("redline dashboard --reports-dir .redline/demo/reports", script)
+ self.assertIn("redline app --reports-dir .redline/demo/reports", script)
self.assertIn("--github-summary", script)
self.assertIn("--all-cases", script)
self.assertIn("redline suite add all-suite.json", script)
diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py
index 6e80ba0..e167ef9 100644
--- a/tests/test_quickstart.py
+++ b/tests/test_quickstart.py
@@ -14,7 +14,7 @@ def test_readme_demo_dashboard_command_points_at_demo_reports(self) -> None:
readme = Path("README.md").read_text(encoding="utf-8")
self.assertIn("redline demo --public --compact", readme)
- self.assertIn("redline dashboard --reports-dir .redline/demo/reports --style app --open", readme)
+ self.assertIn("redline app --reports-dir .redline/demo/reports", readme)
def test_readme_quickstart_path_catches_realistic_regressions(self) -> None:
repo = Path(__file__).resolve().parents[1]
diff --git a/tests/test_site.py b/tests/test_site.py
index 21c8449..b8c8436 100644
--- a/tests/test_site.py
+++ b/tests/test_site.py
@@ -31,7 +31,7 @@ def test_site_homepage_has_launch_copy_and_product_paths(self) -> None:
self.assertIn("Local-first prompt regression diffs", html)
self.assertIn("Star on GitHub", html)
self.assertIn("redline demo --public --compact", html)
- self.assertIn("redline dashboard --reports-dir .redline/demo/reports --open", html)
+ self.assertIn("redline app --reports-dir .redline/demo/reports", html)
self.assertIn("redline eval --prompt prompts/v2.txt", html)
self.assertIn("candidate lost valid JSON format", html)
self.assertIn("Ship readiness", html)
From 57371ce9fc186b08c5c6e8284e0f497d0fff4a1a Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 28 Jun 2026 20:31:25 -0600
Subject: [PATCH 36/55] Add project readiness status command
---
README.md | 11 ++
docs/commands.md | 1 +
redline/cli.py | 41 ++++++++
redline/status.py | 217 +++++++++++++++++++++++++++++++++++++++
tests/test_cli_config.py | 2 +
tests/test_status.py | 92 +++++++++++++++++
6 files changed, 364 insertions(+)
create mode 100644 redline/status.py
create mode 100644 tests/test_status.py
diff --git a/README.md b/README.md
index ffd9b57..61490d4 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,15 @@ The demo catches ten synthetic regressions without API keys, private logs, a
cloud account, or an LLM judge. It writes JSON, Markdown, and self-contained
HTML reports under `.redline/demo`.
+Ask redline what to do next:
+
+```bash
+redline status --reports-dir .redline/demo/reports
+```
+
+`status` reads local config, suites, reports, history, and audit evidence, then
+prints the next command instead of leaving you to infer the workflow.
+
Open the guided local product app:
```bash
@@ -285,6 +294,8 @@ redline is built around the full prompt-regression loop:
- `redline sbom`: write CycloneDX SBOM release evidence for security review.
- `redline app`: open the guided local product surface for importing logs,
generating suites, reviewing regressions, recording history, and wiring CI/MCP.
+- `redline status`: show project readiness and the next command from local
+ evidence.
- `redline history`, `redline compare`, and `redline dashboard`: track quality
over time and inspect report artifacts locally. The dashboard surfaces
feature-level rollups, prompt-level eval rows, benchmark evidence, and a
diff --git a/docs/commands.md b/docs/commands.md
index 138535f..ec498b2 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -7,6 +7,7 @@ output and examples after installing.
| --- | --- | --- |
| `redline` | Show first-run help. | `--version` |
| `redline demo` | Generate and diff the bundled demo logs. | `--public`, `--compact` |
+| `redline status` | Show project readiness, latest local evidence, and the next command to run. | `--reports-dir`, `--history`, `--checkpoint`, `--limit`, `--json` |
| `redline app` | Open the guided local product app for import, suite, eval, review, history, and integration commands. | `--reports-dir`, `--history`, `--checkpoint`, `--out`, `--no-open`, `--json` |
| `redline init` | Write `redline.json`, runner files, and optional CI workflow. | `--runner`, `--copy-runner`, `--github-action`, `--force` |
| `redline doctor` | Check config, suite, replay, reports, audit, and team workflow. | `--strict`, `--json` |
diff --git a/redline/cli.py b/redline/cli.py
index 52e699d..2a825b2 100644
--- a/redline/cli.py
+++ b/redline/cli.py
@@ -109,6 +109,7 @@
runner_adapters,
)
from .sbom import build_sbom, format_sbom_report
+from .status import build_project_status, format_project_status
from .summary import (
format_prompt_manifest_summary,
format_suite_summary,
@@ -149,6 +150,7 @@ def _root_help() -> str:
Start here:
redline demo
+ redline status
redline app
redline quick-check path/to/baseline.jsonl path/to/candidate.jsonl --open
redline dashboard
@@ -221,6 +223,15 @@ def build_parser() -> argparse.ArgumentParser:
doctor_parser.add_argument("--strict", action="store_true", help="exit non-zero when warnings are present")
doctor_parser.set_defaults(func=cmd_doctor)
+ status_parser = subparsers.add_parser("status", help="show project readiness and the next command to run")
+ status_parser.add_argument("--config", default=DEFAULT_CONFIG_PATH, help="config path to read")
+ status_parser.add_argument("--reports-dir", default=".redline/reports", help="directory containing redline JSON reports")
+ status_parser.add_argument("--history", default=".redline/history.jsonl", help="history JSONL path")
+ status_parser.add_argument("--checkpoint", default=".redline/audit-checkpoint.json", help="audit checkpoint JSON path")
+ status_parser.add_argument("--limit", type=int, default=20, help="recent reports/history entries to inspect; use 0 for all")
+ status_parser.add_argument("--json", action="store_true", help="print machine-readable status metadata")
+ status_parser.set_defaults(func=cmd_status)
+
sbom_parser = subparsers.add_parser("sbom", help="write CycloneDX SBOM release evidence")
sbom_parser.add_argument("--out", help="write SBOM JSON to this path")
sbom_parser.add_argument("--json", action="store_true", help="print machine-readable JSON")
@@ -678,6 +689,36 @@ def cmd_doctor(args: argparse.Namespace) -> int:
return 0
+def cmd_status(args: argparse.Namespace) -> int:
+ if args.limit < 0:
+ raise ValueError("status --limit must be 0 or greater")
+ config = load_config(args.config)
+ suite_path = str(config.get("suite") or "redline-suite.json")
+ suite = None
+ suite_error = None
+ if Path(suite_path).exists():
+ try:
+ suite = _read_suite_or_manifest(suite_path)
+ except ValueError as exc:
+ suite_error = str(exc)
+ status = build_project_status(
+ config_path=args.config,
+ config=config,
+ suite=suite,
+ suite_error=suite_error,
+ suite_git_ignored=_is_git_ignored(suite_path),
+ reports_dir=args.reports_dir,
+ history_path=args.history,
+ checkpoint_path=args.checkpoint,
+ limit=args.limit,
+ )
+ if args.json:
+ print(json.dumps(status, indent=2, sort_keys=True))
+ else:
+ print(format_project_status(status), end="")
+ return 0
+
+
def cmd_sbom(args: argparse.Namespace) -> int:
sbom = build_sbom()
if args.out:
diff --git a/redline/status.py b/redline/status.py
new file mode 100644
index 0000000..9682169
--- /dev/null
+++ b/redline/status.py
@@ -0,0 +1,217 @@
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Any
+
+from .dashboard import build_dashboard
+from .diff import TRUST_SCOPE
+from .doctor import doctor_report
+
+
+def build_project_status(
+ *,
+ config_path: str,
+ config: dict[str, Any],
+ suite: dict[str, Any] | None,
+ suite_error: str | None = None,
+ suite_git_ignored: bool = False,
+ reports_dir: str | Path = ".redline/reports",
+ history_path: str | Path = ".redline/history.jsonl",
+ checkpoint_path: str | Path = ".redline/audit-checkpoint.json",
+ limit: int = 20,
+) -> dict[str, Any]:
+ doctor = doctor_report(
+ config_path=config_path,
+ config=config,
+ suite=suite,
+ suite_error=suite_error,
+ suite_git_ignored=suite_git_ignored,
+ )
+ dashboard = build_dashboard(
+ reports_dir=reports_dir,
+ history_path=history_path,
+ checkpoint_path=checkpoint_path,
+ limit=limit,
+ )
+ latest = _first_dict(dashboard.get("reports"))
+ summary = _dict(latest.get("summary"))
+ review_cases = _list(latest.get("review_cases"))
+ blocking = _count(summary, "regression") + _count(summary, "missing")
+ changed = _count(summary, "changed")
+ suite_path = str(config.get("suite") or "redline-suite.json")
+ state, message, next_command = _state_and_next(
+ doctor=doctor,
+ latest=latest,
+ summary=summary,
+ review_cases=review_cases,
+ blocking=blocking,
+ changed=changed,
+ suite_path=suite_path,
+ reports_dir=str(reports_dir),
+ history_path=str(history_path),
+ )
+ return {
+ "version": "0.1",
+ "state": state,
+ "message": message,
+ "next_command": next_command,
+ "config_path": config_path,
+ "suite_path": suite_path,
+ "reports_dir": str(reports_dir),
+ "history_path": str(history_path),
+ "checkpoint_path": str(checkpoint_path),
+ "doctor": {
+ "errors": int(doctor.get("errors") or 0),
+ "warnings": int(doctor.get("warnings") or 0),
+ "checks": doctor.get("checks") if isinstance(doctor.get("checks"), list) else [],
+ "next_steps": doctor.get("next_steps") if isinstance(doctor.get("next_steps"), list) else [],
+ },
+ "reports": len(_list(dashboard.get("reports"))),
+ "history": len(_list(dashboard.get("history"))),
+ "benchmarks": len(_list(dashboard.get("benchmarks"))),
+ "checkpoint": bool(dashboard.get("checkpoint")),
+ "latest_report": latest,
+ "summary": summary,
+ "blocking": blocking,
+ "changed": changed,
+ "scope": TRUST_SCOPE,
+ }
+
+
+def format_project_status(status: dict[str, Any]) -> str:
+ latest = _dict(status.get("latest_report"))
+ summary = _dict(status.get("summary"))
+ doctor = _dict(status.get("doctor"))
+ checks = _list(doctor.get("checks"))
+ lines = [
+ "redline status",
+ "",
+ f"State: {str(status.get('state') or '').upper()} - {status.get('message')}",
+ f"Next: {status.get('next_command')}",
+ "",
+ "Evidence",
+ f"- Config: {_check_line(checks, 'config')}",
+ f"- Suite: {_check_line(checks, 'suite')}",
+ f"- Replay: {_check_line(checks, 'replay')}",
+ f"- Reports: {status.get('reports')} in {status.get('reports_dir')}",
+ f"- History: {status.get('history')} in {status.get('history_path')}",
+ f"- Benchmarks: {status.get('benchmarks')}",
+ f"- Audit checkpoint: {'yes' if status.get('checkpoint') else 'no'}",
+ f"- Doctor: errors={doctor.get('errors', 0)} warnings={doctor.get('warnings', 0)}",
+ ]
+ if latest:
+ lines.extend(
+ [
+ "",
+ "Latest report",
+ f"- File: {latest.get('path')}",
+ f"- Summary: {_summary_line(summary)}",
+ f"- Decision: {_decision_line(latest)}",
+ ]
+ )
+ else:
+ lines.extend(["", "Latest report", "- No redline report found yet."])
+ lines.extend(["", "Trust boundary", f"- {status.get('scope') or TRUST_SCOPE}"])
+ next_steps = _list(doctor.get("next_steps"))
+ if next_steps:
+ lines.extend(["", "Doctor next steps"])
+ lines.extend(f"- {step}" for step in next_steps)
+ return "\n".join(str(line) for line in lines) + "\n"
+
+
+def _state_and_next(
+ *,
+ doctor: dict[str, Any],
+ latest: dict[str, Any],
+ summary: dict[str, Any],
+ review_cases: list[Any],
+ blocking: int,
+ changed: int,
+ suite_path: str,
+ reports_dir: str,
+ history_path: str,
+) -> tuple[str, str, str]:
+ checks = {str(check.get("name") or ""): check for check in _list(doctor.get("checks")) if isinstance(check, dict)}
+ doctor_steps = [str(step) for step in _list(doctor.get("next_steps")) if str(step).strip()]
+ config = _dict(checks.get("config"))
+ suite = _dict(checks.get("suite"))
+ if config.get("status") == "warn":
+ return "setup", "project is not initialized", _first_step(doctor_steps, "redline init --runner stdio --copy-runner")
+ if suite.get("status") in {"warn", "error"}:
+ return "setup", "no usable suite baseline yet", _first_step(
+ doctor_steps,
+ f"redline suite path/to/log.jsonl --out {suite_path}",
+ )
+ if not latest:
+ return "ready", "suite exists; run the first eval or diff", "redline eval --compact"
+ first_case = _first_dict(review_cases)
+ case_command = _case_command(first_case, fallback_suite=suite_path)
+ if blocking:
+ return "blocked", f"latest report has {blocking} blocking case(s)", case_command
+ if changed:
+ return "review", f"latest report has {changed} changed case(s)", case_command
+ if int(doctor.get("errors") or 0):
+ return "setup", "doctor found setup errors", _first_step(doctor_steps, "redline doctor")
+ if not Path(history_path).exists():
+ report_path = str(latest.get("path") or f"{reports_dir}/eval.json")
+ return "record", "latest report is clear; record it in history", (
+ f"redline history {report_path} --label prompt-v2 --out {history_path}"
+ )
+ return "ready", "latest report has no blocking structural regressions", (
+ f"redline app --reports-dir {reports_dir} --history {history_path}"
+ )
+
+
+def _case_command(case: dict[str, Any], *, fallback_suite: str) -> str:
+ case_id = str(case.get("suite_case_id") or case.get("case_id") or "")
+ suite = str(case.get("suite") or fallback_suite)
+ return f"redline case {suite} {case_id}"
+
+
+def _summary_line(summary: dict[str, Any]) -> str:
+ if not summary:
+ return "none"
+ keys = ("cases", "regression", "missing", "changed", "neutral", "improved", "accepted", "ignored")
+ return " ".join(f"{key}={_count(summary, key)}" for key in keys if key in summary)
+
+
+def _decision_line(report: dict[str, Any]) -> str:
+ decision = _dict(report.get("decision"))
+ return str(decision.get("recommended_action") or "review report details")
+
+
+def _check_line(checks: list[Any], name: str) -> str:
+ for item in checks:
+ if isinstance(item, dict) and item.get("name") == name:
+ return f"{item.get('status')} - {item.get('message')}"
+ return "not checked"
+
+
+def _first_step(steps: list[str], fallback: str) -> str:
+ if not steps:
+ return fallback
+ step = steps[0]
+ if ": redline " in step:
+ return "redline " + step.split(": redline ", 1)[1]
+ return step
+
+
+def _first_dict(value: Any) -> dict[str, Any]:
+ items = _list(value)
+ first = items[0] if items else {}
+ return first if isinstance(first, dict) else {}
+
+
+def _dict(value: Any) -> dict[str, Any]:
+ return value if isinstance(value, dict) else {}
+
+
+def _list(value: Any) -> list[Any]:
+ return value if isinstance(value, list) else []
+
+
+def _count(summary: dict[str, Any], key: str) -> int:
+ try:
+ return int(summary.get(key) or 0)
+ except (TypeError, ValueError):
+ return 0
diff --git a/tests/test_cli_config.py b/tests/test_cli_config.py
index f06cc76..cbbcdd8 100644
--- a/tests/test_cli_config.py
+++ b/tests/test_cli_config.py
@@ -21,6 +21,7 @@ def test_bare_cli_prints_first_run_help(self) -> None:
text = output.getvalue()
self.assertIn("Start here:", text)
self.assertIn("redline demo", text)
+ self.assertIn("redline status", text)
self.assertIn("redline quick-check path/to/baseline.jsonl path/to/candidate.jsonl --open", text)
self.assertIn("redline init --runner stdio --copy-runner", text)
self.assertIn("Review loop:", text)
@@ -37,6 +38,7 @@ def test_root_help_prints_first_run_help(self) -> None:
text = output.getvalue()
self.assertIn("Start here:", text)
self.assertIn("redline demo", text)
+ self.assertIn("redline status", text)
self.assertIn("redline quick-check path/to/baseline.jsonl path/to/candidate.jsonl --open", text)
self.assertNotIn("suite-add", text)
self.assertNotIn("==SUPPRESS==", text)
diff --git a/tests/test_status.py b/tests/test_status.py
new file mode 100644
index 0000000..e42d9db
--- /dev/null
+++ b/tests/test_status.py
@@ -0,0 +1,92 @@
+import contextlib
+import io
+import json
+import os
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+from redline.cli import main
+from redline.io import LogRecord, write_json
+from redline.suite import build_suite
+
+
+class StatusTests(unittest.TestCase):
+ def test_status_guides_uninitialized_project(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ previous = Path.cwd()
+ os.chdir(directory)
+ try:
+ output = io.StringIO()
+ with contextlib.redirect_stdout(output):
+ self.assertEqual(main(["status"]), 0)
+ finally:
+ os.chdir(previous)
+
+ text = output.getvalue()
+ self.assertIn("redline status", text)
+ self.assertIn("State: SETUP - project is not initialized", text)
+ self.assertIn("Next: redline init --runner stdio --copy-runner", text)
+ self.assertIn("- Config: warn - redline.json not found", text)
+ self.assertIn("- No redline report found yet.", text)
+
+ def test_status_surfaces_blocking_latest_report(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ write_json(
+ root / "redline.json",
+ {
+ "suite": "redline-suite.json",
+ "replay": f"{sys.executable} -c 'import sys; print(sys.stdin.read())'",
+ "reports": {"json": ".redline/reports/eval.json"},
+ },
+ )
+ suite = build_suite(
+ [LogRecord(1, "Return JSON with owner.", '{"owner":"support"}', {})],
+ source="baseline.jsonl",
+ input_field="prompt",
+ output_field="response",
+ all_cases=True,
+ )
+ write_json(root / "redline-suite.json", suite)
+ write_json(
+ root / ".redline" / "reports" / "eval.json",
+ {
+ "suite": "redline-suite.json",
+ "summary": {"cases": 1, "regression": 1, "missing": 0, "changed": 0, "neutral": 0},
+ "decision": {"recommended_action": "fix blocking cases before shipping"},
+ "diffs": [
+ {
+ "case_id": "case_001",
+ "suite_case_id": "case_001",
+ "status": "regression",
+ "prompt": "Return JSON with owner.",
+ "reasons": ["candidate lost valid JSON format"],
+ }
+ ],
+ },
+ )
+ previous = Path.cwd()
+ os.chdir(root)
+ try:
+ output = io.StringIO()
+ with contextlib.redirect_stdout(output):
+ self.assertEqual(main(["status"]), 0)
+ json_output = io.StringIO()
+ with contextlib.redirect_stdout(json_output):
+ self.assertEqual(main(["status", "--json"]), 0)
+ finally:
+ os.chdir(previous)
+
+ text = output.getvalue()
+ self.assertIn("State: BLOCKED - latest report has 1 blocking case(s)", text)
+ self.assertIn("Next: redline case redline-suite.json case_001", text)
+ self.assertIn("- Reports: 1 in .redline/reports", text)
+ self.assertIn("- Summary: cases=1 regression=1 missing=0 changed=0 neutral=0", text)
+ self.assertIn("- Decision: fix blocking cases before shipping", text)
+
+ payload = json.loads(json_output.getvalue())
+ self.assertEqual(payload["state"], "blocked")
+ self.assertEqual(payload["blocking"], 1)
+ self.assertEqual(payload["next_command"], "redline case redline-suite.json case_001")
From de70ea875651ed8b1be5ba2c41a339317ad92342 Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 28 Jun 2026 20:34:21 -0600
Subject: [PATCH 37/55] Add one-command app demo launch
---
README.md | 20 +++++++++++++++-----
docs/commands.md | 2 +-
redline/cli.py | 10 +++++++++-
tests/test_dashboard.py | 26 ++++++++++++++++++++++++++
4 files changed, 51 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index 61490d4..4375e67 100644
--- a/README.md
+++ b/README.md
@@ -44,15 +44,25 @@ Install from PyPI:
python -m pip install redline-ai
```
-Run the public proof:
+Run the guided local app with the public proof loaded:
+
+```bash
+redline app --demo
+```
+
+This generates the public demo reports, opens the local product app, and shows
+the full import -> suite -> eval -> review workflow. The demo catches ten
+synthetic regressions without API keys, private logs, a cloud account, or an LLM
+judge.
+
+Prefer terminal output first:
```bash
redline demo --public --compact
```
-The demo catches ten synthetic regressions without API keys, private logs, a
-cloud account, or an LLM judge. It writes JSON, Markdown, and self-contained
-HTML reports under `.redline/demo`.
+The demo writes JSON, Markdown, and self-contained HTML reports under
+`.redline/demo`.
Ask redline what to do next:
@@ -63,7 +73,7 @@ redline status --reports-dir .redline/demo/reports
`status` reads local config, suites, reports, history, and audit evidence, then
prints the next command instead of leaving you to infer the workflow.
-Open the guided local product app:
+Open the guided local product app on existing reports:
```bash
redline app --reports-dir .redline/demo/reports
diff --git a/docs/commands.md b/docs/commands.md
index ec498b2..34b12bf 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -8,7 +8,7 @@ output and examples after installing.
| `redline` | Show first-run help. | `--version` |
| `redline demo` | Generate and diff the bundled demo logs. | `--public`, `--compact` |
| `redline status` | Show project readiness, latest local evidence, and the next command to run. | `--reports-dir`, `--history`, `--checkpoint`, `--limit`, `--json` |
-| `redline app` | Open the guided local product app for import, suite, eval, review, history, and integration commands. | `--reports-dir`, `--history`, `--checkpoint`, `--out`, `--no-open`, `--json` |
+| `redline app` | Open the guided local product app for import, suite, eval, review, history, and integration commands. | `--demo`, `--reports-dir`, `--history`, `--checkpoint`, `--out`, `--no-open`, `--json` |
| `redline init` | Write `redline.json`, runner files, and optional CI workflow. | `--runner`, `--copy-runner`, `--github-action`, `--force` |
| `redline doctor` | Check config, suite, replay, reports, audit, and team workflow. | `--strict`, `--json` |
| `redline watch` | Collect prompt-response observations or print snippets. | `--log`, `--stats`, `--snippet`, `--follow` |
diff --git a/redline/cli.py b/redline/cli.py
index 2a825b2..056c842 100644
--- a/redline/cli.py
+++ b/redline/cli.py
@@ -151,7 +151,7 @@ def _root_help() -> str:
Start here:
redline demo
redline status
- redline app
+ redline app --demo
redline quick-check path/to/baseline.jsonl path/to/candidate.jsonl --open
redline dashboard
redline init --runner stdio --copy-runner
@@ -514,6 +514,8 @@ def build_parser() -> argparse.ArgumentParser:
app_parser.add_argument("--checkpoint", default=".redline/audit-checkpoint.json", help="audit checkpoint JSON path")
app_parser.add_argument("--out", default=".redline/app.html", help="app HTML output path")
app_parser.add_argument("--limit", type=int, default=20, help="recent reports/history entries to include; use 0 for all")
+ app_parser.add_argument("--demo", action="store_true", help="generate the public demo reports before opening the app")
+ app_parser.add_argument("--demo-out", default=".redline/demo", help="demo output directory for --demo")
app_parser.add_argument("--open", dest="open", action="store_true", default=True, help="open the local app in the default browser")
app_parser.add_argument("--no-open", dest="open", action="store_false", help="write the app HTML without opening a browser")
app_parser.add_argument("--json", action="store_true", help="print machine-readable app metadata")
@@ -1572,6 +1574,12 @@ def cmd_dashboard(args: argparse.Namespace) -> int:
def cmd_app(args: argparse.Namespace) -> int:
+ if args.demo:
+ run_demo(args.demo_out, public=True)
+ if args.reports_dir == ".redline/reports":
+ args.reports_dir = str(Path(args.demo_out) / "reports")
+ if not args.json:
+ print(f"Generated public demo evidence in {Path(args.demo_out)}.")
return _write_dashboard_artifact(
args,
style="app",
diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py
index 21e24d8..9a31989 100644
--- a/tests/test_dashboard.py
+++ b/tests/test_dashboard.py
@@ -611,6 +611,32 @@ def test_cli_app_can_write_without_opening_browser(self) -> None:
self.assertIn("Open:", stdout.getvalue())
open_browser.assert_not_called()
+ def test_cli_app_demo_generates_public_reports(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ output = root / ".redline" / "app.html"
+
+ current = Path.cwd()
+ stdout = io.StringIO()
+ try:
+ import os
+
+ os.chdir(root)
+ with patch("redline.cli.webbrowser.open") as open_browser:
+ with contextlib.redirect_stdout(stdout):
+ code = main(["app", "--demo", "--no-open", "--out", str(output)])
+ finally:
+ os.chdir(current)
+
+ self.assertEqual(code, 0)
+ text = output.read_text(encoding="utf-8")
+ self.assertIn("Generated public demo evidence in .redline/demo.", stdout.getvalue())
+ self.assertIn("Reports: 1", stdout.getvalue())
+ self.assertTrue((root / ".redline" / "demo" / "reports" / "public_diff.json").exists())
+ self.assertIn("public_diff.json", text)
+ self.assertIn("Workflow", text)
+ open_browser.assert_not_called()
+
if __name__ == "__main__":
unittest.main()
From 07e9891096448597b99ef3838166c57a13de9239 Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 28 Jun 2026 20:38:41 -0600
Subject: [PATCH 38/55] Add auto-mapped log import
---
README.md | 2 ++
docs/commands.md | 7 +++---
docs/import-guides.md | 2 ++
redline/cli.py | 52 +++++++++++++++++++++++++++++++++++++---
redline/dashboard.py | 4 ++--
tests/test_cli_config.py | 45 ++++++++++++++++++++++++++++++++++
tests/test_dashboard.py | 1 +
7 files changed, 105 insertions(+), 8 deletions(-)
diff --git a/README.md b/README.md
index 4375e67..5d959b4 100644
--- a/README.md
+++ b/README.md
@@ -153,6 +153,8 @@ bounded FastAPI/ASGI middleware.
```bash
redline import downloaded.jsonl --detect
+redline import downloaded.jsonl --auto-map --preview 3
+redline import downloaded.jsonl --auto-map --out logs/baseline.jsonl
redline import downloaded.jsonl --input-field instruction --output-field response --preview 3
redline import downloaded.jsonl --input-field instruction --output-field response --out logs/baseline.jsonl
redline import langfuse-export.jsonl --preset langfuse --out logs/baseline.jsonl
diff --git a/docs/commands.md b/docs/commands.md
index 34b12bf..ee9e70c 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -12,7 +12,7 @@ output and examples after installing.
| `redline init` | Write `redline.json`, runner files, and optional CI workflow. | `--runner`, `--copy-runner`, `--github-action`, `--force` |
| `redline doctor` | Check config, suite, replay, reports, audit, and team workflow. | `--strict`, `--json` |
| `redline watch` | Collect prompt-response observations or print snippets. | `--log`, `--stats`, `--snippet`, `--follow` |
-| `redline import` | Normalize exported JSONL fields into redline `prompt`/`response` logs, with best-effort redaction on by default. | `--list-presets`, `--detect`, `--preview`, `--preset`, `--input-field`, `--output-field`, `--context-field`, `--metadata-field`, `--limit`, `--out`, `--no-redact` |
+| `redline import` | Normalize exported JSONL fields into redline `prompt`/`response` logs, with best-effort redaction on by default. | `--list-presets`, `--detect`, `--auto-map`, `--preview`, `--preset`, `--input-field`, `--output-field`, `--context-field`, `--metadata-field`, `--limit`, `--out`, `--no-redact` |
| `redline redact` | Check or write best-effort redacted logs. | `--check`, `--out`, `--json` |
| `redline cluster` | Inspect deterministic behavior-signature groups before suite generation. | `--max-cases`, `--json` |
| `redline suite` | Generate a suite from baseline JSONL logs. | `--out`, `--max-cases`, `--all-cases`, `--owner` |
@@ -57,8 +57,9 @@ templates for factual, tone, hallucination, policy, or reasoning risks.
## Import Help
-Use `redline import --detect` when you do not know an export's field names, then
-`--preview 3` before writing normalized logs. Source-specific recipes live in
+Use `redline import --detect` when you do not know an export's field names.
+Use `--auto-map --preview 3` when you want redline to try the best detected
+mapping before writing normalized logs. Source-specific recipes live in
[docs/import-guides.md](import-guides.md).
## Diff profiles
diff --git a/docs/import-guides.md b/docs/import-guides.md
index ad13280..616ffc5 100644
--- a/docs/import-guides.md
+++ b/docs/import-guides.md
@@ -6,6 +6,8 @@ write the normalized baseline.
```bash
redline import raw-export.jsonl --detect
+redline import raw-export.jsonl --auto-map --preview 3
+redline import raw-export.jsonl --auto-map --out logs/baseline.jsonl
redline import raw-export.jsonl --input-field request.prompt --output-field response.text --preview 3
redline import raw-export.jsonl --input-field request.prompt --output-field response.text --out logs/baseline.jsonl
```
diff --git a/redline/cli.py b/redline/cli.py
index 056c842..abfcab3 100644
--- a/redline/cli.py
+++ b/redline/cli.py
@@ -263,6 +263,7 @@ def build_parser() -> argparse.ArgumentParser:
import_parser.add_argument("path", nargs="?", help="source JSONL file to normalize")
import_parser.add_argument("--list-presets", action="store_true", help="list built-in import presets")
import_parser.add_argument("--detect", action="store_true", help="suggest prompt and response field mappings")
+ import_parser.add_argument("--auto-map", action="store_true", help="use the best detected prompt/response mapping")
import_parser.add_argument("--preset", choices=sorted(IMPORT_PRESETS), help="field mapping preset for common log exports")
import_parser.add_argument("--input-field", help="source field path containing prompt text")
import_parser.add_argument("--output-field", help="source field path containing response text")
@@ -835,9 +836,14 @@ def cmd_import(args: argparse.Namespace) -> int:
else:
print(format_import_detection(result), end="")
return 0
- preset = import_preset(args.preset) if args.preset else {}
+ auto_mapping = _auto_import_mapping(args) if args.auto_map else {}
+ preset_name = args.preset or str(auto_mapping.get("preset") or "")
+ preset = import_preset(preset_name) if preset_name else {}
input_field = args.input_field or str(preset.get("input_field") or "prompt")
output_field = args.output_field or str(preset.get("output_field") or "response")
+ if args.auto_map:
+ input_field = args.input_field or str(auto_mapping.get("input_field") or input_field)
+ output_field = args.output_field or str(auto_mapping.get("output_field") or output_field)
context_field = args.context_field if args.context_field is not None else _optional_preset_string(preset, "context_field")
id_field = args.id_field if args.id_field is not None else _optional_preset_string(preset, "id_field")
metadata_fields = _preset_string_list(preset, "metadata_fields")
@@ -854,7 +860,10 @@ def cmd_import(args: argparse.Namespace) -> int:
redact=not args.no_redact,
placeholder=args.redaction_placeholder,
)
- report["preset"] = args.preset or ""
+ report["preset"] = preset_name
+ report["auto_mapped"] = bool(args.auto_map)
+ if auto_mapping:
+ report["auto_mapping"] = auto_mapping
if args.json:
print(json.dumps(report, indent=2, sort_keys=True))
else:
@@ -874,11 +883,16 @@ def cmd_import(args: argparse.Namespace) -> int:
redact=not args.no_redact,
placeholder=args.redaction_placeholder,
)
- report["preset"] = args.preset or ""
+ report["preset"] = preset_name
+ report["auto_mapped"] = bool(args.auto_map)
+ if auto_mapping:
+ report["auto_mapping"] = auto_mapping
if args.json:
print(json.dumps(report, indent=2, sort_keys=True))
else:
print(f"Imported {report['records']} prompt-response pairs from {Path(str(report['source']))}.")
+ if report["auto_mapped"]:
+ _print_auto_mapping(report)
if report["preset"]:
print(f"Preset: {report['preset']}")
print(f"Mapped prompt: {report['input_field']}")
@@ -907,8 +921,40 @@ def cmd_import(args: argparse.Namespace) -> int:
return 0
+def _auto_import_mapping(args: argparse.Namespace) -> dict[str, object]:
+ detection = detect_import_fields(args.path, limit=args.limit or 20)
+ suggestions = detection.get("suggestions")
+ if not isinstance(suggestions, list) or not suggestions:
+ raise ValueError(
+ "auto-map could not find a prompt/response mapping; run "
+ "`redline import path/to/export.jsonl --detect` and pass fields manually"
+ )
+ best = suggestions[0]
+ if not isinstance(best, dict):
+ raise ValueError("auto-map produced an invalid mapping; pass --input-field and --output-field manually")
+ return {
+ "input_field": str(best.get("input_field") or ""),
+ "output_field": str(best.get("output_field") or ""),
+ "preset": str(best.get("preset") or ""),
+ "score": int(best.get("score") or 0),
+ "matches": int(best.get("matches") or 0),
+ "records_scanned": int(best.get("records_scanned") or 0),
+ }
+
+
+def _print_auto_mapping(report: Mapping[str, object]) -> None:
+ mapping = report.get("auto_mapping")
+ mapping = mapping if isinstance(mapping, Mapping) else {}
+ details = f"score {mapping.get('score', 0)}"
+ if mapping.get("preset"):
+ details += f"; preset {mapping['preset']}"
+ print(f"Auto-mapped: yes ({details})")
+
+
def _print_import_preview(report: Mapping[str, object]) -> None:
print(f"Previewed {report['previewed']} prompt-response pairs from {Path(str(report['source']))}.")
+ if report.get("auto_mapped"):
+ _print_auto_mapping(report)
if report.get("preset"):
print(f"Preset: {report['preset']}")
print(f"Mapped prompt: {report['input_field']}")
diff --git a/redline/dashboard.py b/redline/dashboard.py
index 6989f09..aafcb06 100644
--- a/redline/dashboard.py
+++ b/redline/dashboard.py
@@ -357,7 +357,7 @@ def _app_workflow_items(
"stage": "2",
"title": "Preview import",
"body": "Check a few mapped rows before creating a baseline log.",
- "command": "redline import path/to/export.jsonl --preset langfuse --preview 3",
+ "command": "redline import path/to/export.jsonl --auto-map --preview 3",
"tone": "green" if has_reports else "amber",
},
{
@@ -564,7 +564,7 @@ def _app_logs_screen(*, reports: list[Any], suite_summary: dict[str, Any]) -> st
"
"
f'
{_app_icon("upload")} Import log file
'
'
'
- '
Drop exported JSONL into the project, then detect fields locally.
Use redline import --detect, then --preview 3 before writing normalized logs.
'
+ '
Drop exported JSONL into the project, then detect fields locally.
Use redline import --detect, then --auto-map --preview 3 before writing normalized logs.
'
'
1
Detect fields from Langfuse, Helicone, Datadog, OpenAI chat, or custom JSONL exports.
'
'
2
Redaction is best-effort pattern matching, not a privacy boundary. Inspect private logs locally.
'
'
3
Run redline quick-check baseline.jsonl candidate.jsonl --open for the fastest first pass.
'
diff --git a/tests/test_cli_config.py b/tests/test_cli_config.py
index cbbcdd8..49e3715 100644
--- a/tests/test_cli_config.py
+++ b/tests/test_cli_config.py
@@ -191,6 +191,51 @@ def test_import_command_previews_external_jsonl_without_output(self) -> None:
self.assertIn('"category": "summarization"', text)
self.assertFalse(imported.exists())
+ def test_import_command_auto_maps_external_jsonl(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ source = root / "downloaded.jsonl"
+ imported = root / "baseline.jsonl"
+ source.write_text(
+ (
+ '{"instruction": "Summarize", "context": "Policy text", '
+ '"response": "Summary", "category": "summarization"}\n'
+ ),
+ encoding="utf-8",
+ )
+ output = io.StringIO()
+
+ with contextlib.redirect_stdout(output):
+ self.assertEqual(main(["import", str(source), "--auto-map", "--out", str(imported)]), 0)
+
+ text = output.getvalue()
+ self.assertIn("Auto-mapped: yes (score 100; preset dolly)", text)
+ self.assertIn("Preset: dolly", text)
+ self.assertIn("Mapped prompt: instruction", text)
+ self.assertIn("Mapped response: response", text)
+ row = json.loads(imported.read_text(encoding="utf-8"))
+ self.assertEqual(row["prompt"], "Summarize\n\nContext:\nPolicy text")
+ self.assertEqual(row["response"], "Summary")
+ self.assertEqual(row["metadata"], {"category": "summarization"})
+
+ def test_import_command_auto_maps_preview_without_output(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ source = root / "downloaded.jsonl"
+ imported = root / "baseline.jsonl"
+ source.write_text('{"input": "Classify", "output": "billing"}\n', encoding="utf-8")
+ output = io.StringIO()
+
+ with contextlib.redirect_stdout(output):
+ self.assertEqual(main(["import", str(source), "--auto-map", "--preview", "1"]), 0)
+
+ text = output.getvalue()
+ self.assertIn("Previewed 1 prompt-response pairs", text)
+ self.assertIn("Auto-mapped: yes (score 100; preset langfuse)", text)
+ self.assertIn("Mapped prompt: input", text)
+ self.assertIn("Mapped response: output", text)
+ self.assertFalse(imported.exists())
+
def test_import_command_detects_external_jsonl_fields(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py
index 9a31989..a0f9f8d 100644
--- a/tests/test_dashboard.py
+++ b/tests/test_dashboard.py
@@ -328,6 +328,7 @@ def test_app_dashboard_renders_real_report_data(self) -> None:
self.assertIn("dashboard never runs shell commands", html)
self.assertIn('data-copy="redline demo --public --compact"', html)
self.assertIn("redline import path/to/export.jsonl --detect", html)
+ self.assertIn("redline import path/to/export.jsonl --auto-map --preview 3", html)
self.assertIn("redline suite .redline/logs/baseline.jsonl", html)
self.assertIn("redline eval --compact", html)
self.assertIn("redline history .redline/reports/eval.json", html)
From bf60c2eddcedac5ce52548d56a1b35d918d0ac5f Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 28 Jun 2026 20:41:34 -0600
Subject: [PATCH 39/55] Show suite readiness during generation
---
README.md | 3 +++
redline/cli.py | 10 ++++++++++
tests/test_cli_config.py | 3 +++
3 files changed, 16 insertions(+)
diff --git a/README.md b/README.md
index 5d959b4..6cea875 100644
--- a/README.md
+++ b/README.md
@@ -166,6 +166,9 @@ Use `--detect` when you do not know the field names. Use `--preview` when the
export is new to you; it shows mapped, redacted sample rows without writing a
baseline file.
+Suite generation prints a readiness score and improvement suggestions. That
+score measures suite health, not model quality or candidate safety.
+
### 2. Suite
redline groups behavior into deterministic signatures and selects
diff --git a/redline/cli.py b/redline/cli.py
index abfcab3..19af749 100644
--- a/redline/cli.py
+++ b/redline/cli.py
@@ -1265,11 +1265,16 @@ def cmd_suite(args: argparse.Namespace) -> int:
},
},
)
+ suite_health = suite_summary(suite)
+ readiness = suite_health.get("suite_readiness")
+ readiness = readiness if isinstance(readiness, dict) else {}
summary = suite["summary"]
print(f"Generated {summary['cases']} cases from {summary['records_seen']} records.")
if summary.get("duplicate_prompt_response_pairs"):
print(f"Skipped {summary['duplicate_prompt_response_pairs']} duplicate prompt-response pairs.")
print(f"Detected {summary['clusters']} behavior-signature groups.")
+ print(f"Suite readiness: {readiness.get('label', 'unknown')} ({readiness.get('score', 0)}/100)")
+ print("Readiness scope: suite health, not model quality or candidate safety")
if summary.get("selection") != "all":
unique_pairs = int(summary.get("unique_prompt_response_pairs", summary["records_seen"]))
cases = int(summary["cases"])
@@ -1278,6 +1283,11 @@ def cmd_suite(args: argparse.Namespace) -> int:
f"Selected {cases} representative cases from {unique_pairs} unique prompt-response pairs. "
"Use --all-cases for exhaustive coverage or redline suite add for must-cover edge cases."
)
+ improvement_steps = suite_health.get("next_steps")
+ if isinstance(improvement_steps, list) and improvement_steps:
+ print("Improve suite:")
+ for step in improvement_steps[:2]:
+ print(f"- {step}")
print(f"Wrote {Path(output)}.")
print()
print("Next:")
diff --git a/tests/test_cli_config.py b/tests/test_cli_config.py
index 49e3715..496880d 100644
--- a/tests/test_cli_config.py
+++ b/tests/test_cli_config.py
@@ -849,6 +849,9 @@ def test_suite_output_explains_representative_selection(self) -> None:
)
text = output.getvalue()
+ self.assertIn("Suite readiness:", text)
+ self.assertIn("Readiness scope: suite health, not model quality or candidate safety", text)
+ self.assertIn("Improve suite:", text)
self.assertIn("Selected 1 representative cases from 3 unique prompt-response pairs.", text)
self.assertIn("Use --all-cases for exhaustive coverage", text)
self.assertIn("redline suite add for must-cover edge cases", text)
From 5da3ecadf786019ed5fd43b19a6d740833ed4ead Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 28 Jun 2026 20:51:30 -0600
Subject: [PATCH 40/55] Expose status and auto import through MCP
---
docs/mcp.md | 9 +++++--
redline/mcp.py | 65 ++++++++++++++++++++++++++++++++++-------------
tests/test_mcp.py | 48 ++++++++++++++++++++++++++++++++++
3 files changed, 103 insertions(+), 19 deletions(-)
diff --git a/docs/mcp.md b/docs/mcp.md
index d694b64..43bd8f1 100644
--- a/docs/mcp.md
+++ b/docs/mcp.md
@@ -63,11 +63,12 @@ reports.
Available tools:
+- `redline_status`
- `redline_doctor`
- `redline_suite`
- `redline_quick_check`
- `redline_redact`
-- `redline_import` (supports `detect` and `preview` before writing)
+- `redline_import` (supports `detect`, `auto_map`, and `preview` before writing)
- `redline_import_presets`
- `redline_watch_stats`
- `redline_watch_snippets`
@@ -103,7 +104,7 @@ assistants can reason over reports without scraping terminal text.
The server also exposes MCP prompt templates for the workflows agents should
reach for most often:
-- `check_prompt_change`: run doctor, then eval a changed prompt file.
+- `check_prompt_change`: run status/doctor, then eval a changed prompt file.
- `build_suite_from_logs`: generate a suite, validate it, and summarize coverage.
- `review_candidate_outputs`: quick-check two JSONL logs or diff candidate outputs against a suite, then lead with blocking findings.
- `setup_redline_project`: guide first-time setup through runner selection,
@@ -122,6 +123,10 @@ Ask your assistant:
Run redline doctor in this repo and tell me the next setup step.
```
+```text
+Run redline status in this repo and tell me the exact next command.
+```
+
```text
Set up redline for this project. Use my existing logs if available, choose the
right runner adapter, and do not mutate the baseline.
diff --git a/redline/mcp.py b/redline/mcp.py
index ca46efa..7468e00 100644
--- a/redline/mcp.py
+++ b/redline/mcp.py
@@ -107,8 +107,8 @@ def main(argv: Sequence[str] | None = None) -> int:
print(
"redline-mcp\n\n"
"Local MCP stdio server for redline.\n\n"
- "Run this command from an MCP client. It exposes redline doctor, suite,\n"
- "quick-check, watch stats, prompts, runners, judges, redact, audit, SBOM, benchmark, validate, summary, diff, eval, history, dashboard, cases, case, and guarded mark tools.\n",
+ "Run this command from an MCP client. It exposes redline status, doctor, suite,\n"
+ "quick-check, import, watch stats, prompts, runners, judges, redact, audit, SBOM, benchmark, validate, summary, diff, eval, history, dashboard, cases, case, and guarded mark tools.\n",
end="",
)
return 0
@@ -399,11 +399,12 @@ def _build_check_prompt_change_prompt(arguments: dict[str, Any]) -> str:
f"- Prompt file: {prompt_path}\n"
f"- Suite: {suite_path}\n\n"
"Use this workflow:\n"
- "1. Call `redline_doctor` first. If setup has errors, stop and tell me the next fix.\n"
- "2. Call `redline_eval` with the prompt file when provided and the suite when provided.\n"
- "3. Treat exit code 1 as a redline finding, not a tool failure.\n"
- "4. Summarize regressions, missing outputs, changed cases, and the recommended action.\n"
- "5. Do not call baseline mutation commands or tell me the change is safe when redline only found neutral output.\n"
+ "1. Call `redline_status` first so I can see current readiness and the exact next command.\n"
+ "2. Call `redline_doctor` if status reports setup gaps. If setup has errors, stop and tell me the next fix.\n"
+ "3. Call `redline_eval` with the prompt file when provided and the suite when provided.\n"
+ "4. Treat exit code 1 as a redline finding, not a tool failure.\n"
+ "5. Summarize regressions, missing outputs, changed cases, and the recommended action.\n"
+ "6. Do not call baseline mutation commands or tell me the change is safe when redline only found neutral output.\n"
)
@@ -460,16 +461,18 @@ def _build_setup_redline_project_prompt(arguments: dict[str, Any]) -> str:
f"- Runner preference: {runner}\n"
f"- Judge preference: {judge}\n\n"
"Use this workflow:\n"
- "1. Call `redline_doctor` first and explain the next setup gap in plain language.\n"
- "2. Call `redline_runners` to show adapter choices. If I named a runner, copy that runner; otherwise recommend the safest adapter for my app shape before copying anything.\n"
- "3. If I do not already have logs, call `redline_watch_snippets` for the app shape so I can add local capture first.\n"
- "4. If prompt files are available, call `redline_prompts` to create or check a prompt-to-suite manifest.\n"
- "5. If logs are available, call `redline_suite`, then `redline_validate` and `redline_summary` so I can inspect coverage before trusting the suite.\n"
- "6. If summary reports coverage gaps, call `redline_cases` or `redline_case` and recommend a `redline suite add` command I can run; do not mutate the suite yourself.\n"
- "7. Call `redline_budget` before recommending CI gating.\n"
- "8. Call `redline_judges` only when structural checks cannot cover factual, tone, hallucination, or reasoning risk; copy a judge template only after naming why it is needed.\n"
- "9. Finish by re-running `redline_doctor` with strict setup when possible and list the exact next commands I should run.\n"
- "10. Do not call baseline mutation commands, do not upload private logs, and do not say green or neutral means semantically safe.\n"
+ "1. Call `redline_status` first and explain the next setup gap in plain language.\n"
+ "2. Call `redline_doctor` when status reports setup gaps or when I ask for strict setup validation.\n"
+ "3. Call `redline_runners` to show adapter choices. If I named a runner, copy that runner; otherwise recommend the safest adapter for my app shape before copying anything.\n"
+ "4. If I do not already have logs, call `redline_watch_snippets` for the app shape so I can add local capture first.\n"
+ "5. If prompt files are available, call `redline_prompts` to create or check a prompt-to-suite manifest.\n"
+ "6. If logs are available but field names are unclear, call `redline_import` with `detect=true`, then use `auto_map=true` with preview before writing normalized logs.\n"
+ "7. If logs are available, call `redline_suite`, then `redline_validate` and `redline_summary` so I can inspect coverage before trusting the suite.\n"
+ "8. If summary reports coverage gaps, call `redline_cases` or `redline_case` and recommend a `redline suite add` command I can run; do not mutate the suite yourself.\n"
+ "9. Call `redline_budget` before recommending CI gating.\n"
+ "10. Call `redline_judges` only when structural checks cannot cover factual, tone, hallucination, or reasoning risk; copy a judge template only after naming why it is needed.\n"
+ "11. Finish by re-running `redline_status` and list the exact next commands I should run.\n"
+ "12. Do not call baseline mutation commands, do not upload private logs, and do not say green or neutral means semantically safe.\n"
)
@@ -484,6 +487,21 @@ def _optional_prompt_argument(arguments: dict[str, Any], key: str, fallback: str
def _tools() -> list[ToolSpec]:
return [
+ ToolSpec(
+ "redline_status",
+ "Show project readiness, latest local evidence, and the next command to run.",
+ _schema(
+ {
+ "config": _string("Config path to read."),
+ "reports_dir": _string("Directory containing redline JSON reports."),
+ "history": _string("History JSONL path."),
+ "checkpoint": _string("Audit checkpoint JSON path."),
+ "limit": _integer("Recent reports/history entries to inspect; use 0 for all."),
+ "json": _boolean("Print machine-readable JSON."),
+ }
+ ),
+ _build_status,
+ ),
ToolSpec(
"redline_doctor",
"Check local redline setup health and print next steps.",
@@ -557,6 +575,7 @@ def _tools() -> list[ToolSpec]:
"path": _string("Source JSONL file to normalize."),
"out": _string("Redline JSONL output path."),
"detect": _boolean("Suggest prompt and response field mappings without writing output."),
+ "auto_map": _boolean("Use the best detected prompt/response mapping."),
"preset": _string("Optional import preset such as langfuse, helicone, datadog, dolly, or openai-chat."),
"input_field": _string("Source field path containing prompt text."),
"output_field": _string("Source field path containing response text."),
@@ -885,6 +904,17 @@ def _tool_by_name(name: str) -> ToolSpec | None:
return next((tool for tool in _tools() if tool.name == name), None)
+def _build_status(arguments: dict[str, Any]) -> list[str]:
+ args = ["status"]
+ _add_option(args, "--config", arguments.get("config"))
+ _add_option(args, "--reports-dir", arguments.get("reports_dir"))
+ _add_option(args, "--history", arguments.get("history"))
+ _add_option(args, "--checkpoint", arguments.get("checkpoint"))
+ _add_option(args, "--limit", arguments.get("limit"))
+ _add_flag(args, "--json", arguments.get("json"))
+ return args
+
+
def _build_doctor(arguments: dict[str, Any]) -> list[str]:
args = ["doctor"]
_add_option(args, "--config", arguments.get("config"))
@@ -940,6 +970,7 @@ def _build_import(arguments: dict[str, Any]) -> list[str]:
else:
_add_option(args, "--out", arguments.get("out"))
_add_flag(args, "--detect", arguments.get("detect"))
+ _add_flag(args, "--auto-map", arguments.get("auto_map"))
_add_option(args, "--preset", arguments.get("preset"))
_add_option(args, "--input-field", arguments.get("input_field"))
_add_option(args, "--output-field", arguments.get("output_field"))
diff --git a/tests/test_mcp.py b/tests/test_mcp.py
index f94971f..28cd313 100644
--- a/tests/test_mcp.py
+++ b/tests/test_mcp.py
@@ -28,6 +28,7 @@ def test_tools_list_exposes_redline_tools_with_guarded_writes(self) -> None:
assert response is not None
names = {tool["name"] for tool in response["result"]["tools"]}
+ self.assertIn("redline_status", names)
self.assertIn("redline_doctor", names)
self.assertIn("redline_suite", names)
self.assertIn("redline_quick_check", names)
@@ -172,6 +173,34 @@ def test_import_tool_detects_external_jsonl_fields(self) -> None:
self.assertEqual(result["structuredContent"]["json"]["suggestions"][0]["input_field"], "input")
self.assertEqual(result["structuredContent"]["json"]["suggestions"][0]["output_field"], "output")
+ def test_import_tool_auto_maps_external_jsonl(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ source = root / "downloaded.jsonl"
+ output = root / "baseline.jsonl"
+ source.write_text('{"instruction": "Classify", "response": "billing"}\n', encoding="utf-8")
+
+ result = call_tool(
+ "redline_import",
+ {
+ "cwd": directory,
+ "path": str(source),
+ "out": str(output),
+ "auto_map": True,
+ "json": True,
+ },
+ )
+ wrote_output = output.exists()
+
+ self.assertFalse(result["isError"])
+ self.assertEqual(result["structuredContent"]["exit_code"], 0)
+ payload = result["structuredContent"]["json"]
+ self.assertEqual(payload["records"], 1)
+ self.assertTrue(payload["auto_mapped"])
+ self.assertEqual(payload["input_field"], "instruction")
+ self.assertEqual(payload["output_field"], "response")
+ self.assertTrue(wrote_output)
+
def test_import_presets_tool_lists_mappings(self) -> None:
result = call_tool("redline_import_presets", {"json": True})
@@ -226,6 +255,7 @@ def test_prompt_get_builds_check_prompt_change_workflow(self) -> None:
assert response is not None
text = response["result"]["messages"][0]["content"]["text"]
+ self.assertIn("redline_status", text)
self.assertIn("redline_doctor", text)
self.assertIn("redline_eval", text)
self.assertIn("prompts/v2.txt", text)
@@ -304,9 +334,11 @@ def test_prompt_get_builds_first_time_setup_workflow(self) -> None:
assert response is not None
text = response["result"]["messages"][0]["content"]["text"]
+ self.assertIn("redline_status", text)
self.assertIn("redline_doctor", text)
self.assertIn("redline_runners", text)
self.assertIn("redline_watch_snippets", text)
+ self.assertIn("auto_map=true", text)
self.assertIn("redline_prompts", text)
self.assertIn("redline_suite", text)
self.assertIn("redline_validate", text)
@@ -321,6 +353,22 @@ def test_prompt_get_builds_first_time_setup_workflow(self) -> None:
self.assertIn("support-rubric", text)
self.assertIn("do not say green or neutral means semantically safe", text)
+ def test_status_tool_reports_next_command(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ result = call_tool(
+ "redline_status",
+ {
+ "cwd": directory,
+ "json": True,
+ },
+ )
+
+ self.assertFalse(result["isError"])
+ self.assertEqual(result["structuredContent"]["exit_code"], 0)
+ payload = result["structuredContent"]["json"]
+ self.assertEqual(payload["state"], "setup")
+ self.assertEqual(payload["next_command"], "redline init --runner stdio --copy-runner")
+
def test_unknown_prompt_returns_jsonrpc_error(self) -> None:
response = handle_jsonrpc_line(
json.dumps(
From 77be338e46bd08e53e0c753f519a1a09fb0f5a85 Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 28 Jun 2026 20:57:09 -0600
Subject: [PATCH 41/55] Add per-case impact guidance
---
redline-report.schema.json | 4 +++
redline/diff.py | 32 ++++++++++++++++++++++++
redline/reports.py | 29 ++++------------------
tests/test_diff.py | 51 ++++++++++++++++++++++++++++++++++++++
tests/test_reports.py | 27 ++++++++++++++++++++
5 files changed, 119 insertions(+), 24 deletions(-)
diff --git a/redline-report.schema.json b/redline-report.schema.json
index 52c2e6f..04214a0 100644
--- a/redline-report.schema.json
+++ b/redline-report.schema.json
@@ -118,6 +118,10 @@
"baseline_response": {"type": "string"},
"candidate_response": {"type": ["string", "null"]},
"reasons": {"type": "array", "items": {"type": "string"}},
+ "impact": {
+ "type": "string",
+ "description": "Plain-English explanation of why this case matters to reviewers."
+ },
"confidence": {"type": "string", "enum": ["low", "medium", "high"]},
"signal": {
"type": "string",
diff --git a/redline/diff.py b/redline/diff.py
index b0005c8..8778a93 100644
--- a/redline/diff.py
+++ b/redline/diff.py
@@ -149,6 +149,7 @@ def to_dict(self) -> dict[str, Any]:
"baseline_response": self.baseline_response,
"candidate_response": self.candidate_response,
"reasons": list(self.reasons),
+ "impact": case_impact(self.status, self.reasons),
"confidence": self.confidence,
"signal": self.signal,
"baseline_features": self.baseline_features,
@@ -552,6 +553,34 @@ def _case_confidence_signal(status: str, reasons: list[str]) -> tuple[str, str]:
return "medium", "shallow_semantic"
+def case_impact(status: str, reasons: tuple[str, ...] | list[str]) -> str:
+ text = " ".join(str(reason).lower() for reason in reasons)
+ normalized_status = status.lower()
+ if normalized_status == "missing":
+ return "Replay did not produce a candidate output, so this baseline behavior is untested."
+ if "valid json" in text or "json keys" in text:
+ return "Downstream code may fail if consumers expect parseable JSON or required fields."
+ if "markdown table" in text or "table structure" in text:
+ return "A consumer expecting rows and columns may receive unstructured prose instead."
+ if "code block" in text:
+ return "A developer or tool expecting copyable code may lose the executable structure."
+ if "bullet list" in text or "numbered list" in text:
+ return "A workflow expecting ordered or scannable steps may become harder to review."
+ if "missing numbers" in text or "missing urls" in text or "missing entities" in text:
+ return "Concrete details used for decisions, routing, or compliance may have been dropped."
+ if "newly refuses" in text or "refusal" in text:
+ return "A previously supported safe workflow may now be blocked."
+ if "became empty" in text:
+ return "The user may receive no usable answer."
+ if "content changed substantially" in text or "much shorter" in text:
+ return "Meaning may have changed enough to require human review before acceptance."
+ if normalized_status == "regression":
+ return "This case changed in a way redline treats as blocking before shipping."
+ if normalized_status == "changed":
+ return "Review whether this behavioral change is intentional before accepting it."
+ return ""
+
+
def _is_requirement_reason(reason: str) -> bool:
return reason.startswith("candidate missing required text:") or reason.startswith(
"candidate includes forbidden text:"
@@ -637,6 +666,9 @@ def format_report(result: dict[str, Any], *, title: str = "redline diff") -> str
lines.append(status.upper())
for item in matching:
lines.append(f"- {_case_label(item)}: {_preview(item['prompt'])}")
+ impact = str(item.get("impact") or "")
+ if impact:
+ lines.append(f" - why this matters: {impact}")
for reason in item["reasons"]:
lines.append(f" - {reason}")
lines.append("")
diff --git a/redline/reports.py b/redline/reports.py
index 0440adb..3587b69 100644
--- a/redline/reports.py
+++ b/redline/reports.py
@@ -5,6 +5,7 @@
from typing import Any
from xml.etree import ElementTree
+from .diff import case_impact
from .labels import behavior_label
@@ -530,32 +531,12 @@ def _percent_value(value: object) -> str:
def _why_this_matters(item: dict[str, Any]) -> str:
+ impact = str(item.get("impact") or "").strip()
+ if impact:
+ return impact
status = str(item.get("status") or "").lower()
reasons = item.get("reasons")
- text = " ".join(str(reason).lower() for reason in reasons) if isinstance(reasons, list) else ""
- if status == "missing":
- return "Replay did not produce a candidate output, so this baseline behavior is untested."
- if "valid json" in text or "json keys" in text:
- return "Downstream code may fail if consumers expect parseable JSON or required fields."
- if "markdown table" in text or "table structure" in text:
- return "A consumer expecting rows and columns may receive unstructured prose instead."
- if "code block" in text:
- return "A developer or tool expecting copyable code may lose the executable structure."
- if "bullet list" in text or "numbered list" in text:
- return "A workflow expecting ordered or scannable steps may become harder to review."
- if "missing numbers" in text or "missing urls" in text or "missing entities" in text:
- return "Concrete details used for decisions, routing, or compliance may have been dropped."
- if "newly refuses" in text or "refusal" in text:
- return "A previously supported safe workflow may now be blocked."
- if "became empty" in text:
- return "The user may receive no usable answer."
- if "content changed substantially" in text or "much shorter" in text:
- return "Meaning may have changed enough to require human review before acceptance."
- if status == "regression":
- return "This case changed in a way redline treats as blocking before shipping."
- if status == "changed":
- return "Review whether this behavioral change is intentional before accepting it."
- return ""
+ return case_impact(status, reasons if isinstance(reasons, list) else [])
def _result_warnings(result: dict[str, Any]) -> list[str]:
diff --git a/tests/test_diff.py b/tests/test_diff.py
index 42b4834..335de37 100644
--- a/tests/test_diff.py
+++ b/tests/test_diff.py
@@ -4,6 +4,7 @@
from redline.diff import (
REPORT_SCHEMA_URL,
+ case_impact,
classify_change,
compare_suite_to_candidate,
format_compact_report,
@@ -32,6 +33,7 @@ def test_report_schema_documents_generated_report_fields(self) -> None:
self.assertIn("diffs", schema["properties"])
diff_properties = schema["properties"]["diffs"]["items"]["properties"]
self.assertIn("owner_rule", diff_properties)
+ self.assertIn("impact", diff_properties)
def test_report_carries_suite_methodology(self) -> None:
suite = build_suite(
@@ -357,6 +359,20 @@ def test_compare_detects_missing_candidate(self) -> None:
self.assertIn("cluster", result["diffs"][0])
self.assertEqual(result["diffs"][0]["confidence"], "high")
self.assertEqual(result["diffs"][0]["signal"], "structural")
+ self.assertEqual(
+ result["diffs"][0]["impact"],
+ "Replay did not produce a candidate output, so this baseline behavior is untested.",
+ )
+
+ def test_case_impact_explains_common_regression_shapes(self) -> None:
+ self.assertEqual(
+ case_impact("regression", ["candidate missing numbers: 30"]),
+ "Concrete details used for decisions, routing, or compliance may have been dropped.",
+ )
+ self.assertEqual(
+ case_impact("changed", ["short answer changed"]),
+ "Review whether this behavioral change is intentional before accepting it.",
+ )
def test_compare_summarizes_plain_english_diagnosis(self) -> None:
prompt = "Return a numbered rollout checklist with owner, URL, and 30 day deadline."
@@ -514,6 +530,41 @@ def test_format_report_includes_decision(self) -> None:
self.assertIn("Warnings:", report)
self.assertIn("prompt file prompts/v2.txt is newer than suite", report)
+ def test_format_report_includes_case_impact(self) -> None:
+ result = {
+ "summary": {
+ "cases": 1,
+ "regression": 1,
+ "changed": 0,
+ "improved": 0,
+ "accepted": 0,
+ "ignored": 0,
+ "neutral": 0,
+ "missing": 0,
+ },
+ "decision": {
+ "confidence": "high",
+ "recommended_action": "fix blocking cases before shipping",
+ "scope": "structural checks only",
+ },
+ "diffs": [
+ {
+ "case_id": "case_001",
+ "status": "regression",
+ "prompt": "Return JSON",
+ "reasons": ["candidate lost valid JSON format"],
+ "impact": "Downstream code may fail if consumers expect parseable JSON or required fields.",
+ }
+ ],
+ }
+
+ report = format_report(result)
+
+ self.assertIn(
+ "why this matters: Downstream code may fail if consumers expect parseable JSON or required fields.",
+ report,
+ )
+
def test_format_compact_report_outputs_one_line_per_actionable_case(self) -> None:
result = {
"summary": {
diff --git a/tests/test_reports.py b/tests/test_reports.py
index c8ca296..21a44e2 100644
--- a/tests/test_reports.py
+++ b/tests/test_reports.py
@@ -152,6 +152,33 @@ def test_markdown_inline_code_preserves_backticks(self) -> None:
self.assertIn("Prompt: ``Use `json` output``", report)
+ def test_markdown_report_prefers_explicit_case_impact(self) -> None:
+ result = {
+ "summary": {"regression": 1, "changed": 0, "improved": 0, "neutral": 0, "missing": 0},
+ "diffs": [
+ {
+ "case_id": "case_001",
+ "status": "regression",
+ "prompt": "Return JSON",
+ "baseline_response": '{"owner":"billing"}',
+ "candidate_response": "billing",
+ "reasons": ["candidate lost valid JSON format"],
+ "impact": "Billing routing automation may fail because the owner field disappeared.",
+ }
+ ],
+ }
+
+ report = format_markdown_report(result)
+
+ self.assertIn(
+ "Why this matters: Billing routing automation may fail because the owner field disappeared.",
+ report,
+ )
+ self.assertNotIn(
+ "Why this matters: Downstream code may fail if consumers expect parseable JSON or required fields.",
+ report,
+ )
+
def test_markdown_code_blocks_use_fence_longer_than_output_backticks(self) -> None:
result = {
"summary": {"regression": 0, "changed": 1, "improved": 0, "neutral": 0, "missing": 0},
From e9646caeec326353282adfbe67ad83e3026d0758 Mon Sep 17 00:00:00 2001
From: Gowtham
Date: Sun, 28 Jun 2026 21:03:41 -0600
Subject: [PATCH 42/55] Show case impact in dashboard review
---
redline/dashboard.py | 13 ++++++++++---
tests/test_dashboard.py | 9 +++++++++
2 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/redline/dashboard.py b/redline/dashboard.py
index aafcb06..e8c38fb 100644
--- a/redline/dashboard.py
+++ b/redline/dashboard.py
@@ -7,7 +7,7 @@
from typing import Any
from urllib.parse import quote
-from .diff import TRUST_SCOPE
+from .diff import TRUST_SCOPE, case_impact
from .history import history_trend, read_history
from .io import read_json
@@ -706,11 +706,13 @@ def _app_review_rows(items: list[Any], *, empty: str = "No review cases found.")
case_id = str(item.get("case_id") or item.get("suite_case_id") or "-")
prompt = _preview(str(item.get("prompt") or item.get("reason") or "Review case"), 96)
reason = _preview(str(item.get("reason") or "; ".join(str(value) for value in item.get("reasons", []) if value) or ""), 120)
+ impact = _preview(str(item.get("impact") or ""), 140)
+ impact_html = f'
Why this matters: {_h(impact)}
' if impact else ""
tone = "red" if status in {"regression", "missing"} else "amber" if status == "changed" else "green"
rows.append(
'
'
f'
{_h(status[:1].upper())}
'
- f'
{_h(case_id)} - {_h(prompt)}
{_h(reason or status)}
'
+ f'
{_h(case_id)} - {_h(prompt)}
{_h(reason or status)}
{impact_html}
'
f'{_h(status)}'
"
"
)
@@ -1303,6 +1305,9 @@ def _report_review_cases(diffs: Any, *, limit: int = 12, suite_path: str = "") -
first_reason = ""
if isinstance(reasons, list) and reasons:
first_reason = str(reasons[0])
+ impact = str(item.get("impact") or "").strip()
+ if not impact:
+ impact = case_impact(status, reasons if isinstance(reasons, list) else [])
rows.append(
{
"case_id": str(item.get("case_id") or ""),
@@ -1310,6 +1315,7 @@ def _report_review_cases(diffs: Any, *, limit: int = 12, suite_path: str = "") -
"owner": str(item.get("owner") or ""),
"prompt": str(item.get("prompt") or ""),
"reason": first_reason,
+ "impact": impact,
"confidence": str(item.get("confidence") or ""),
"signal": str(item.get("signal") or ""),
"prompt_path": str(item.get("prompt_path") or ""),
@@ -1970,7 +1976,7 @@ def _review_queue_panel(review_cases: Any) -> str:
"
"
f"
{_h(str(item.get('status') or '-'))}
"
f"
{_h(str(item.get('case_id') or '-'))}{_h(_preview(str(item.get('prompt') or '')))}
"
- f"
{_h(str(item.get('reason') or '-'))}
"
+ f"
{_h(str(item.get('reason') or '-'))}{_h(str(item.get('impact') or ''))}
@@ -110,7 +104,16 @@ Full guide: [docs/troubleshooting.md](docs/troubleshooting.md).
## Product Proof
-These are real local artifacts generated by `redline demo --public --compact`.
+redline has two proof paths: a fast first-run demo and a larger public-data
+dogfood run.
+
+| Proof | Command or data | Result |
+| --- | --- | --- |
+| First-run demo | `redline demo --public --compact` | 10 synthetic regressions caught locally with no API keys. |
+| Internet dogfood | 100 prompt-response rows sampled from [Databricks Dolly 15k](https://huggingface.co/datasets/databricks/databricks-dolly-15k) | 51 regressions, 27 changed cases, 22 neutral controls, and 0 dashboard warnings. |
+| Release gate | tests, lint, type check, action smoke, and release build | Package, CI, report, dashboard, and MCP paths are validated before publish. |
+
+These screenshots are local artifacts from the 100-row internet dogfood run.
| Dashboard | HTML report |
| --- | --- |
diff --git a/docs/case-studies.md b/docs/case-studies.md
index 7d8c678..d042412 100644
--- a/docs/case-studies.md
+++ b/docs/case-studies.md
@@ -1,8 +1,9 @@
# Case Studies
-These are repo-local dogfood runs that anyone can reproduce from a fresh
-checkout. They are not customer case studies yet. They are the proof fixtures we
-use until external users share anonymized logs.
+These are dogfood runs we use until external users share anonymized logs. The
+first two are repo-local fixtures that anyone can reproduce from a fresh
+checkout. The public internet dogfood run uses third-party rows that stay under
+`.redline/private/`, so only the protocol and aggregate result are committed.
The important property is that the data is checked in, deterministic, and
launch-safe. If a future redline change stops catching these regressions, the
@@ -131,6 +132,68 @@ Expected result: long-form assistant differences appear as `changed` review
items unless a candidate loses stronger deterministic signals such as required
structure, URLs, refusals, or empty output.
+## Case Study 4: Public Internet Dogfood
+
+Scenario: download a public instruction-response dataset, import a 100-row
+sample, create a deterministic local "candidate got shorter" variant, then
+verify that redline diagnoses the regression pattern across reports and
+dashboard surfaces.
+
+Source:
+
+- [Databricks Dolly 15k](https://huggingface.co/datasets/databricks/databricks-dolly-15k)
+- Local raw and normalized rows stay under `.redline/private/`.
+
+Protocol used:
+
+```bash
+curl -L \
+ https://huggingface.co/datasets/databricks/databricks-dolly-15k/resolve/main/databricks-dolly-15k.jsonl \
+ -o .redline/private/internet-dogfood-100/dolly-raw.jsonl
+
+redline import .redline/private/internet-dogfood-100/dolly-raw.jsonl \
+ --preset dolly \
+ --limit 100 \
+ --out .redline/private/internet-dogfood-100/dolly-baseline-100.jsonl
+
+redline suite .redline/private/internet-dogfood-100/dolly-baseline-100.jsonl \
+ --out .redline/private/internet-dogfood-100/dolly-suite-100.json \
+ --all-cases
+```
+
+The candidate file for this run was generated locally by applying deterministic
+shortening, refusal, empty-output, and generic-answer mutations to the imported
+baseline. It stays under `.redline/private/` with the downloaded source rows.
+For real dogfood, replace that mutation step with your actual candidate model or
+prompt runner.
+
+```bash
+redline diff .redline/private/internet-dogfood-100/dolly-suite-100.json \
+ .redline/private/internet-dogfood-100/dolly-candidate-100.jsonl \
+ --compact \
+ --fail-on none
+```
+
+Current result:
+
+```text
+redline diff: cases=100 regression=51 changed=27 improved=0 accepted=0 ignored=0 missing=0 neutral=22
+Diagnosis: Candidate got shorter, lost required structure, dropped concrete details, returned empty outputs, and changed content substantially; fix blocking cases before shipping.
+```
+
+What redline catches:
+
+- Shorter candidate outputs that drop concrete details.
+- Empty outputs and newly refused safe requests.
+- Structure loss in code, lists, and other format-sensitive answers.
+- Suite coverage across 100/100 cases and 20/20 behavior groups.
+- Dashboard sidecar filtering: 1 report, 1 benchmark, 1 history entry, and 0
+ warnings in the local product app.
+
+Why this matters: the demo proves the first-run promise; this run proves the
+same local loop can process a larger public prompt-response sample and produce
+dashboard/report evidence without cloud services or private data.
+
## External Case Studies Still Needed
The repo fixtures prove the loop and protect regressions in redline itself. The
diff --git a/site/assets/redline-dashboard-proof.png b/site/assets/redline-dashboard-proof.png
index 0a1b48b2b99da8f1f4a50ad23dbe29d5d631bdad..0f8e509a053076d5b5747e6836edead6e5c595e1 100644
GIT binary patch
literal 393322
zcmdSBWmH^SmoAJ;a7aN2?g{P$w_qVya4R6VlVF8IaJK-#2@Zw36z=Zs5Zv9VuTJ-S
zPT#NZ+x`EJvG%B4Huf5G&NbIF)j$Xs=d1>^~8_e(fPs39R&Jxw)w~
zXr6N~)l@k`mimaxtOiI}^#dMM|Jai-}L^=PpGl&{ULLQ1A%|v};*pJHbScf%|vE*BtNcrevq8mR1bme;FAP
zz6do1r0?tEkz{@U#~~B}hiri(XlTRw?;iE{^FgoRUyWyc&z)Dqr-c6>#~C}q>n!0T
z-^#}SWm>fB?~&e#c?`yW6_op*rzP9og{JI#Q-RpK6ZiKd{*5$z3vjdGj^E3K9n=2L
z)1rjOp%H^$?81JND*ykez&wTXe%mnodksz7BLMO7@fpR%sWx|a*;ZFYHn-K=Zhv`b
z@+0u3D!h7iK_@7v%E>jAS6Ep19WbrDzxtbsQ=~Zd3LPW%ji@O4nc1kdvkK8`z^5j>z22D-Hm;GwWGKWU9`$jw2v};30GHY14BclK7xJ1)c;NF
ze(thx*^W8Vkv)BVn#jFE17l-Jof5jj%c>_|oYAsWJLl%coRrreW8>mzwY7<7imG%D
zC2U^W>R3!cCLe1LHU=5!E8cU-lP@*O@S2)tHdLf0z%*=z8Zbfyulbj5kd2{8ZbQ?)Ch>CgQF7Q=9WM}^#9WzYjRn9oNh_<`klSg2_?si
zcWui)N^^^i?(gMl+X80(P3?v8;b8j;1MQO!|4z_i`UGM-T;1j-mE`>2e{DMIQ{dt(
zh*V`xO~%pD&?rz-XHPfLm1yXapKQ^vxmwAbH)<4xBi)0BxX2b|Q2|il_b{uUocOBy
zZ>|0S3!aS!6crXWF}zE9eR*9H(d+JrE95CtRW%t&!F{9)v7T^sv%36XNsha|aV1oJ
zFV6U~wv?YI!;^tfBSXf@oA}>_^4FrzYQ;rEN9RMt$}gZOVr%YxE<{A={~>LT!pv_)
z)2Rk0+$OiRIRCm{1>4Wt==Tpl3SqkcZEYR^{MOfT$!yq@{IO=iIXaJms%mQQ8XFrC
z1d|p+@M1SMcaPOcQa5>{Bn_Q%7WW-KC=xJ2`s+LCv}68bdRgLl?d6+jz4G>%$he6A%Y!J>AVIpB*uOv!J9~!sWFekU*PEj<
z_1)lR&Trr>O|@K2Yh?Z}zcSxT@yq1OuIZC`d%wiR$8R54X&WHY`bJ@zOwUQz+_FPA
z77f1;EuH0ViNQCqwo)zA|E1;z4nO4<)KJwt{;E45ZCi!%#Z`i?8tu4D_*J@!7Xs)b
z_K()Khnfw%x}67pr3L!eVQ%{*|AzqK)mwC6WIc#)kY>T1Vc(7
zbxatXWit@9tlJCp*O|Vu=dbLG5S;4fIXCmzndCITVe#3oNdE>}bIE2kA)N{TK;>?2
z=6UuZ+m_+4Z0Z{V9P(XAF8(q{YU9ZGmNx(CFUMQ>4dG7QqEwE3;^7?tY4^{v&;av0
zTp;$`2lr|}xE3wU8H^CZ*49b2gni)@hxntE$CSkdc)P&At)u-I4HVoRrUb&6DtkJj
zfvt6VsXC#0>jDPu{ytHyrytZk3{|Cf0llBZ4;0@I4i&mgSxYjPxX@_(1>=d)tBkD5
zTFgZRa};Hc^r#MI;KLmLwrpkkU+Pl!N0PV-2Iqv0fzY_#GstkNfy|~^{q#(d!S=dV
zz>JTF7L#OZA&M1%0lOQx=@Y;8*?(^TIB*<>_lIG!K6;88kn+Xq+ZQB*DL;JB
zO5q77NG(-%Ia#)x4U39Ol#ZmPQMs?fi07=|
zg;SQe^ay_ckWh*L>xWQ#6YFb##iZ;XM=2|m)(r6Y4*c-tS(Kr_IHe#@h=2NJrt;Y7
zdbXDg1F35n#rSZ}lFn5kO~gQj+!He4{+{c+2M_IATLUTlbv#VE-g5lZLz9#GZFsY?
zFcShJu{NxGy6ttK4)dbo5Yq?aP$c?@-UmdG2};-5tS}NVWKgl_)Wn!)cn|&zRWo`h
zYd$HO;dNi^P1zT7^;_ISWOVp4=d_lCDtaRJtYDp&F!tNMu3&9pSsna|)p4R=9gjh7K9@_u
zwysMq?q-02wK#d^13@7GUZ8zQ{$-)KF>Nll^6tRN_9M6*Zl8dVk%&7o);WdjzPqfY
zN($LhDYQ3|L_W(50U(QQ6m>mZzjOQqt3~BLPpE$BGIl
zuxG5IXk}{$S+=dUo)bT5*b+e>?pDEtO5FgYg!JY5!V3PRFsjGfW1l(7hbtkuRZH+vN@D=L8PoNKFIF2{tKXrwXu
zmYS|-eFkwHclv$dpSQbk@{`?~T}~5K6^Yq!^9(#U+|V&F2+$^UdF^8tM4yzIZ2w5h
zwy(gziP`YHDlH@9r#joE6T1;d@J)AN47waNqYyaxY2}kk>$O|+zeAz>4Q>S*;fAGj=@@z{!pvPN$Wjw;6Q(BzMP$O
z6|>u;JIxM-D{?_#-4k==wt2XcY{}~eKR7a?xzBbj*NQYEMElNv%Jc%zxTY3NGdrSFr4{!5vb#+Z_1t=8
zQH_Z6d+s0GWlG{TAA^+x`I5|LyL8fq2gVlh8~jv~6~sgf6X84M90YDm-i*WXI>jFI
z!#BtTxkr&!rGN(72mH(-nl3pt#5dQK`Q1d3<)ge>fzjUs|(r)w3x{%h{I3
zq+nfL1y+FuzEl=rb%Y`iPe5!XQdHb%>O5wbZV(3q^_L?=@O%(q2Cm)Nc{7AhV
zQJr)F=bvY1Xn^+mfz2NIKjCa`POrW0HiC2IJ`+@r@gCQvJ1kl*y1ix`N_>95Hu|K<
zYrkqU*jotqa0V65ZSw(M?3-r}H~^#`tx?q!UFw*1YP4E2&Le^EG{%w`b(8?wgEgF;
z=l91;Lk)>6WRJ!QM_=$BGUXFxv+KsD5R=D_JG{FLde@ChQ_&cv2q`tyjbTm--^MzcTd;%uu*;_Y`eyJ
z-#_v_<^m@wCL{iYeQx?uW5juw&7N9RP=Aw;`R8a(dEyNXYl3Zfev)R*@csRQB|dJx
z60^0r@9WuP)qy0Sd*FJJayLV!CuQ5qLwwGNd4^lrIsgD**-dw?ggl=3j8s*R3Mic_
zbegU-@dPdS!uv<~wtU-izkfUx-R_T#OJ2dP^0+<#--_zfvK)t0-e^^BvJJ4dGK7SL
z{FI9y<#1-xI;*}qiL6SeV`DR@uhb!Aed?LWpCqtdteFac#JU@37v8F2T)%t^=^B@F5XjRncPTye7}GUxQESSni>{vt4VQnGSVjN;_PxvjwkOV^48}zJ0%iW4hdc
zZiQ~yrfD@>(x~UMp#HtIKBcz1fA;?t`8<88k#K}Dyc
zs3gL}6`{d%#`dAG_vnGT4z!*gL(pQ69bNE{xf*ye#3m>13Armw#CslF8i5QWW6(OD
zkaCeZKelz(TdEFB9o+HedTYfGx!rrcR62S##T1N
zjBVBHw&nW+F#+9+806`0D|)6h?l4{~0lj86V?b4j%zjo>K}wG)w!GfO@-qA
zt@%hIHre8#)By2hxxGQ-=?m(x=T&bT^u@z0S>2)C7k>87x3!j*mTk;ucnLAMAh;Ek
zdZSv)&VZ>;C8D4+O1TfeC`F&2s*^>X_LT3}n(aw`FEfVN8o2M%eczBu+Upr)wRQ=0
z4Vj%P@G-P;EI}p{biuVLw{5Pw80H#FS=dq}W~i)GAUm!cyqTn!2xo0l9_8N^T4*tk
z9$2rt)3&W2#8%~SUv5w}xiD3|;=At7hSX+UHUshHE6AfI)jgf|5096&PbGtMlXcY7
zX6j07CK%XH)KFZl&3`x|9R&VX5Tv=_QEe(Yo8ScJEM5{WLUVS
zL*cRjYt^BFv)(#Mr{|h4Lz(IZ9FV~<0$ZHJETHH!`OeQSLT5wK_Pi>;bF6ko2|?2@
zHVOKv+y8`!2VC%{zGyQ-Jj7zi&0)QJOCqsn`Nz4GY`UJ${)|m2X6Wvz7s!ptbhF|p
z93Pjr0;BEv@|T#{D8p!y^C~FoA=GBA23dAYeXXS-4N>^Gld=$JcClRx;WOrRiuZ!v
zj}n3VakkeNH-I%?LtA^fbNjg}`v|y|Z-0J%!WchWxw)`nTophwF}g9@&7VHtK51U8
zvo)`4G#eSj-{dkIO}UP|+MCc_thKfWYHS7Jh%SUo`lDb0FyZWx16C`Il@b^-K(bX;
zM+iRkcEUVe57_53)=o@8*kn`;nL;{1(8hZ}@+B=N=NIY7x7LI>Z`kLlLX4%?TAvdV
z>Y241)ZWw|bO?Ubv0mx$4;EZB4sF!bcTNv$>mmb-g=DaAVA3E#FNTD;C^oqcd`sUa
zA|n}IkxY1<=4CP@yGRU6Q~K?#dO(#}lu}d1?xtEilxVVG7Pxe=WFJ1_30Cx!`NkI1
z&G0ez)o%@tb7Bb*5!C&47YnZLtM{s96Mu?x(EV|nWui!ptH$Hw&i)|e2qEzMu(>i{
zL{2D@V_}lMxt!10EMGHb@_e{T5c9fsJ$e`HY*R?B>xXo;XGDl<`Bmp(&LGt?WODYY
zhE0ShQ(7fztWZ>GHazJ7pCekB^(Do^EKsS<8^4gP&E=T(zHzsvdSIv$D0X`y?b~}(
z+7y0e`5J8;2oH}hWc3qU>wpGXAW>a1GRHjpEv17(bq*B5qwJMX96;7*2P$5-e1~8Z6i6iHVEN^*|4d=$~2|50ZL%X&Fafxre?N
z~btXW3iPs
z+2-TZcrnPtt-ImV_p`J=!jR3QX!-lcNrIV5m3wC9oBXx>8xpod91z8IX?=D$(B`Gu
z)qX#J0S;GZZZG;sl}V$&V-sW6utnKR`PL}L<#W5^y}7WfGrsk_>tjEGd5hkJjbs+m
za(Q!-kfc$bDAaOilY6*aK!oLg<+)eBq&VZaCDZi6AP_^hY1zWwQMJUK<56lhK*KtB
zxkKA>X7PI$N~7bg{WV*t^wBtU_>x|MbGvn1Fg;bgG7^%4TlyPpWD~X|?2v*~K6~y>E+oxm)DD8hS0Q7Mx2FUq
z=rq+RmKlz$>#S*$o#cr*rE8Z(90RjYcsTQDvR
zv)nTFqhJJBX@%aLidN{sFS?mVMAq6Y*dxTcoC39CGj?z@lbE$Fs(w;sNNTnIi5pT^
zU7)WZtwhLXyGn(j-Z$cJOn>j|`8>0H-%YQ7XE
zjv_JdUNiKH6Y#jQ7?|~F$}XA2Wi%=6BUh&+-Q|)Tfz-{4^|#35r4
z4%R{&MvqQbTvsfs*i(6|%|y0n>nx|t+%8Azqt4BP$T~#JS~R4Nu=APScU$i^<_Hq&bawgp`Ho_eQhb2zv2an9w>=_2Mpe6q1x>L_}^<&54H2=NYfM~Sv
z&96#?4lBpqC&Pe~ScXg!tNq`?iL=I5N<#PcUMcOXj^t%z8hmS>@lL0+$G^h4tS`re
zF4G{FH_dSwv1LRIHyH8xDf^yX*Qz6}JhqD;v90oJ0S)b`-+L0=jq2C*8oaKzcilUt
zCEX9B0Rp~h#yx`xu=O4p%N5=siq)GYe+F(R%*K&I&tj`mdO=0Hm-qYLARP1397Y`f@*1*Wt%Qc!u8pa{dL~vT`2p-5`v2k
z^=6S!c|jl}LBB6{X{Eu*U`>GOKR|6c*cVt2dIaAke<`AWewd__wy_ob^F&a#yG3Le
zu!_x~kU0IEN_%-GQmqKZpinmgWhdABTzgth?5geibkOw5@Ec9x(x-VVWa@rcKZf{{;!mEsy&Hd4++q%5P
zqFdwh7Vs7CYuplf)Do`NN4ub8taBpbj&wm+9zqJ)L_uKtYA3(y!_y@jM-NrDQtC}q
zGO*UFQN;Nn4zEw8yc#YN;S_QiM1M8kMpA*#zZFrfpdE7EuomDD<*PnGWZ?NVpU+N@
zAzI|21}_TcSM$+>ZytrJm7$Qc&M9f-IuROq_p@~)Y2Tpox&)rc?fw|)b@Dg2KeS*j
z03|*lJ=yK40A)A~>rCm0#GbLj^IT==HCn%h3HU6$;^UUY^9!jP)ayNCh1^TjG5+I$
z_02ZpYx9j4)p+k8+#5p%q8|IM(=wT>*Ha#{A7?kE9+a$=Ho3XdUMO>jRSM7qVGTP!CLb48zruU29en!-wNCL9T!c2F^%63}iGir>7)Vq{3mW
z^y3`!t6MvpR{CG`)tAiDO(sI>7=}w6I1hjIa@z%5zsj0
z{cXI`Pmm4r*P9+91GTTu1YQ;7bl+Hm$)%ZM^mPOl3uP}ep%BUfg%2xNDA}kNrF?7S
z1o