From 07300b90bfdb543216863eb26a83247e578e7096 Mon Sep 17 00:00:00 2001 From: Tim Stranske Date: Sun, 26 Apr 2026 10:35:58 -0500 Subject: [PATCH 1/2] Harden weekly metrics telemetry contracts --- .../terminal-disposition-coverage.test.js | 29 +++++++++++++ .../weekly-metrics-artifacts.test.js | 41 +++++++++++++++++++ .../scripts/terminal_disposition_coverage.js | 19 ++++++++- .github/scripts/weekly_metrics_artifacts.js | 38 +++++++++++++++++ scripts/aggregate_agent_metrics.py | 27 ++++++++++-- .../scripts/terminal_disposition_coverage.js | 19 ++++++++- .../scripts/weekly_metrics_artifacts.js | 38 +++++++++++++++++ .../scripts/aggregate_agent_metrics.py | 27 ++++++++++-- tests/scripts/test_aggregate_agent_metrics.py | 15 +++++++ 9 files changed, 245 insertions(+), 8 deletions(-) diff --git a/.github/scripts/__tests__/terminal-disposition-coverage.test.js b/.github/scripts/__tests__/terminal-disposition-coverage.test.js index 9e48e7354..d12a40d82 100644 --- a/.github/scripts/__tests__/terminal-disposition-coverage.test.js +++ b/.github/scripts/__tests__/terminal-disposition-coverage.test.js @@ -686,6 +686,35 @@ test('normalizes artifact selection reports without selected family counts', () ); }); +test('normalizes legacy artifact selection latest candidate maps', () => { + const summary = normalizeArtifactSelectionSummary({ + status: 'pass', + candidate_family_counts: { + 'review-thread-terminal-disposition': 2, + }, + selected_family_counts: {}, + latest_candidate_by_family: { + 'review-thread-terminal-disposition': { + id: 44, + name: 'review-thread-terminal-disposition-latest', + created_at: '2026-04-25T12:00:00Z', + }, + }, + selected_artifacts: [], + }); + + const reviewThreadStatus = summary.terminal_priority_family_statuses.find( + (status) => status.family === 'review-thread-terminal-disposition' + ); + assert.equal(reviewThreadStatus.status, 'available'); + assert.equal(reviewThreadStatus.selected_artifact, null); + assert.deepEqual(reviewThreadStatus.latest_candidate, { + id: 44, + name: 'review-thread-terminal-disposition-latest', + created_at: '2026-04-25T12:00:00Z', + }); +}); + test('collects only terminal disposition ndjson files from metrics artifacts', () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'terminal-coverage-')); const terminalDir = path.join(dir, 'review-thread-terminal-disposition-123', 'agent-metrics'); diff --git a/.github/scripts/__tests__/weekly-metrics-artifacts.test.js b/.github/scripts/__tests__/weekly-metrics-artifacts.test.js index f033c3e2a..054d7528e 100644 --- a/.github/scripts/__tests__/weekly-metrics-artifacts.test.js +++ b/.github/scripts/__tests__/weekly-metrics-artifacts.test.js @@ -8,6 +8,7 @@ const { collectRepoArtifacts, formatArtifactTsv, formatSelectionMarkdown, + latestCandidateByFamily, missingPriorityFamilies, normalizeSelectionOptions, priorityFamilyStatuses, @@ -102,6 +103,23 @@ test('selects only recent matching artifacts with a machine-readable report', () 'verifier-terminal-disposition', 'bot-comment-auth-coverage-reusable', ]); + assert.deepEqual( + report.latest_candidate_by_family, + { + 'bot-comment-auth-coverage-wrapper': { + id: 8, + name: 'bot-comment-auth-coverage-wrapper-77-2', + created_at: '2026-04-25T08:30:00Z', + updated_at: '2026-04-25T08:30:00Z', + }, + 'review-thread-terminal-disposition': { + id: 5, + name: 'review-thread-terminal-disposition-77', + created_at: '2026-04-25T11:00:00Z', + updated_at: '2026-04-25T11:00:00Z', + }, + } + ); assert.deepEqual( report.priority_family_statuses.map((family) => ({ family: family.family, @@ -250,6 +268,29 @@ test('reports priority telemetry families that are absent from the scan', () => ]); }); +test('maps latest candidate artifacts by priority family', () => { + const candidates = [ + artifact(1, 'review-thread-terminal-disposition-older', '2026-04-25T10:00:00Z'), + artifact(2, 'review-thread-terminal-disposition-newer', '2026-04-25T11:00:00Z'), + artifact(3, 'keepalive-metrics', '2026-04-25T12:00:00Z'), + ].map((raw) => ({ + id: raw.id, + name: raw.name, + family: artifactFamily(raw.name), + created_at: raw.created_at, + updated_at: raw.updated_at, + })); + + assert.deepEqual(latestCandidateByFamily(candidates), { + 'review-thread-terminal-disposition': { + id: 2, + name: 'review-thread-terminal-disposition-newer', + created_at: '2026-04-25T11:00:00Z', + updated_at: '2026-04-25T11:00:00Z', + }, + }); +}); + test('builds priority family statuses for available but unselected artifacts', () => { const candidates = [ artifact(1, 'bot-comment-auth-coverage-wrapper-1', '2026-04-25T11:00:00Z'), diff --git a/.github/scripts/terminal_disposition_coverage.js b/.github/scripts/terminal_disposition_coverage.js index 7706dd5cd..d2a384eb6 100644 --- a/.github/scripts/terminal_disposition_coverage.js +++ b/.github/scripts/terminal_disposition_coverage.js @@ -440,6 +440,21 @@ function normalizeSelectionArtifact(artifact = {}) { return normalized; } +function latestCandidateFromSelectionReport(report = {}, family = '') { + const candidateMaps = [ + report.latest_candidate_by_family, + report.latest_candidates_by_family, + ]; + for (const candidatesByFamily of candidateMaps) { + if (!candidatesByFamily || typeof candidatesByFamily !== 'object' || Array.isArray(candidatesByFamily)) { + continue; + } + const candidate = normalizeSelectionArtifact(candidatesByFamily[family]); + if (candidate) return candidate; + } + return null; +} + function normalizeTerminalPriorityFamilyStatuses(report = {}, selectedArtifacts = []) { const rawStatuses = Array.isArray(report.priority_family_statuses) ? report.priority_family_statuses @@ -466,12 +481,14 @@ function normalizeTerminalPriorityFamilyStatuses(report = {}, selectedArtifacts const selectedCount = Number(report.selected_family_counts?.[family]) || selectedArtifacts.filter((artifact) => artifact.family === family).length; const selectedArtifact = selectedArtifacts.find((artifact) => artifact.family === family) || null; + const latestCandidate = latestCandidateFromSelectionReport(report, family) || + (selectedArtifact ? normalizeSelectionArtifact(selectedArtifact) : null); byFamily.set(family, { family, status: selectedCount > 0 ? 'selected' : (candidateCount > 0 ? 'available' : 'missing'), candidate_count: candidateCount, selected_count: selectedCount, - latest_candidate: selectedArtifact ? normalizeSelectionArtifact(selectedArtifact) : null, + latest_candidate: latestCandidate, selected_artifact: selectedArtifact ? normalizeSelectionArtifact(selectedArtifact) : null, }); } diff --git a/.github/scripts/weekly_metrics_artifacts.js b/.github/scripts/weekly_metrics_artifacts.js index 27a808b0b..616991ca7 100644 --- a/.github/scripts/weekly_metrics_artifacts.js +++ b/.github/scripts/weekly_metrics_artifacts.js @@ -166,6 +166,41 @@ function priorityFamilyStatuses({ }); } +function latestCandidateByFamily(candidates = []) { + const latestByFamily = new Map(); + for (const candidate of candidates) { + const family = cleanString(candidate.family); + if (!PRIORITY_METRICS_FAMILIES.includes(family)) continue; + const existing = latestByFamily.get(family); + const candidateTimestamp = Number(candidate.timestamp_ms) || artifactTimestampMs(candidate); + const existingTimestamp = existing ? Number(existing.timestamp_ms) || artifactTimestampMs(existing) : -1; + const candidateId = Number(candidate.id) || 0; + const existingId = existing ? Number(existing.id) || 0 : -1; + if ( + !existing || + candidateTimestamp > existingTimestamp || + (candidateTimestamp === existingTimestamp && candidateId > existingId) + ) { + latestByFamily.set(family, candidate); + } + } + const entries = []; + for (const family of PRIORITY_METRICS_FAMILIES) { + const latestCandidate = latestByFamily.get(family); + if (!latestCandidate) continue; + entries.push([ + family, + { + id: latestCandidate.id, + name: latestCandidate.name, + created_at: latestCandidate.created_at, + updated_at: latestCandidate.updated_at, + }, + ]); + } + return Object.fromEntries(entries); +} + function selectMetricsArtifacts(artifacts = [], options = {}) { const config = normalizeSelectionOptions(options); const stats = { @@ -261,6 +296,7 @@ function selectMetricsArtifacts(artifacts = [], options = {}) { ...stats, candidate_family_counts: sortedCountObject(candidateFamilyCounts), selected_family_counts: sortedCountObject(familyCounts), + latest_candidate_by_family: latestCandidateByFamily(candidates), missing_priority_families: missingPriorityFamilies(candidateFamilyCounts), priority_family_statuses: priorityFamilyStatuses({ candidates, @@ -302,6 +338,7 @@ function buildSelectionErrorReport(options = {}, error = {}) { ignored_total_limit_count: 0, candidate_family_counts: {}, selected_family_counts: {}, + latest_candidate_by_family: {}, missing_priority_families: [...PRIORITY_METRICS_FAMILIES], priority_family_statuses: priorityFamilyStatuses(), selected_artifacts: [], @@ -489,6 +526,7 @@ module.exports = { collectRepoArtifacts, formatArtifactTsv, formatSelectionMarkdown, + latestCandidateByFamily, missingPriorityFamilies, normalizeSelectionOptions, priorityFamilyStatuses, diff --git a/scripts/aggregate_agent_metrics.py b/scripts/aggregate_agent_metrics.py index fad40bb09..958330da3 100755 --- a/scripts/aggregate_agent_metrics.py +++ b/scripts/aggregate_agent_metrics.py @@ -44,6 +44,8 @@ ), ) _MAX_PARSE_ERROR_ROWS = 25 +_MAX_LEGACY_JSON_FALLBACK_LINES = 5000 +_MAX_LEGACY_JSON_FALLBACK_BYTES = 1024 * 1024 @dataclass(frozen=True) @@ -180,13 +182,25 @@ def _read_ndjson(files: Iterable[Path]) -> tuple[list[dict[str, Any]], list[Pars file_entries: list[dict[str, Any]] = [] file_errors: list[ParseErrorDetail] = [] raw_lines_for_fallback: list[str] = [] + raw_fallback_bytes = 0 + raw_fallback_truncated = False with handle: for line_number, line in enumerate(handle, start=1): raw = line.strip() if not raw: continue - if not file_entries: - raw_lines_for_fallback.append(raw) + if not file_entries and not raw_fallback_truncated: + raw_bytes = len(raw.encode("utf-8")) + 1 + fallback_within_limit = ( + len(raw_lines_for_fallback) < _MAX_LEGACY_JSON_FALLBACK_LINES + and raw_fallback_bytes + raw_bytes <= _MAX_LEGACY_JSON_FALLBACK_BYTES + ) + if fallback_within_limit: + raw_fallback_bytes += raw_bytes + raw_lines_for_fallback.append(raw) + else: + raw_fallback_truncated = True + raw_lines_for_fallback = [] try: parsed = json.loads(raw) except json.JSONDecodeError: @@ -197,7 +211,14 @@ def _read_ndjson(files: Iterable[Path]) -> tuple[list[dict[str, Any]], list[Pars raw_lines_for_fallback = [] else: file_errors.append(_parse_error_detail(path, line_number, "non-object-json")) - if file_errors and not file_entries and raw_lines_for_fallback: + if file_errors and not file_entries and raw_fallback_truncated: + file_errors.append(_parse_error_detail(path, None, "legacy-json-fallback-buffer-limit")) + if ( + file_errors + and not file_entries + and raw_lines_for_fallback + and not raw_fallback_truncated + ): try: parsed_file = json.loads("\n".join(raw_lines_for_fallback)) except json.JSONDecodeError: diff --git a/templates/consumer-repo/.github/scripts/terminal_disposition_coverage.js b/templates/consumer-repo/.github/scripts/terminal_disposition_coverage.js index 7706dd5cd..d2a384eb6 100644 --- a/templates/consumer-repo/.github/scripts/terminal_disposition_coverage.js +++ b/templates/consumer-repo/.github/scripts/terminal_disposition_coverage.js @@ -440,6 +440,21 @@ function normalizeSelectionArtifact(artifact = {}) { return normalized; } +function latestCandidateFromSelectionReport(report = {}, family = '') { + const candidateMaps = [ + report.latest_candidate_by_family, + report.latest_candidates_by_family, + ]; + for (const candidatesByFamily of candidateMaps) { + if (!candidatesByFamily || typeof candidatesByFamily !== 'object' || Array.isArray(candidatesByFamily)) { + continue; + } + const candidate = normalizeSelectionArtifact(candidatesByFamily[family]); + if (candidate) return candidate; + } + return null; +} + function normalizeTerminalPriorityFamilyStatuses(report = {}, selectedArtifacts = []) { const rawStatuses = Array.isArray(report.priority_family_statuses) ? report.priority_family_statuses @@ -466,12 +481,14 @@ function normalizeTerminalPriorityFamilyStatuses(report = {}, selectedArtifacts const selectedCount = Number(report.selected_family_counts?.[family]) || selectedArtifacts.filter((artifact) => artifact.family === family).length; const selectedArtifact = selectedArtifacts.find((artifact) => artifact.family === family) || null; + const latestCandidate = latestCandidateFromSelectionReport(report, family) || + (selectedArtifact ? normalizeSelectionArtifact(selectedArtifact) : null); byFamily.set(family, { family, status: selectedCount > 0 ? 'selected' : (candidateCount > 0 ? 'available' : 'missing'), candidate_count: candidateCount, selected_count: selectedCount, - latest_candidate: selectedArtifact ? normalizeSelectionArtifact(selectedArtifact) : null, + latest_candidate: latestCandidate, selected_artifact: selectedArtifact ? normalizeSelectionArtifact(selectedArtifact) : null, }); } diff --git a/templates/consumer-repo/.github/scripts/weekly_metrics_artifacts.js b/templates/consumer-repo/.github/scripts/weekly_metrics_artifacts.js index 27a808b0b..616991ca7 100644 --- a/templates/consumer-repo/.github/scripts/weekly_metrics_artifacts.js +++ b/templates/consumer-repo/.github/scripts/weekly_metrics_artifacts.js @@ -166,6 +166,41 @@ function priorityFamilyStatuses({ }); } +function latestCandidateByFamily(candidates = []) { + const latestByFamily = new Map(); + for (const candidate of candidates) { + const family = cleanString(candidate.family); + if (!PRIORITY_METRICS_FAMILIES.includes(family)) continue; + const existing = latestByFamily.get(family); + const candidateTimestamp = Number(candidate.timestamp_ms) || artifactTimestampMs(candidate); + const existingTimestamp = existing ? Number(existing.timestamp_ms) || artifactTimestampMs(existing) : -1; + const candidateId = Number(candidate.id) || 0; + const existingId = existing ? Number(existing.id) || 0 : -1; + if ( + !existing || + candidateTimestamp > existingTimestamp || + (candidateTimestamp === existingTimestamp && candidateId > existingId) + ) { + latestByFamily.set(family, candidate); + } + } + const entries = []; + for (const family of PRIORITY_METRICS_FAMILIES) { + const latestCandidate = latestByFamily.get(family); + if (!latestCandidate) continue; + entries.push([ + family, + { + id: latestCandidate.id, + name: latestCandidate.name, + created_at: latestCandidate.created_at, + updated_at: latestCandidate.updated_at, + }, + ]); + } + return Object.fromEntries(entries); +} + function selectMetricsArtifacts(artifacts = [], options = {}) { const config = normalizeSelectionOptions(options); const stats = { @@ -261,6 +296,7 @@ function selectMetricsArtifacts(artifacts = [], options = {}) { ...stats, candidate_family_counts: sortedCountObject(candidateFamilyCounts), selected_family_counts: sortedCountObject(familyCounts), + latest_candidate_by_family: latestCandidateByFamily(candidates), missing_priority_families: missingPriorityFamilies(candidateFamilyCounts), priority_family_statuses: priorityFamilyStatuses({ candidates, @@ -302,6 +338,7 @@ function buildSelectionErrorReport(options = {}, error = {}) { ignored_total_limit_count: 0, candidate_family_counts: {}, selected_family_counts: {}, + latest_candidate_by_family: {}, missing_priority_families: [...PRIORITY_METRICS_FAMILIES], priority_family_statuses: priorityFamilyStatuses(), selected_artifacts: [], @@ -489,6 +526,7 @@ module.exports = { collectRepoArtifacts, formatArtifactTsv, formatSelectionMarkdown, + latestCandidateByFamily, missingPriorityFamilies, normalizeSelectionOptions, priorityFamilyStatuses, diff --git a/templates/consumer-repo/scripts/aggregate_agent_metrics.py b/templates/consumer-repo/scripts/aggregate_agent_metrics.py index fad40bb09..958330da3 100755 --- a/templates/consumer-repo/scripts/aggregate_agent_metrics.py +++ b/templates/consumer-repo/scripts/aggregate_agent_metrics.py @@ -44,6 +44,8 @@ ), ) _MAX_PARSE_ERROR_ROWS = 25 +_MAX_LEGACY_JSON_FALLBACK_LINES = 5000 +_MAX_LEGACY_JSON_FALLBACK_BYTES = 1024 * 1024 @dataclass(frozen=True) @@ -180,13 +182,25 @@ def _read_ndjson(files: Iterable[Path]) -> tuple[list[dict[str, Any]], list[Pars file_entries: list[dict[str, Any]] = [] file_errors: list[ParseErrorDetail] = [] raw_lines_for_fallback: list[str] = [] + raw_fallback_bytes = 0 + raw_fallback_truncated = False with handle: for line_number, line in enumerate(handle, start=1): raw = line.strip() if not raw: continue - if not file_entries: - raw_lines_for_fallback.append(raw) + if not file_entries and not raw_fallback_truncated: + raw_bytes = len(raw.encode("utf-8")) + 1 + fallback_within_limit = ( + len(raw_lines_for_fallback) < _MAX_LEGACY_JSON_FALLBACK_LINES + and raw_fallback_bytes + raw_bytes <= _MAX_LEGACY_JSON_FALLBACK_BYTES + ) + if fallback_within_limit: + raw_fallback_bytes += raw_bytes + raw_lines_for_fallback.append(raw) + else: + raw_fallback_truncated = True + raw_lines_for_fallback = [] try: parsed = json.loads(raw) except json.JSONDecodeError: @@ -197,7 +211,14 @@ def _read_ndjson(files: Iterable[Path]) -> tuple[list[dict[str, Any]], list[Pars raw_lines_for_fallback = [] else: file_errors.append(_parse_error_detail(path, line_number, "non-object-json")) - if file_errors and not file_entries and raw_lines_for_fallback: + if file_errors and not file_entries and raw_fallback_truncated: + file_errors.append(_parse_error_detail(path, None, "legacy-json-fallback-buffer-limit")) + if ( + file_errors + and not file_entries + and raw_lines_for_fallback + and not raw_fallback_truncated + ): try: parsed_file = json.loads("\n".join(raw_lines_for_fallback)) except json.JSONDecodeError: diff --git a/tests/scripts/test_aggregate_agent_metrics.py b/tests/scripts/test_aggregate_agent_metrics.py index ad833f7dd..57d2b9ce4 100644 --- a/tests/scripts/test_aggregate_agent_metrics.py +++ b/tests/scripts/test_aggregate_agent_metrics.py @@ -432,6 +432,21 @@ def test_read_ndjson_accepts_legacy_pretty_json_object(tmp_path: Path) -> None: assert entries[0]["artifact_name"] == "keepalive-metrics" +def test_read_ndjson_bounds_legacy_json_fallback_buffer(tmp_path: Path) -> None: + path = tmp_path / "large-invalid.ndjson" + path.write_text( + "\n".join(["{"] * (aggregate_agent_metrics._MAX_LEGACY_JSON_FALLBACK_LINES + 1)) + "\n", + encoding="utf-8", + ) + + entries, errors = aggregate_agent_metrics._read_ndjson([path]) + + assert entries == [] + assert len(errors) == aggregate_agent_metrics._MAX_LEGACY_JSON_FALLBACK_LINES + 2 + assert errors[-1].line is None + assert errors[-1].reason == "legacy-json-fallback-buffer-limit" + + def test_classify_entry_prefers_explicit_type() -> None: assert aggregate_agent_metrics._classify_entry({"metric_type": "Keepalive"}) == "keepalive" assert aggregate_agent_metrics._classify_entry({"workflow": "autofix"}) == "autofix" From a4d2a8dce0b17eb935c06ed76384e7648a930a9f Mon Sep 17 00:00:00 2001 From: Tim Stranske Date: Sun, 26 Apr 2026 10:40:16 -0500 Subject: [PATCH 2/2] Format weekly review evaluator --- scripts/repo_review_evaluator.py | 49 +++++++-------------- tests/scripts/test_repo_review_evaluator.py | 4 +- 2 files changed, 19 insertions(+), 34 deletions(-) diff --git a/scripts/repo_review_evaluator.py b/scripts/repo_review_evaluator.py index 3544792aa..e166ee100 100644 --- a/scripts/repo_review_evaluator.py +++ b/scripts/repo_review_evaluator.py @@ -444,11 +444,7 @@ def gap_label(severity: str) -> str: def build_review_execution(state: dict[str, Any]) -> dict[str, Any]: repo_path = Path(state["local_path"]) tracked_files = tracked_repo_files(repo_path) - implementation_files = [ - path - for path in tracked_files - if is_implementation_file(path) - ] + implementation_files = [path for path in tracked_files if is_implementation_file(path)] test_files = [path for path in tracked_files if is_test_file(path)] implementation_scan_files = implementation_files[:REVIEW_SCAN_FILE_LIMIT] test_scan_files = test_files[:REVIEW_SCAN_FILE_LIMIT] @@ -499,8 +495,7 @@ def build_review_execution(state: dict[str, Any]) -> dict[str, Any]: :12 ] design_headings = { - rel_path: markdown_headings(repo_path, rel_path) - for rel_path in state["design_files"][:6] + rel_path: markdown_headings(repo_path, rel_path) for rel_path in state["design_files"][:6] } domain_hits = keyword_file_hits( repo_path, @@ -521,12 +516,12 @@ def build_review_execution(state: dict[str, Any]) -> dict[str, Any]: if not state["design_files"]: design_severity = "blocks testing" - design_finding = "No tracked design sources were found; the design contract is not reviewable." - else: - design_severity = "none" design_finding = ( - f"Collected {state['design_source_count']} design sources and registry anchor for comparison." + "No tracked design sources were found; the design contract is not reviewable." ) + else: + design_severity = "none" + design_finding = f"Collected {state['design_source_count']} design sources and registry anchor for comparison." dimensions.append( { "id": "design_contract", @@ -580,14 +575,10 @@ def build_review_execution(state: dict[str, Any]) -> dict[str, Any]: test_finding = "No tests or CI workflow files were detected." elif not smoke_test_files: test_severity = "material" - test_finding = ( - "Tests or workflows exist, but the automated pass did not find smoke/e2e/live readiness markers." - ) + test_finding = "Tests or workflows exist, but the automated pass did not find smoke/e2e/live readiness markers." else: test_severity = "needs human decision" - test_finding = ( - "Test and smoke/integration markers exist; review must verify they prove the intended user journey." - ) + test_finding = "Test and smoke/integration markers exist; review must verify they prove the intended user journey." dimensions.append( { "id": "test_and_live_readiness", @@ -628,14 +619,10 @@ def build_review_execution(state: dict[str, Any]) -> dict[str, Any]: if state["issue_draft_count"] or state["archive_candidate_count"]: issue_severity = "needs human decision" - issue_finding = ( - "Draft inputs exist; approve only after checking them against the executed review evidence." - ) + issue_finding = "Draft inputs exist; approve only after checking them against the executed review evidence." elif state["remote_open_issue_count"]: issue_severity = "needs human decision" - issue_finding = ( - "No local/archive candidates are queued, but remote open issues need reconciliation before drafting more." - ) + issue_finding = "No local/archive candidates are queued, but remote open issues need reconciliation before drafting more." else: issue_severity = "needs human decision" issue_finding = ( @@ -1367,9 +1354,7 @@ def write_repo_artifacts(output_dir: Path, state: dict[str, Any], max_drafts: in "", ] ) - (repo_dir / "review-execution.md").write_text( - "\n".join(execution_lines), encoding="utf-8" - ) + (repo_dir / "review-execution.md").write_text("\n".join(execution_lines), encoding="utf-8") draft_lines = [ f"# Issue Drafts: {state['repo']}", @@ -1432,9 +1417,7 @@ def write_packet(output_dir: Path, states: list[dict[str, Any]], generated_on: s ignored = [state for state in states if state["status"] == "ignored"] review_pending = [state for state in active if state["review_status"] == PENDING_REVIEW_STATUS] blocked = [ - state - for state in active - if str(state["review_execution"]["status"]).startswith("blocked") + state for state in active if str(state["review_execution"]["status"]).startswith("blocked") ] issue_candidate_repos = [ state for state in active if state["issue_queue_status"] == "draft candidates present" @@ -1442,9 +1425,7 @@ def write_packet(output_dir: Path, states: list[dict[str, Any]], generated_on: s executed_reviews = [ state for state in active if state["review_execution"]["status"] == "executed" ] - automated_gap_repos = [ - state for state in active if state["review_execution"]["gap_count"] > 0 - ] + automated_gap_repos = [state for state in active if state["review_execution"]["gap_count"] > 0] lines = [ f"# Weekly Design Review Decision Packet - {generated_on}", @@ -1481,7 +1462,9 @@ def write_packet(output_dir: Path, states: list[dict[str, Any]], generated_on: s "approve/edit/defer issue drafts." ) elif str(state["review_execution"]["status"]).startswith("blocked"): - human_action = "resolve the blocker, rerun review execution, then queue the human decision." + human_action = ( + "resolve the blocker, rerun review execution, then queue the human decision." + ) else: human_action = "conduct the standardized review, then approve/edit/defer issue drafts." lines.extend( diff --git a/tests/scripts/test_repo_review_evaluator.py b/tests/scripts/test_repo_review_evaluator.py index 10bac8af6..a378c886f 100644 --- a/tests/scripts/test_repo_review_evaluator.py +++ b/tests/scripts/test_repo_review_evaluator.py @@ -261,7 +261,9 @@ def test_gitnexus_ignore_change_is_helper_input_not_review_blocker(tmp_path: Pat (repo_dir / ".gitignore").write_text("node_modules/\n", encoding="utf-8") subprocess = evaluator.subprocess subprocess.run(["git", "-C", str(repo_dir), "init"], check=True, capture_output=True) - subprocess.run(["git", "-C", str(repo_dir), "config", "user.email", "test@example.com"], check=True) + subprocess.run( + ["git", "-C", str(repo_dir), "config", "user.email", "test@example.com"], check=True + ) subprocess.run(["git", "-C", str(repo_dir), "config", "user.name", "Test User"], check=True) subprocess.run(["git", "-C", str(repo_dir), "add", "README.md", ".gitignore"], check=True) subprocess.run(