diff --git a/plugins/codex-security/references/final-report.md b/plugins/codex-security/references/final-report.md
index 7cc973d3d..3f8298bb4 100644
--- a/plugins/codex-security/references/final-report.md
+++ b/plugins/codex-security/references/final-report.md
@@ -14,7 +14,11 @@ The final readable output is a deterministic projection of `scan-manifest.json`,
When writing `findings.json` alongside this readable output, populate the optional structured details in `finding-detail-fields.md` from the same validated evidence. Do not parse the rendered report back into finding data.
-Use `report.md` as the primary readable entry point. Explain report-relevant artifact paths in the report itself, especially in `Scope`, `Reviewed Surfaces`, and `Open Questions And Follow Up`.
+Use `report.md` as the primary readable entry point. Explain report-relevant artifact paths in the report itself, especially in `Scope`, `Reviewed Surfaces`, `Deferred Requested Work`, and `Open Questions And Follow Up`.
+
+Keep unfinished requested work and essential in-scope proof gaps in `coverage.deferred`.
+The report presents these separately from optional `coverage.openQuestions` follow-up.
+Neither section changes the scan execution outcome.
In the final response, link the generated markdown report path as the primary readable artifact.
diff --git a/plugins/codex-security/scripts/report_projection.py b/plugins/codex-security/scripts/report_projection.py
index 364628997..3efa11da5 100644
--- a/plugins/codex-security/scripts/report_projection.py
+++ b/plugins/codex-security/scripts/report_projection.py
@@ -987,10 +987,10 @@ def build_report_markdown(
+ " |"
)
open_questions = coverage.get("openQuestions", [])
- questions = list(open_questions) if isinstance(open_questions, list) else []
+ deferred_questions = []
deferred = coverage.get("deferred", [])
if isinstance(deferred, list):
- questions.extend(
+ deferred_questions = [
{
"question": item.get("reason", "Deferred review requires follow-up."),
"followUpPrompt": " ".join(
@@ -1007,9 +1007,14 @@ def build_report_markdown(
}
for item in deferred
if isinstance(item, dict)
- )
- if questions:
- lines.extend(["", "## Open Questions And Follow Up", ""])
+ ]
+ for heading, questions in (
+ ("Deferred Requested Work", deferred_questions),
+ ("Open Questions And Follow Up", open_questions),
+ ):
+ if not isinstance(questions, list) or not questions:
+ continue
+ lines.extend(["", f"## {heading}", ""])
for question in questions:
if not isinstance(question, dict):
continue
diff --git a/plugins/codex-security/scripts/workbench_db.py b/plugins/codex-security/scripts/workbench_db.py
index 52b9bdfc3..ab522465f 100644
--- a/plugins/codex-security/scripts/workbench_db.py
+++ b/plugins/codex-security/scripts/workbench_db.py
@@ -1962,6 +1962,12 @@ def parse_scan_recipe(value: str, repository: Path) -> dict[str, Any]:
return recipe
+def coverage_summary_for_history(scan: sqlite3.Row) -> dict[str, Any]:
+ return scan_history.coverage_summary_for_history(
+ _WORKBENCH_DB_CONTEXT, scan, require_canonical_scan_directory
+ )
+
+
def coverage_for_comparison(scan: sqlite3.Row) -> dict[str, Any]:
return saved_results.coverage_for_comparison(_WORKBENCH_DB_CONTEXT, scan)
@@ -2729,6 +2735,17 @@ def scan_context(
return context
+def get_scan(connection: sqlite3.Connection, args: argparse.Namespace) -> dict[str, Any]:
+ result = scan_context(connection, args.scan_id, args.occurrence_id)
+ if result["scan"]["progress"]["status"] != "running":
+ scan = require_scan(connection, args.scan_id)
+ try:
+ result["scan"] = dict(result["scan"], coverage=coverage_summary_for_history(scan))
+ except (OSError, RuntimeError, SystemExit):
+ pass # Historical artifacts may no longer be available or verifiable.
+ return result
+
+
def list_findings(connection: sqlite3.Connection, args: argparse.Namespace) -> dict[str, Any]:
scan = require_scan(connection, args.scan_id)
backfill_legacy_finding_details(connection, scan)
@@ -3476,7 +3493,7 @@ def main(*, before_archive: Callable[[], None] | None = None) -> None:
elif args.command == "start-headless-standard-scan":
result = _start_prompt_driven_scan(connection, args, headless_standard=True)
elif args.command == "get-scan":
- result = scan_context(connection, args.scan_id, args.occurrence_id)
+ result = get_scan(connection, args)
elif args.command == "rename-scan":
result = scan_history.rename_scan(
connection, require_scan(connection, args.scan_id), args.name
diff --git a/plugins/codex-security/scripts/workbench_scan_history.py b/plugins/codex-security/scripts/workbench_scan_history.py
index 33cccd12c..d27c8079d 100644
--- a/plugins/codex-security/scripts/workbench_scan_history.py
+++ b/plugins/codex-security/scripts/workbench_scan_history.py
@@ -2,6 +2,7 @@
import argparse
import fnmatch
+import hashlib
import json
import os
import sqlite3
@@ -14,7 +15,12 @@
# Some plugin hosts launch Python with safe-path isolation enabled.
sys.path.insert(0, str(Path(__file__).resolve().parent))
-from finalize_scan_contract import ContractError, _prepare_scan_finalization
+from finalize_scan_contract import (
+ ContractError,
+ _prepare_scan_finalization,
+ _read_scan_local_json_bytes,
+ _read_sealed_scan,
+)
from report_projection import SEVERITY_ORDER
from workbench_constants import ARTIFACTS, FINDINGS_PAGE_MAX
from workbench_scan_start import scan_target_identity
@@ -138,6 +144,65 @@ def cli_scan_resume(
return result
+def coverage_summary_for_history(
+ wb: Any, scan: sqlite3.Row, canonical_scan_directory: Callable[[Path], Path]
+) -> dict[str, Any]:
+ scan_dir = canonical_scan_directory(Path(scan["scan_dir"]))
+ coverage_ref = ARTIFACTS["coverage"]
+ try:
+ if scan["seal_manifest_digest"] is None:
+ # Older workbench rows can have valid seals without a pinned digest.
+ manifest, _, coverage, _ = _read_sealed_scan(scan_dir, None, "history coverage")
+ wb.verify_manifest_binding(scan, manifest)
+ if coverage["mode"] != wb.expected_coverage_mode(scan):
+ raise ContractError("coverage.mode: must match the workbench scan")
+ else:
+ # Completion already validated the full scan. History only needs the
+ # pinned manifest and its coverage artifact, not findings or receipts.
+ manifest, manifest_bytes = _read_scan_local_json_bytes(
+ scan_dir, ARTIFACTS["manifest"], ARTIFACTS["manifest"]
+ )
+ if (
+ f"sha256:{hashlib.sha256(manifest_bytes).hexdigest()}"
+ != scan["seal_manifest_digest"]
+ ):
+ raise ContractError("The sealed scan manifest changed after completion.")
+ sealed_scan = manifest["scan"]
+ if (
+ sealed_scan["id"] != scan["id"]
+ or not sealed_scan.get("sealedAt")
+ or sealed_scan["coverageRef"] != coverage_ref
+ ):
+ raise ContractError("The sealed coverage does not belong to this scan.")
+ coverage, coverage_bytes = _read_scan_local_json_bytes(
+ scan_dir, coverage_ref, coverage_ref
+ )
+ expected = next(
+ (
+ artifact["sha256"]
+ for artifact in sealed_scan["artifacts"]
+ if artifact["path"] == coverage_ref
+ ),
+ None,
+ )
+ if (
+ hashlib.sha256(coverage_bytes).hexdigest() != expected
+ or coverage.get("scanId") != scan["id"]
+ ):
+ raise ContractError("The sealed coverage changed after completion.")
+ except ContractError as exc:
+ raise SystemExit(str(exc)) from exc
+ return {
+ key: coverage[key]
+ for key in (
+ "mode",
+ "includePaths",
+ "excludePaths",
+ "explicitExclusions",
+ )
+ }
+
+
def _windows_path_key(value: str) -> str:
return os.path.normcase(os.path.realpath(value))
diff --git a/plugins/codex-security/tests/test_report_projection.py b/plugins/codex-security/tests/test_report_projection.py
index 7d493e875..fa60043c3 100644
--- a/plugins/codex-security/tests/test_report_projection.py
+++ b/plugins/codex-security/tests/test_report_projection.py
@@ -842,7 +842,8 @@ def test_projection_preserves_follow_up_without_classifying_its_reason(reason: s
markdown = PROJECTION.build_report_markdown(manifest, findings, coverage)
assert "No findings were reported." in markdown
- assert "## Open Questions And Follow Up" in markdown
+ assert "## Deferred Requested Work" in markdown
+ assert "## Open Questions And Follow Up" not in markdown
assert reason in markdown
assert "Review deferred unit candidate-parser" in markdown
assert "Paths: src/parser.py." in markdown
@@ -857,7 +858,7 @@ def test_finding_summary_link_uses_stable_markdown_anchor() -> None:
assert '' in markdown
-def test_projection_keeps_deferred_follow_up_with_open_questions() -> None:
+def test_projection_separates_deferred_work_from_open_questions() -> None:
manifest, findings, coverage = canonical_documents()
coverage["openQuestions"] = [{"question": "Is production authentication enabled?"}]
@@ -877,6 +878,12 @@ def test_projection_keeps_deferred_follow_up_with_open_questions() -> None:
assert "Review deferred unit parser-review" in markdown
assert "Paths: src/parser.py." in markdown
assert "Surfaces: parser-surface." in markdown
+ deferred_section, optional_section = markdown.split("## Open Questions And Follow Up")
+ assert "## Deferred Requested Work" in deferred_section
+ assert "Parser review incomplete." in deferred_section
+ assert "Is production authentication enabled?" not in deferred_section
+ assert "Is production authentication enabled?" in optional_section
+ assert "Parser review incomplete." not in optional_section
def test_projection_includes_surface_evidence_receipts() -> None:
diff --git a/plugins/codex-security/tests/test_workbench_scan_history.py b/plugins/codex-security/tests/test_workbench_scan_history.py
index ec9b67a9f..f571cf65b 100644
--- a/plugins/codex-security/tests/test_workbench_scan_history.py
+++ b/plugins/codex-security/tests/test_workbench_scan_history.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import copy
+import hashlib
import json
import shutil
import sqlite3
@@ -275,6 +276,120 @@ def create_cli_scan(
return launched
+@pytest.mark.parametrize("artifact", ["findings.json", "coverage.json", "scan-manifest.json"])
+@pytest.mark.parametrize("mutation", ["remove", "modify"])
+def test_get_scan_reads_pinned_coverage_independently_of_unrelated_artifacts(
+ tmp_path: Path, artifact: str, mutation: str
+) -> None:
+ state_dir = tmp_path / "state"
+ repository = tmp_path / "repository"
+ repository.mkdir()
+ scan = create_cli_scan(state_dir, tmp_path / "results", repository, finding=True)
+ scan_dir = Path(scan["scanDir"])
+ before = get_scan(state_dir, scan["scanId"])["scan"]
+ assert before["coverage"]["includePaths"] == ["."]
+ artifact_path = scan_dir / artifact
+ if mutation == "remove":
+ artifact_path.unlink()
+ else:
+ artifact_path.write_bytes(artifact_path.read_bytes() + b"\n")
+
+ after = get_scan(state_dir, scan["scanId"])["scan"]
+ assert after["progress"]["status"] == "complete"
+ assert after["findings"] == before["findings"]
+ if artifact == "findings.json":
+ assert after["coverage"] == before["coverage"]
+ else:
+ assert "coverage" not in after
+
+
+@pytest.mark.parametrize("finding", [False, True])
+@pytest.mark.parametrize("follow_up", [False, True])
+def test_get_scan_reads_legacy_sealed_history_coverage_without_writes(
+ tmp_path: Path, finding: bool, follow_up: bool
+) -> None:
+ state_dir = tmp_path / "state"
+ repository = tmp_path / "repository"
+ repository.mkdir()
+ scan = create_cli_scan(
+ state_dir, tmp_path / "results", repository, finding=finding, follow_up=follow_up
+ )
+ scan_dir = Path(scan["scanDir"])
+ artifacts = {path: path.read_bytes() for path in scan_dir.rglob("*") if path.is_file()}
+ with sqlite3.connect(state_dir / "workbench.sqlite3") as connection:
+ assert connection.execute(
+ "SELECT COUNT(*) FROM finding_occurrences WHERE scan_id = ? AND details_json = '{}'",
+ (scan["scanId"],),
+ ).fetchone() == (0,)
+ connection.execute(
+ "UPDATE scans SET seal_manifest_digest = NULL WHERE id = ?", (scan["scanId"],)
+ )
+
+ assert get_scan(state_dir, scan["scanId"])["scan"]["coverage"] == {
+ "mode": "repository",
+ "includePaths": ["."],
+ "excludePaths": [],
+ "explicitExclusions": [],
+ }
+ assert {path: path.read_bytes() for path in scan_dir.rglob("*") if path.is_file()} == artifacts
+ with sqlite3.connect(state_dir / "workbench.sqlite3") as connection:
+ assert connection.execute(
+ "SELECT seal_manifest_digest FROM scans WHERE id = ?", (scan["scanId"],)
+ ).fetchone() == (None,)
+
+
+@pytest.mark.parametrize(
+ "corruption",
+ ["coverage", "findings", "missing-coverage", "missing-manifest", "unsealed", "owner", "mode"],
+)
+def test_get_scan_rejects_unverifiable_legacy_sealed_history_coverage(
+ tmp_path: Path, corruption: str
+) -> None:
+ state_dir = tmp_path / "state"
+ repository = tmp_path / "repository"
+ repository.mkdir()
+ scan = create_cli_scan(state_dir, tmp_path / "results", repository, finding=False)
+ scan_dir = Path(scan["scanDir"])
+ with sqlite3.connect(state_dir / "workbench.sqlite3") as connection:
+ connection.execute(
+ "UPDATE scans SET seal_manifest_digest = NULL WHERE id = ?", (scan["scanId"],)
+ )
+ manifest_path = scan_dir / "scan-manifest.json"
+ manifest = json.loads(manifest_path.read_text())
+ if corruption in {"coverage", "findings"}:
+ path = scan_dir / f"{corruption}.json"
+ path.write_bytes(path.read_bytes() + b"\n")
+ elif corruption.startswith("missing-"):
+ (
+ scan_dir
+ / ("coverage.json" if corruption == "missing-coverage" else "scan-manifest.json")
+ ).unlink()
+ elif corruption == "unsealed":
+ del manifest["scan"]["sealedAt"]
+ del manifest["scan"]["artifacts"]
+ manifest_path.write_text(json.dumps(manifest))
+ elif corruption == "owner":
+ manifest["scan"]["target"]["targetId"] = "unrelated-target"
+ manifest_path.write_text(json.dumps(manifest))
+ else:
+ coverage_path = scan_dir / "coverage.json"
+ coverage = json.loads(coverage_path.read_text())
+ coverage["mode"] = "deep_repository"
+ coverage_path.write_text(json.dumps(coverage))
+ for artifact in manifest["scan"]["artifacts"]:
+ if artifact["path"] == "coverage.json":
+ artifact["sha256"] = hashlib.sha256(coverage_path.read_bytes()).hexdigest()
+ manifest_path.write_text(json.dumps(manifest))
+ artifacts = {path: path.read_bytes() for path in scan_dir.rglob("*") if path.is_file()}
+
+ assert "coverage" not in get_scan(state_dir, scan["scanId"])["scan"]
+ assert {path: path.read_bytes() for path in scan_dir.rglob("*") if path.is_file()} == artifacts
+ with sqlite3.connect(state_dir / "workbench.sqlite3") as connection:
+ assert connection.execute(
+ "SELECT seal_manifest_digest FROM scans WHERE id = ?", (scan["scanId"],)
+ ).fetchone() == (None,)
+
+
def test_cli_scan_lifecycle_persists_recipes_lineage_and_filtered_history(tmp_path: Path) -> None:
state_dir = tmp_path / "state"
repository = tmp_path / "repository"
diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md
index 606c65d47..b0782d5d5 100644
--- a/sdk/typescript/README.md
+++ b/sdk/typescript/README.md
@@ -190,6 +190,10 @@ directory must be private to you (`chmod 700`).
| `threatModel` / `threatModelPath` | Saved threat model and document path; either can be `null`. |
| `cost` | Estimated model usage cost, or `null` when unavailable. |
+Review the requested paths and exclusions alongside the execution outcome.
+`coverage.deferred` records unfinished requested work, while
+`coverage.openQuestions` records optional follow-up separately.
+
Review coverage alongside findings. A report with no findings does not establish
that the repository is secure. Reports and logs can contain source code,
vulnerability details, and credentials; review them before sharing.
diff --git a/sdk/typescript/docs/cli.md b/sdk/typescript/docs/cli.md
index b92fdff53..e7341b7be 100644
--- a/sdk/typescript/docs/cli.md
+++ b/sdk/typescript/docs/cli.md
@@ -344,6 +344,11 @@ codex-security scan . --working-tree
codex-security scan . --output-dir /path/outside/repository/results --dry-run
```
+`--path` selects files or directories. The scan summary and saved scan details
+show requested paths and exclusions. `coverage.deferred` records unfinished
+requested work separately from optional `coverage.openQuestions` follow-up.
+Human-readable output quotes ambiguous paths, while JSON retains their original strings.
+
`--diff` scans committed changes; `--working-tree` scans staged and unstaged
changes. Deep scans support whole repositories and path scopes. Working-tree
snapshots include untracked nested Git repositories. Initialized submodules must
diff --git a/sdk/typescript/scripts/package-dist-files.mjs b/sdk/typescript/scripts/package-dist-files.mjs
index f4f5c4432..f013bd029 100644
--- a/sdk/typescript/scripts/package-dist-files.mjs
+++ b/sdk/typescript/scripts/package-dist-files.mjs
@@ -25,6 +25,7 @@ const distModules = [
"contract",
"cost",
"cost-model",
+ "coverage-presentation",
"custom-validation",
"custom-validation-prompt",
"custom-publish",
diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts
index 31b820dd5..eb6b5d035 100644
--- a/sdk/typescript/src/cli.ts
+++ b/sdk/typescript/src/cli.ts
@@ -49,6 +49,7 @@ import { promisify, stripVTControlCharacters } from "node:util";
import { Cli, z } from "incur";
import Ajv2020, { type ValidateFunction } from "ajv/dist/2020.js";
import { formatCliHelp } from "./cli-help.js";
+import { formatCoverageScope } from "./coverage-presentation.js";
import { scanLogsJson } from "./cli-scan-logs-json.js";
import {
ARTIFACT_EXPORT_FILENAMES,
@@ -9511,6 +9512,7 @@ function printScanSummary(
? ""
: ` ${paint("THREAT MODEL", "1;36")} ${paint(errorMessage(threatModelPath), 4)}\n`) +
`\n ${paint("FINDINGS", 1)} ${paint(`${findingCount}${findingSummary === "" ? "" : ` (${findingSummary})`}`, findingColor)}\n` +
+ ` ${paint("SCOPE", 1)} ${formatCoverageScope(result.coverage)}\n` +
(deepScanStop === undefined
? ""
: ` ${paint("STOPPED", 1)} ${deepScanStop.reason}\n`) +
diff --git a/sdk/typescript/src/coverage-presentation.ts b/sdk/typescript/src/coverage-presentation.ts
new file mode 100644
index 000000000..a1f6c3dbb
--- /dev/null
+++ b/sdk/typescript/src/coverage-presentation.ts
@@ -0,0 +1,52 @@
+import type { CoverageDocument } from "./models.js";
+
+export type CoverageSummary = Pick<
+ CoverageDocument,
+ "mode" | "includePaths" | "excludePaths"
+> &
+ Partial>;
+
+export function formatScopePath(path: string): string {
+ if (path.length > 0 && !/[\s,;'"\\\u0000-\u001f\u007f-\u009f]/u.test(path)) {
+ return path;
+ }
+ return JSON.stringify(path).replace(
+ /[\u007f-\u009f]/gu,
+ (character) =>
+ `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`,
+ );
+}
+
+function scopePathParts(paths: readonly string[], finalSuffix = ""): string[] {
+ return paths.map(
+ (path, index) =>
+ `${formatScopePath(path)}${index < paths.length - 1 ? "," : finalSuffix}`,
+ );
+}
+
+export function formatCoverageScopeParts(coverage: CoverageSummary): string[] {
+ const mode =
+ coverage.mode === "scoped_path"
+ ? "scoped paths"
+ : coverage.mode.replaceAll("_", " ");
+ const exclusions = [
+ ...new Set([
+ ...coverage.excludePaths,
+ ...(coverage.explicitExclusions ?? []).map(({ pattern }) => pattern),
+ ]),
+ ];
+ const suffix = exclusions.length > 0 ? ";" : "";
+ return [
+ `${mode}:`,
+ ...(coverage.includePaths.length > 0
+ ? scopePathParts(coverage.includePaths, suffix)
+ : [`(no included paths)${suffix}`]),
+ ...(exclusions.length > 0
+ ? ["excluding", ...scopePathParts(exclusions)]
+ : []),
+ ];
+}
+
+export function formatCoverageScope(coverage: CoverageSummary): string {
+ return formatCoverageScopeParts(coverage).join(" ");
+}
diff --git a/sdk/typescript/src/scan-history-renderer.ts b/sdk/typescript/src/scan-history-renderer.ts
index 1e76fdf30..2f731c7f8 100644
--- a/sdk/typescript/src/scan-history-renderer.ts
+++ b/sdk/typescript/src/scan-history-renderer.ts
@@ -1,6 +1,10 @@
import { basename, relative } from "node:path";
import stringWidth from "string-width";
import type { JsonObject } from "./config.js";
+import {
+ formatCoverageScopeParts,
+ type CoverageSummary,
+} from "./coverage-presentation.js";
export type HistoryCommand =
"list" | "show" | "findings" | "compare" | "match-all";
@@ -81,11 +85,17 @@ export function renderScanHistory(
` ${accent("━".repeat(width - 4))}`,
];
- const wrap = (value: string, indent: number, prefix?: string): void => {
+ const wrap = (
+ value: string | readonly string[],
+ indent: number,
+ prefix?: string,
+ ): void => {
const available = width - indent - 2;
let line = "";
let lineWidth = 0;
- for (const word of clean(value).split(/\s+/)) {
+ const words =
+ typeof value === "string" ? clean(value).split(/\s+/) : value.map(clean);
+ for (const word of words) {
if (line.length > 0 && lineWidth + stringWidth(word) + 1 > available) {
lines.push(`${prefix || " ".repeat(indent)}${line}`);
prefix = undefined;
@@ -266,6 +276,14 @@ export function renderScanHistory(
} else if (typeof result["updatedAt"] === "string") {
lines.push(` ${strong("UPDATED")} ${clean(result["updatedAt"])}`);
}
+ const canonicalCoverage = result["coverage"] as CoverageSummary | undefined;
+ if (canonicalCoverage) {
+ wrap(
+ formatCoverageScopeParts(canonicalCoverage),
+ 11,
+ ` ${strong("SCOPE")} `,
+ );
+ }
if (result["failureMessage"]) {
wrap(String(result["failureMessage"]), 11, ` ${paint("ERROR", 31)} `);
}
@@ -305,7 +323,9 @@ export function renderScanHistory(
const recipe = result["recipe"] as JsonObject | undefined;
const target = recipe?.["target"] as JsonObject | undefined;
if (target) {
- lines.push(` ${strong("SCOPE")} ${clean(JSON.stringify(target))}`);
+ lines.push(
+ ` ${strong("SAVED SCOPE")} ${clean(JSON.stringify(target))}`,
+ );
}
const deepScan = recipe?.["deepScan"] as JsonObject | undefined;
if (deepScan) {
diff --git a/sdk/typescript/tests-ts/cli.test.ts b/sdk/typescript/tests-ts/cli.test.ts
index c6bbd8317..53c5ec110 100644
--- a/sdk/typescript/tests-ts/cli.test.ts
+++ b/sdk/typescript/tests-ts/cli.test.ts
@@ -4192,6 +4192,7 @@ describe("CLI", () => {
` REPORT ${result.reportPath}`,
"",
" FINDINGS 1 (1 high)",
+ " SCOPE repository: .",
" ELAPSED 6m 37s",
" TOKENS unavailable uncached input, 200 cache reads, unavailable cache writes, 30 output, 1,280 total",
" RESULTS /tmp/scan",
diff --git a/sdk/typescript/tests-ts/coverage-presentation.test.ts b/sdk/typescript/tests-ts/coverage-presentation.test.ts
new file mode 100644
index 000000000..1ebe96d3b
--- /dev/null
+++ b/sdk/typescript/tests-ts/coverage-presentation.test.ts
@@ -0,0 +1,49 @@
+import { describe, expect, test } from "bun:test";
+import {
+ formatCoverageScope,
+ formatScopePath,
+} from "../src/coverage-presentation.js";
+
+describe("coverage scope presentation", () => {
+ test("distinguishes included paths from exclusions and preserves Unicode", () => {
+ expect(
+ formatCoverageScope({
+ mode: "scoped_path",
+ includePaths: ["src; excluding tests", "src/cafe\u0301.ts"],
+ excludePaths: ["generated/**"],
+ explicitExclusions: [
+ { pattern: "generated/**", reason: "Generated source" },
+ { pattern: "fixtures", reason: "Fixtures" },
+ ],
+ }),
+ ).toBe(
+ 'scoped paths: "src; excluding tests", src/cafe\u0301.ts; excluding generated/**, fixtures',
+ );
+ });
+
+ test("quotes whitespace and escapes terminal controls without changing path data", () => {
+ for (const path of [
+ "src/a b.ts",
+ "src/trailing ",
+ 'src/"quoted"',
+ "src/line\nfeed",
+ "src/\u001b[31m",
+ "src/\u009b31m",
+ ]) {
+ const encoded = formatScopePath(path);
+ expect(encoded).not.toMatch(/[\u0000-\u001f\u007f-\u009f]/u);
+ expect(JSON.parse(encoded)).toBe(path);
+ }
+ expect(formatScopePath("src/parser.ts")).toBe("src/parser.ts");
+ });
+
+ test("shows an empty included scope without implying whole-repository coverage", () => {
+ expect(
+ formatCoverageScope({
+ mode: "diff",
+ includePaths: [],
+ excludePaths: [],
+ }),
+ ).toBe("diff: (no included paths)");
+ });
+});
diff --git a/sdk/typescript/tests-ts/scan-history-renderer.test.ts b/sdk/typescript/tests-ts/scan-history-renderer.test.ts
index 0460fa977..1779fa34d 100644
--- a/sdk/typescript/tests-ts/scan-history-renderer.test.ts
+++ b/sdk/typescript/tests-ts/scan-history-renderer.test.ts
@@ -3,6 +3,37 @@ import { describe, expect, test } from "bun:test";
import { renderScanHistory } from "../src/scan-history-renderer.js";
describe("scan history renderer", () => {
+ test("shows canonical scope separately from the saved recipe without changing execution status", () => {
+ const scan = {
+ targetPath: "/synthetic/repository",
+ scanId: "synthetic-scan",
+ mode: "standard",
+ progress: { status: "complete" },
+ findings: [],
+ recipe: { target: { kind: "paths", paths: ["src"] } },
+ };
+ const text = renderScanHistory(
+ {
+ ...scan,
+ coverage: {
+ mode: "scoped_path",
+ includePaths: ["src/a b.ts", "src/trailing "],
+ excludePaths: ["generated/**"],
+ explicitExclusions: [{ pattern: "fixtures", reason: "Fixtures" }],
+ },
+ },
+ "show",
+ { color: false, columns: 120 },
+ );
+ expect(text).toContain("COMPLETE");
+ expect(text).toContain('scoped paths: "src/a b.ts", "src/trailing ";');
+ expect(text).toContain("excluding generated/**, fixtures");
+ expect(text).toContain("SAVED SCOPE");
+ const historical = renderScanHistory(scan, "show", { color: false });
+ expect(historical).toContain("COMPLETE");
+ expect(historical).not.toContain("scoped paths:");
+ });
+
test("counts singleton sides when the other side is grouped", () => {
const text = renderScanHistory(
{