claude-dev-env 4.3.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/pr-description-writer.md +2 -2
- package/.agents/agents/test_agent_frontmatter.py +2 -2
- package/.agents/agents/test_pr_description_writer_style.py +1 -3
- package/.agents/skills/e-code-review/SKILL.md +12 -12
- package/.agents/skills/e-code-review/preflight-proposal.contract.test.mjs +20 -0
- package/.agents/skills/e-code-review/reference/fix.md +15 -17
- package/.agents/skills/e-code-review/reference/loop.md +35 -39
- package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +2 -1
- package/.agents/skills/e-code-review/reference/preflight-proposal.md +44 -9
- package/.agents/skills/e-code-review/reference/runner-selection.md +13 -14
- package/.agents/skills/issue-tracker/SKILL.md +5 -4
- package/.agents/skills/pull-request/SKILL.md +11 -13
- package/.agents/skills/pull-request/reference/publication-tasks.md +3 -3
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +9 -1
- package/.agents/skills/pull-request/scripts/pull_request.py +33 -44
- package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +9 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -3
- package/.agents/skills/skill-builder/SKILL.md +3 -196
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +6 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +64 -19
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +59 -0
- package/bin/install.agents-home.test.mjs +15 -13
- package/bin/install.mjs +28 -0
- package/bin/install.policy-lint.test.mjs +78 -0
- package/bin/install.prune.test.mjs +59 -20
- package/bin/install.pstack-plugin.test.mjs +114 -0
- package/bin/install.test.mjs +37 -0
- package/bin/refresh-pstack-plugin-skills.test.mjs +211 -0
- package/hooks/advisory/pr_done_reminder.py +342 -0
- package/hooks/advisory/test_pr_done_reminder.py +249 -0
- package/hooks/blocking/bash_post_call_dispatcher.py +53 -6
- package/hooks/blocking/code_rules_paired_test.py +22 -15
- package/hooks/blocking/test_bash_post_call_dispatcher.py +65 -4
- package/hooks/blocking/test_code_rules_enforcer_paired_test.py +36 -29
- package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +4 -4
- package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +5 -5
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +68 -133
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +20 -51
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +18 -18
- package/hooks/git-hooks/git_hooks_constants/staged_policy_lint.py +12 -0
- package/hooks/git-hooks/pre_commit.py +52 -11
- package/hooks/git-hooks/test_installed_policy_lint.py +159 -0
- package/hooks/git-hooks/test_native_policy_commit.py +87 -0
- package/hooks/git-hooks/test_pre_commit.py +246 -0
- package/hooks/hooks.json +1 -1
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +14 -1
- package/hooks/hooks_constants/pr_done_reminder_constants.py +98 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -43
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +19 -2
- package/hooks/hooks_constants/test_pr_done_reminder_constants.py +40 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +20 -32
- package/package.json +2 -2
- package/rules/durable-post-artifacts.md +2 -1
- package/rules/git-workflow.md +6 -6
- package/scripts/check.ps1 +0 -1
- package/{.agents/skills/run-claude-dev-env/driver.mjs → scripts/ci/installer-lifecycle.mjs} +3 -28
- package/scripts/ci/installer-lifecycle.test.mjs +26 -0
- package/scripts/ci/windows-installer-lifecycle.ps1 +1 -11
- package/scripts/dev_env_scripts_constants/shared_tree_constants.py +4 -5
- package/{.agents/skills/_shared/pr-loop/scripts → scripts}/durable_post_lint.py +5 -41
- package/{.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/durable_post_lint_constants.py → scripts/durable_post_lint_config/config/constants.py} +0 -10
- package/{.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint_constants.py → scripts/durable_post_lint_config/config/test_constants.py} +1 -1
- package/scripts/policy_lint/adapter_detectors.py +13 -6
- package/scripts/policy_lint/adapters.py +7 -4
- package/scripts/policy_lint/config/constants.py +5 -0
- package/scripts/policy_lint/engine.py +48 -4
- package/scripts/policy_lint/model.py +1 -0
- package/scripts/policy_lint/registry.py +1 -1
- package/scripts/policy_lint/selection.py +2 -1
- package/scripts/policy_lint/selection_git.py +2 -0
- package/scripts/refresh_pstack_plugin_skills.mjs +200 -0
- package/scripts/shared_tree_paths.py +32 -3
- package/scripts/test_shared_tree_paths.py +33 -0
- package/scripts/tests/test_policy_lint_rules_archive.py +120 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +25 -4
- package/scripts/tests/test_policy_lint_rules_installed_loader.py +29 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +227 -13
- package/scripts/tests/test_policy_lint_selection.py +25 -2
- package/scripts/tests/test_policy_lint_selection_native_index.py +30 -0
- package/scripts/tests/test_rules.py +3 -7
- package/.agents/skills/_shared/pr-loop/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/audit-contract.md +0 -5
- package/.agents/skills/_shared/pr-loop/audit-reply-template.md +0 -5
- package/.agents/skills/_shared/pr-loop/code-rules-gate.md +0 -5
- package/.agents/skills/_shared/pr-loop/fix-protocol.md +0 -5
- package/.agents/skills/_shared/pr-loop/gh-payloads.md +0 -5
- package/.agents/skills/_shared/pr-loop/portable-driver.md +0 -149
- package/.agents/skills/_shared/pr-loop/post-audit-thread-contract.md +0 -5
- package/.agents/skills/_shared/pr-loop/precatch-rubric.md +0 -5
- package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +0 -119
- package/.agents/skills/_shared/pr-loop/preflight-proposal.md +0 -5
- package/.agents/skills/_shared/pr-loop/prompts/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +0 -324
- package/.agents/skills/_shared/pr-loop/scripts/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +0 -29
- package/.agents/skills/_shared/pr-loop/scripts/_cli_utils.py +0 -22
- package/.agents/skills/_shared/pr-loop/scripts/_path_resolver.py +0 -186
- package/.agents/skills/_shared/pr-loop/scripts/_xml_utils.py +0 -20
- package/.agents/skills/_shared/pr-loop/scripts/audit_category_schema.py +0 -355
- package/.agents/skills/_shared/pr-loop/scripts/build_audit_prompt.py +0 -289
- package/.agents/skills/_shared/pr-loop/scripts/build_converge_task_list.py +0 -310
- package/.agents/skills/_shared/pr-loop/scripts/build_fix_prompt.py +0 -292
- package/.agents/skills/_shared/pr-loop/scripts/init_loop_state.py +0 -134
- package/.agents/skills/_shared/pr-loop/scripts/portable_converge_driver.py +0 -1646
- package/.agents/skills/_shared/pr-loop/scripts/preflight_worktree.py +0 -392
- package/.agents/skills/_shared/pr-loop/scripts/select_converge_pacer.py +0 -215
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/__init__.py +0 -0
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +0 -37
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +0 -56
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +0 -45
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +0 -47
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +0 -157
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +0 -177
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +0 -70
- package/.agents/skills/_shared/pr-loop/scripts/teardown_worktrees.py +0 -172
- package/.agents/skills/_shared/pr-loop/scripts/test__path_resolver.py +0 -106
- package/.agents/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +0 -94
- package/.agents/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +0 -499
- package/.agents/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +0 -140
- package/.agents/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +0 -262
- package/.agents/skills/_shared/pr-loop/scripts/test_init_loop_state.py +0 -71
- package/.agents/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +0 -1119
- package/.agents/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +0 -287
- package/.agents/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +0 -207
- package/.agents/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +0 -59
- package/.agents/skills/_shared/pr-loop/scripts/test_write_handoff.py +0 -201
- package/.agents/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +0 -182
- package/.agents/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +0 -206
- package/.agents/skills/_shared/pr-loop/scripts/write_handoff.py +0 -309
- package/.agents/skills/_shared/pr-loop/state-schema.md +0 -5
- package/.agents/skills/_shared/pr-loop/worker-spawn.md +0 -5
- package/.agents/skills/autoconverge/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/SKILL.md +0 -431
- package/.agents/skills/autoconverge/reference/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/reference/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/reference/closing-report.md +0 -93
- package/.agents/skills/autoconverge/reference/convergence-gates.md +0 -31
- package/.agents/skills/autoconverge/reference/convergence.md +0 -219
- package/.agents/skills/autoconverge/reference/copilot-findings.md +0 -51
- package/.agents/skills/autoconverge/reference/gotchas.md +0 -66
- package/.agents/skills/autoconverge/reference/headless-safety.md +0 -44
- package/.agents/skills/autoconverge/reference/multi-pr.md +0 -128
- package/.agents/skills/autoconverge/reference/self-closing-loop.md +0 -84
- package/.agents/skills/autoconverge/reference/stop-conditions.md +0 -127
- package/.agents/skills/autoconverge/test_portable_pacer_gate.py +0 -54
- package/.agents/skills/autoconverge/workflow/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/workflow/aggregate_runs.py +0 -371
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -250
- package/.agents/skills/autoconverge/workflow/converge.clean-audit.test.mjs +0 -665
- package/.agents/skills/autoconverge/workflow/converge.codex-gate.test.mjs +0 -472
- package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +0 -64
- package/.agents/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +0 -1009
- package/.agents/skills/autoconverge/workflow/converge.fix-progress.test.mjs +0 -107
- package/.agents/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +0 -33
- package/.agents/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +0 -15
- package/.agents/skills/autoconverge/workflow/converge.mjs +0 -3132
- package/.agents/skills/autoconverge/workflow/converge.p2-advance.test.mjs +0 -202
- package/.agents/skills/autoconverge/workflow/converge.path-aware.test.mjs +0 -47
- package/.agents/skills/autoconverge/workflow/converge.precatch.test.mjs +0 -152
- package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +0 -116
- package/.agents/skills/autoconverge/workflow/converge_multi.mjs +0 -169
- package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +0 -173
- package/.agents/skills/autoconverge/workflow/convergence_summary.py +0 -110
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +0 -266
- package/.agents/skills/autoconverge/workflow/render_report.py +0 -1024
- package/.agents/skills/autoconverge/workflow/test_aggregate_runs.py +0 -134
- package/.agents/skills/autoconverge/workflow/test_convergence_summary.py +0 -149
- package/.agents/skills/autoconverge/workflow/test_render_report.py +0 -812
- package/.agents/skills/hitl/SKILL.md +0 -45
- package/.agents/skills/pr-cleanup/SKILL.md +0 -140
- package/.agents/skills/pr-cleanup/reference/process-inventory.md +0 -17
- package/.agents/skills/pr-cleanup/reference/task-seeds.md +0 -22
- package/.agents/skills/pr-name-by-capability/SKILL.md +0 -73
- package/.agents/skills/pr-name-by-capability/reference/fetch-commands.md +0 -29
- package/.agents/skills/pr-name-by-capability/reference/offense-examples.md +0 -32
- package/.agents/skills/pr-name-by-capability/reference/report-template.md +0 -29
- package/.agents/skills/pr-name-by-capability/reference/rule-checklist.md +0 -38
- package/.agents/skills/pr-name-by-capability/reference/task-seeds.md +0 -10
- package/.agents/skills/pr-plain-language-cleanup/SKILL.md +0 -93
- package/.agents/skills/pr-refinement/SKILL.md +0 -31
- package/.agents/skills/pr-shared-extraction/SKILL.md +0 -177
- package/.agents/skills/pr-shared-extraction/reference/examples.md +0 -46
- package/.agents/skills/pr-shared-extraction/reference/offense-taxonomy.md +0 -100
- package/.agents/skills/pr-small-cl/SKILL.md +0 -52
- package/.agents/skills/pr-title-description/SKILL.md +0 -96
- package/.agents/skills/prototype/SKILL.md +0 -85
- package/.agents/skills/prototype/reference/honest-limitations.md +0 -23
- package/.agents/skills/prototype/reference/promotion-tasks.md +0 -23
- package/.agents/skills/prototype/scripts/build_sandbox_settings.py +0 -249
- package/.agents/skills/prototype/scripts/conftest.py +0 -15
- package/.agents/skills/prototype/scripts/launch_sandbox.py +0 -205
- package/.agents/skills/prototype/scripts/probe_sandbox_safety.py +0 -311
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/__init__.py +0 -1
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +0 -41
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +0 -23
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +0 -45
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +0 -10
- package/.agents/skills/prototype/scripts/test_build_sandbox_settings.py +0 -275
- package/.agents/skills/prototype/scripts/test_launch_sandbox.py +0 -303
- package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +0 -312
- package/.agents/skills/prototype/workflows/promotion.md +0 -25
- package/.agents/skills/prototype/workflows/sandbox.md +0 -35
- package/.agents/skills/rebase/.claude/CLAUDE.md +0 -1
- package/.agents/skills/rebase/AGENTS.md +0 -1
- package/.agents/skills/rebase/SKILL.md +0 -178
- package/.agents/skills/review-router/SKILL.md +0 -51
- package/.agents/skills/review-router/reference/route-policy.json +0 -7
- package/.agents/skills/review-router/reference/task-seeds.md +0 -1
- package/.agents/skills/review-router/scripts/conftest.py +0 -4
- package/.agents/skills/review-router/scripts/review_router.py +0 -50
- package/.agents/skills/review-router/scripts/review_router_cli.py +0 -211
- package/.agents/skills/review-router/scripts/review_router_constants/__init__.py +0 -1
- package/.agents/skills/review-router/scripts/review_router_constants/config/__init__.py +0 -0
- package/.agents/skills/review-router/scripts/review_router_constants/config/constants.py +0 -13
- package/.agents/skills/review-router/scripts/test_review_router.py +0 -23
- package/.agents/skills/review-router/scripts/test_review_router_cli.py +0 -47
- package/.agents/skills/review-tier/SKILL.md +0 -56
- package/.agents/skills/review-tier/reference/task-seeds.md +0 -1
- package/.agents/skills/review-tier/reference/tier-policy.json +0 -9
- package/.agents/skills/review-tier/scripts/conftest.py +0 -4
- package/.agents/skills/review-tier/scripts/review_tier.py +0 -359
- package/.agents/skills/review-tier/scripts/review_tier_constants/__init__.py +0 -1
- package/.agents/skills/review-tier/scripts/review_tier_constants/config/__init__.py +0 -0
- package/.agents/skills/review-tier/scripts/review_tier_constants/config/constants.py +0 -29
- package/.agents/skills/review-tier/scripts/test_generation.py +0 -159
- package/.agents/skills/review-tier/scripts/test_review_tier.py +0 -33
- package/.agents/skills/run-claude-dev-env/SKILL.md +0 -84
- package/.agents/skills/session-log/.claude/CLAUDE.md +0 -1
- package/.agents/skills/session-log/AGENTS.md +0 -1
- package/.agents/skills/session-log/SKILL.md +0 -170
- package/.agents/skills/session-log/templates/frontmatter.md +0 -40
- package/.agents/skills/session-tidy/.claude/CLAUDE.md +0 -1
- package/.agents/skills/session-tidy/AGENTS.md +0 -1
- package/.agents/skills/session-tidy/SKILL.md +0 -191
- package/.agents/skills/skill-builder/references/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/references/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/references/delegation-map.md +0 -130
- package/.agents/skills/skill-builder/references/description-field.md +0 -111
- package/.agents/skills/skill-builder/references/deterministic-elements.md +0 -218
- package/.agents/skills/skill-builder/references/progressive-disclosure.md +0 -122
- package/.agents/skills/skill-builder/references/self-audit-checklist.md +0 -126
- package/.agents/skills/skill-builder/references/skill-modularity.md +0 -111
- package/.agents/skills/skill-builder/references/skill-types.md +0 -232
- package/.agents/skills/skill-builder/references/thariq-x-post-skills.json +0 -33
- package/.agents/skills/skill-builder/templates/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/templates/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/templates/gap-analysis.md +0 -87
- package/.agents/skills/skill-builder/workflows/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/workflows/improve-skill.md +0 -136
- package/.agents/skills/skill-builder/workflows/new-skill.md +0 -179
- package/.agents/skills/skill-builder/workflows/polish-skill.md +0 -139
- package/.agents/skills/source-command-sr-loop/SKILL.md +0 -82
- package/.agents/skills/update/.claude/CLAUDE.md +0 -1
- package/.agents/skills/update/AGENTS.md +0 -1
- package/.agents/skills/update/SKILL.md +0 -190
- package/_shared/pr-loop/AGENTS.md +0 -1
- package/_shared/pr-loop/CLAUDE.md +0 -1
- package/_shared/pr-loop/audit-contract.md +0 -183
- package/_shared/pr-loop/audit-reply-template.md +0 -151
- package/_shared/pr-loop/code-rules-gate.md +0 -65
- package/_shared/pr-loop/fix-protocol.md +0 -60
- package/_shared/pr-loop/gh-payloads.md +0 -75
- package/_shared/pr-loop/post-audit-thread-contract.md +0 -51
- package/_shared/pr-loop/precatch-rubric.md +0 -66
- package/_shared/pr-loop/preflight-proposal.md +0 -56
- package/_shared/pr-loop/scripts/post_audit_thread.py +0 -1242
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/post_audit_thread_constants.py +0 -199
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +0 -1115
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +0 -129
- package/_shared/pr-loop/state-schema.md +0 -45
- package/_shared/pr-loop/worker-spawn.md +0 -189
- /package/{.agents/skills/_shared/pr-loop/scripts → scripts}/test_durable_post_lint.py +0 -0
|
@@ -1,1024 +0,0 @@
|
|
|
1
|
-
"""Render a visual convergence summary HTML report from an autoconverge journal."""
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
import html
|
|
5
|
-
import json
|
|
6
|
-
import re
|
|
7
|
-
import sys
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import TextIO
|
|
11
|
-
|
|
12
|
-
from autoconverge_report_constants.render_report_constants import (
|
|
13
|
-
CATEGORY_BUG,
|
|
14
|
-
CATEGORY_LABEL_BY_VALUE,
|
|
15
|
-
CATEGORY_SORT_ORDER,
|
|
16
|
-
CAUSE_MUTED_STYLE,
|
|
17
|
-
DEFAULT_FINDING_CATEGORY,
|
|
18
|
-
DEFAULT_FINDING_SEVERITY,
|
|
19
|
-
DEFAULT_ISSUE_ICON,
|
|
20
|
-
HTML_TITLE_AND_STYLE_TEMPLATE,
|
|
21
|
-
HTML_STYLE_BLOCK,
|
|
22
|
-
ISO_DATE_LENGTH,
|
|
23
|
-
ISSUE_CLASS_FIELD_AFTER_LINES,
|
|
24
|
-
ISSUE_CLASS_FIELD_BEFORE_LINES,
|
|
25
|
-
ISSUE_CLASS_FIELD_CATEGORY,
|
|
26
|
-
ISSUE_CLASS_FIELD_CAUSE,
|
|
27
|
-
ISSUE_CLASS_FIELD_COUNT,
|
|
28
|
-
ISSUE_CLASS_FIELD_MEDIUM,
|
|
29
|
-
ISSUE_CLASS_FIELD_PLAINNAME,
|
|
30
|
-
ISSUE_CLASS_FIELD_SEVERITY,
|
|
31
|
-
ISSUE_CLASS_FIELD_STATUS,
|
|
32
|
-
ISSUE_ICON_BY_CATEGORY,
|
|
33
|
-
JOURNAL_SIBLING_SUBAGENTS,
|
|
34
|
-
JOURNAL_SIBLING_WORKFLOWS,
|
|
35
|
-
LABEL_CONVERGENCE_SUMMARY,
|
|
36
|
-
LABEL_COPILOT_GATE,
|
|
37
|
-
LABEL_PREFIX_FIX,
|
|
38
|
-
LABEL_PREFIX_LENS,
|
|
39
|
-
LABEL_RESOLVE_HEAD,
|
|
40
|
-
MEDIUM_CODE,
|
|
41
|
-
MEDIUM_TERMINAL,
|
|
42
|
-
SCENE_FIELD_CAPTION,
|
|
43
|
-
SCENE_FIELD_CONDITION,
|
|
44
|
-
SCENE_FIELD_RESULT,
|
|
45
|
-
SCENE_FIELD_TRIGGER,
|
|
46
|
-
SCORECARD_LABEL_CAUGHT,
|
|
47
|
-
SCORECARD_LABEL_REMAINING,
|
|
48
|
-
SCORECARD_LABEL_ROUNDS,
|
|
49
|
-
SEVERITY_SORT_RANK,
|
|
50
|
-
SHORT_SHA_LENGTH,
|
|
51
|
-
STATUS_LABEL_BY_VALUE,
|
|
52
|
-
STRUCTURED_OUTPUT_TOOL_NAME,
|
|
53
|
-
SUMMARY_FIELD_FIX_SCENES,
|
|
54
|
-
SUMMARY_FIELD_ISSUE_CLASSES,
|
|
55
|
-
SUMMARY_FIELD_PR_FIX,
|
|
56
|
-
SUMMARY_FIELD_PR_PROBLEM,
|
|
57
|
-
SUMMARY_FIELD_PROBLEM_SCENES,
|
|
58
|
-
SUMMARY_FIELD_VERDICT_LINE,
|
|
59
|
-
TIMELINE_AFTER_LABEL,
|
|
60
|
-
TIMELINE_AFTER_PILL,
|
|
61
|
-
TIMELINE_BEFORE_LABEL,
|
|
62
|
-
TIMELINE_BEFORE_PILL,
|
|
63
|
-
TIMELINE_TERMINAL_BAR_LABEL,
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@dataclass(frozen=True)
|
|
68
|
-
class RawFinding:
|
|
69
|
-
"""A single finding from a lens or copilot-gate agent result."""
|
|
70
|
-
|
|
71
|
-
file: str
|
|
72
|
-
line: int
|
|
73
|
-
severity: str
|
|
74
|
-
title: str
|
|
75
|
-
detail: str
|
|
76
|
-
category: str
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@dataclass(frozen=True)
|
|
80
|
-
class FixRecord:
|
|
81
|
-
"""The structured result of a fix agent, with base-sha context attached."""
|
|
82
|
-
|
|
83
|
-
new_sha: str
|
|
84
|
-
pushed: bool
|
|
85
|
-
resolved_without_commit: bool
|
|
86
|
-
summary: str
|
|
87
|
-
base_sha: str
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@dataclass(frozen=True)
|
|
91
|
-
class PrMetadata:
|
|
92
|
-
"""Owner, repo, number, final sha, and round count for the PR being reported on."""
|
|
93
|
-
|
|
94
|
-
owner: str
|
|
95
|
-
repo: str
|
|
96
|
-
number: int
|
|
97
|
-
final_sha: str
|
|
98
|
-
round_count: int
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
@dataclass
|
|
102
|
-
class RunData:
|
|
103
|
-
"""Aggregated metrics parsed from a workflow journal and agent transcripts."""
|
|
104
|
-
|
|
105
|
-
generated_date: str
|
|
106
|
-
total_finding_count: int
|
|
107
|
-
fix_commit_count: int
|
|
108
|
-
all_distinct_findings: list[RawFinding]
|
|
109
|
-
fix_by_round: dict[int, FixRecord]
|
|
110
|
-
convergence_summary: dict | None
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def _resolve_agents_dir(journal_path: Path) -> Path:
|
|
114
|
-
"""Return the directory containing per-agent transcript files for this run.
|
|
115
|
-
|
|
116
|
-
Args:
|
|
117
|
-
journal_path: Absolute path to the wf_<runId>.json journal file.
|
|
118
|
-
|
|
119
|
-
Returns:
|
|
120
|
-
Path to the subagents/workflows/<runId>/ directory.
|
|
121
|
-
"""
|
|
122
|
-
run_id = journal_path.stem
|
|
123
|
-
return (
|
|
124
|
-
journal_path.parent.parent
|
|
125
|
-
/ JOURNAL_SIBLING_SUBAGENTS
|
|
126
|
-
/ JOURNAL_SIBLING_WORKFLOWS
|
|
127
|
-
/ run_id
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def _extract_structured_output(transcript_path: Path) -> dict | None:
|
|
132
|
-
"""Return the last StructuredOutput tool input from an agent transcript.
|
|
133
|
-
|
|
134
|
-
Args:
|
|
135
|
-
transcript_path: Path to an agent-<id>.jsonl file.
|
|
136
|
-
|
|
137
|
-
Returns:
|
|
138
|
-
The input dict of the last StructuredOutput tool_use, or None when absent.
|
|
139
|
-
"""
|
|
140
|
-
last_input: dict | None = None
|
|
141
|
-
try:
|
|
142
|
-
with transcript_path.open(encoding="utf-8") as transcript_file:
|
|
143
|
-
for each_line in transcript_file:
|
|
144
|
-
last_input = _last_structured_input_in_line(each_line, last_input)
|
|
145
|
-
except OSError:
|
|
146
|
-
return None
|
|
147
|
-
|
|
148
|
-
return last_input
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def _last_structured_input_in_line(
|
|
152
|
-
transcript_line: str, prior_input: dict | None
|
|
153
|
-
) -> dict | None:
|
|
154
|
-
"""Return the StructuredOutput input on a transcript line, else the prior input.
|
|
155
|
-
|
|
156
|
-
Args:
|
|
157
|
-
transcript_line: One raw JSON line from an agent transcript.
|
|
158
|
-
prior_input: The last StructuredOutput input seen before this line.
|
|
159
|
-
|
|
160
|
-
Returns:
|
|
161
|
-
The input dict of the last StructuredOutput tool_use on this line, or
|
|
162
|
-
prior_input when the line carries none.
|
|
163
|
-
"""
|
|
164
|
-
try:
|
|
165
|
-
parsed = json.loads(transcript_line)
|
|
166
|
-
except (ValueError, json.JSONDecodeError):
|
|
167
|
-
return prior_input
|
|
168
|
-
|
|
169
|
-
message = parsed.get("message") if isinstance(parsed, dict) else None
|
|
170
|
-
content_list = message.get("content") if isinstance(message, dict) else None
|
|
171
|
-
if not isinstance(content_list, list):
|
|
172
|
-
return prior_input
|
|
173
|
-
|
|
174
|
-
latest_input = prior_input
|
|
175
|
-
for each_block in content_list:
|
|
176
|
-
if not isinstance(each_block, dict):
|
|
177
|
-
continue
|
|
178
|
-
if (
|
|
179
|
-
each_block.get("type") == "tool_use"
|
|
180
|
-
and each_block.get("name") == STRUCTURED_OUTPUT_TOOL_NAME
|
|
181
|
-
and isinstance(each_block.get("input"), dict)
|
|
182
|
-
):
|
|
183
|
-
latest_input = each_block["input"]
|
|
184
|
-
|
|
185
|
-
return latest_input
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def _build_dedup_key(file_path: str, line: int, title: str) -> tuple[str, int, str]:
|
|
189
|
-
"""Return a deduplication key for a finding.
|
|
190
|
-
|
|
191
|
-
Args:
|
|
192
|
-
file_path: The file field from the finding.
|
|
193
|
-
line: The line number from the finding.
|
|
194
|
-
title: The title field from the finding.
|
|
195
|
-
|
|
196
|
-
Returns:
|
|
197
|
-
A tuple of (file, line, lowercased_title).
|
|
198
|
-
"""
|
|
199
|
-
return (file_path, line, title.lower())
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def _parse_finding_from_dict(raw: dict) -> RawFinding:
|
|
203
|
-
"""Construct a RawFinding from a raw agent result dict.
|
|
204
|
-
|
|
205
|
-
Args:
|
|
206
|
-
raw: The raw finding dict from the agent result.
|
|
207
|
-
|
|
208
|
-
Returns:
|
|
209
|
-
A RawFinding dataclass instance.
|
|
210
|
-
"""
|
|
211
|
-
return RawFinding(
|
|
212
|
-
file=raw.get("file", ""),
|
|
213
|
-
line=raw.get("line", 0),
|
|
214
|
-
severity=raw.get("severity", DEFAULT_FINDING_SEVERITY),
|
|
215
|
-
title=raw.get("title", ""),
|
|
216
|
-
detail=raw.get("detail", ""),
|
|
217
|
-
category=raw.get("category", DEFAULT_FINDING_CATEGORY),
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
def _parse_fix_record(agent_result: dict, base_sha: str) -> FixRecord:
|
|
222
|
-
"""Construct a FixRecord from a fix agent's structured output.
|
|
223
|
-
|
|
224
|
-
Args:
|
|
225
|
-
agent_result: The structured output dict from the fix agent.
|
|
226
|
-
base_sha: The HEAD sha the round reviewed before fixing.
|
|
227
|
-
|
|
228
|
-
Returns:
|
|
229
|
-
A FixRecord dataclass instance.
|
|
230
|
-
"""
|
|
231
|
-
return FixRecord(
|
|
232
|
-
new_sha=agent_result.get("newSha", ""),
|
|
233
|
-
pushed=bool(agent_result.get("pushed", False)),
|
|
234
|
-
resolved_without_commit=bool(agent_result.get("resolvedWithoutCommit", False)),
|
|
235
|
-
summary=agent_result.get("summary", ""),
|
|
236
|
-
base_sha=base_sha,
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def _parse_progress_entries(
|
|
241
|
-
progress_entries: list[dict],
|
|
242
|
-
agents_dir: Path,
|
|
243
|
-
) -> tuple[list[RawFinding], dict[int, FixRecord]]:
|
|
244
|
-
"""Walk workflowProgress in order and collect findings and fix results by round.
|
|
245
|
-
|
|
246
|
-
Args:
|
|
247
|
-
progress_entries: The workflowProgress list from the journal.
|
|
248
|
-
agents_dir: Directory containing per-agent .jsonl transcript files.
|
|
249
|
-
|
|
250
|
-
Returns:
|
|
251
|
-
A tuple of (all_raw_findings, fix_by_round).
|
|
252
|
-
"""
|
|
253
|
-
all_findings: list[RawFinding] = []
|
|
254
|
-
fix_by_round: dict[int, FixRecord] = {}
|
|
255
|
-
current_round = 0
|
|
256
|
-
current_round_base_sha = ""
|
|
257
|
-
|
|
258
|
-
for each_entry in progress_entries:
|
|
259
|
-
label: str = each_entry.get("label", "")
|
|
260
|
-
agent_id: str | None = each_entry.get("agentId")
|
|
261
|
-
|
|
262
|
-
if label == LABEL_RESOLVE_HEAD:
|
|
263
|
-
current_round += 1
|
|
264
|
-
current_round_base_sha = ""
|
|
265
|
-
continue
|
|
266
|
-
|
|
267
|
-
if agent_id is None:
|
|
268
|
-
continue
|
|
269
|
-
|
|
270
|
-
transcript_path = agents_dir / f"agent-{agent_id}.jsonl"
|
|
271
|
-
agent_result = _extract_structured_output(transcript_path)
|
|
272
|
-
if agent_result is None:
|
|
273
|
-
continue
|
|
274
|
-
|
|
275
|
-
is_lens = label.startswith(LABEL_PREFIX_LENS)
|
|
276
|
-
is_copilot = label == LABEL_COPILOT_GATE
|
|
277
|
-
is_fix = label.startswith(LABEL_PREFIX_FIX)
|
|
278
|
-
|
|
279
|
-
if is_lens or is_copilot:
|
|
280
|
-
sha = agent_result.get("sha", "")
|
|
281
|
-
if not current_round_base_sha and sha:
|
|
282
|
-
current_round_base_sha = sha
|
|
283
|
-
raw_findings: list[dict] = agent_result.get("findings", [])
|
|
284
|
-
for each_raw in raw_findings:
|
|
285
|
-
all_findings.append(_parse_finding_from_dict(each_raw))
|
|
286
|
-
|
|
287
|
-
if is_fix:
|
|
288
|
-
fix_by_round[current_round] = _parse_fix_record(
|
|
289
|
-
agent_result, current_round_base_sha
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
return all_findings, fix_by_round
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
def _dedup_findings(all_findings: list[RawFinding]) -> list[RawFinding]:
|
|
296
|
-
"""Deduplicate findings globally by (file, line, lower title), keeping earliest round.
|
|
297
|
-
|
|
298
|
-
Args:
|
|
299
|
-
all_findings: All raw findings in discovery order.
|
|
300
|
-
|
|
301
|
-
Returns:
|
|
302
|
-
A list of distinct findings with the earliest occurrence retained.
|
|
303
|
-
"""
|
|
304
|
-
seen_keys: set[tuple[str, int, str]] = set()
|
|
305
|
-
distinct: list[RawFinding] = []
|
|
306
|
-
for each_finding in all_findings:
|
|
307
|
-
dedup_key = _build_dedup_key(
|
|
308
|
-
each_finding.file, each_finding.line, each_finding.title
|
|
309
|
-
)
|
|
310
|
-
if dedup_key not in seen_keys:
|
|
311
|
-
seen_keys.add(dedup_key)
|
|
312
|
-
distinct.append(each_finding)
|
|
313
|
-
return distinct
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
def _extract_convergence_summary(
|
|
317
|
-
progress_entries: list[dict], agents_dir: Path
|
|
318
|
-
) -> dict | None:
|
|
319
|
-
"""Return the convergence-summary StructuredOutput, or None when absent.
|
|
320
|
-
|
|
321
|
-
Args:
|
|
322
|
-
progress_entries: The workflowProgress list from the journal.
|
|
323
|
-
agents_dir: Directory containing per-agent .jsonl transcript files.
|
|
324
|
-
|
|
325
|
-
Returns:
|
|
326
|
-
The summarizer agent's last StructuredOutput input dict, or None when
|
|
327
|
-
no convergence-summary entry exists or its transcript is unreadable.
|
|
328
|
-
"""
|
|
329
|
-
for each_entry in progress_entries:
|
|
330
|
-
if each_entry.get("label") != LABEL_CONVERGENCE_SUMMARY:
|
|
331
|
-
continue
|
|
332
|
-
agent_id = each_entry.get("agentId")
|
|
333
|
-
if not agent_id:
|
|
334
|
-
return None
|
|
335
|
-
transcript_path = agents_dir / f"agent-{agent_id}.jsonl"
|
|
336
|
-
return _extract_structured_output(transcript_path)
|
|
337
|
-
return None
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def load_run_data(journal_path: Path) -> RunData:
|
|
341
|
-
"""Parse a workflow journal and its agent transcripts into aggregated metrics.
|
|
342
|
-
|
|
343
|
-
Args:
|
|
344
|
-
journal_path: Path to the wf_<runId>.json journal file.
|
|
345
|
-
|
|
346
|
-
Returns:
|
|
347
|
-
A RunData instance with finding lists, fix records, and the summary populated.
|
|
348
|
-
"""
|
|
349
|
-
iso_date_length = ISO_DATE_LENGTH
|
|
350
|
-
journal = json.loads(journal_path.read_text(encoding="utf-8"))
|
|
351
|
-
timestamp: str = journal.get("timestamp", "")
|
|
352
|
-
generated_date = (
|
|
353
|
-
timestamp[:iso_date_length] if len(timestamp) >= iso_date_length else ""
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
progress_entries: list[dict] = journal.get("workflowProgress", [])
|
|
357
|
-
agents_dir = _resolve_agents_dir(journal_path)
|
|
358
|
-
|
|
359
|
-
all_raw_findings, fix_by_round = _parse_progress_entries(
|
|
360
|
-
progress_entries, agents_dir
|
|
361
|
-
)
|
|
362
|
-
distinct_findings = _dedup_findings(all_raw_findings)
|
|
363
|
-
convergence_summary = _extract_convergence_summary(progress_entries, agents_dir)
|
|
364
|
-
|
|
365
|
-
fix_commit_count = sum(1 for each_fix in fix_by_round.values() if each_fix.pushed)
|
|
366
|
-
|
|
367
|
-
return RunData(
|
|
368
|
-
generated_date=generated_date,
|
|
369
|
-
total_finding_count=len(distinct_findings),
|
|
370
|
-
fix_commit_count=fix_commit_count,
|
|
371
|
-
all_distinct_findings=distinct_findings,
|
|
372
|
-
fix_by_round=fix_by_round,
|
|
373
|
-
convergence_summary=convergence_summary,
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
def _is_summary_structurally_valid(convergence_summary: object) -> bool:
|
|
378
|
-
"""Return whether the summary carries a verdict string and an issue-class list.
|
|
379
|
-
|
|
380
|
-
Args:
|
|
381
|
-
convergence_summary: The parsed convergence summary, which may be None or
|
|
382
|
-
any non-dict JSON value an agent emits in place of the expected object.
|
|
383
|
-
|
|
384
|
-
Returns:
|
|
385
|
-
True when the summary is a dict whose verdictLine is a str and whose
|
|
386
|
-
issueClasses is a list, else False.
|
|
387
|
-
"""
|
|
388
|
-
if not isinstance(convergence_summary, dict):
|
|
389
|
-
return False
|
|
390
|
-
verdict_line = convergence_summary.get(SUMMARY_FIELD_VERDICT_LINE)
|
|
391
|
-
issue_classes = convergence_summary.get(SUMMARY_FIELD_ISSUE_CLASSES)
|
|
392
|
-
return isinstance(verdict_line, str) and isinstance(issue_classes, list)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
def _coerce_count(raw_count: object) -> int:
|
|
396
|
-
"""Return a non-negative integer count from an LLM-authored field, else zero.
|
|
397
|
-
|
|
398
|
-
Args:
|
|
399
|
-
raw_count: The count value as read from the convergence summary, which an
|
|
400
|
-
agent may emit as null, a non-numeric string, or a valid number.
|
|
401
|
-
|
|
402
|
-
Returns:
|
|
403
|
-
The value parsed to an int, or 0 when it is null, non-numeric, or absent.
|
|
404
|
-
"""
|
|
405
|
-
if isinstance(raw_count, bool):
|
|
406
|
-
return int(raw_count)
|
|
407
|
-
if isinstance(raw_count, int):
|
|
408
|
-
return raw_count
|
|
409
|
-
if isinstance(raw_count, (float, str)):
|
|
410
|
-
try:
|
|
411
|
-
return int(float(raw_count))
|
|
412
|
-
except (TypeError, ValueError):
|
|
413
|
-
return 0
|
|
414
|
-
return 0
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
def _pluralize(count: int, singular: str, plural: str) -> str:
|
|
418
|
-
"""Return a 'count word' phrase choosing the singular or plural noun.
|
|
419
|
-
|
|
420
|
-
Args:
|
|
421
|
-
count: The quantity that decides the noun form.
|
|
422
|
-
singular: The noun to use when count is exactly one.
|
|
423
|
-
plural: The noun to use for every other count.
|
|
424
|
-
|
|
425
|
-
Returns:
|
|
426
|
-
A phrase like '1 fix commit' or '2 fix commits'.
|
|
427
|
-
"""
|
|
428
|
-
noun = singular if count == 1 else plural
|
|
429
|
-
return f"{count} {noun}"
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
def _render_verdict_banner(
|
|
433
|
-
convergence_summary: dict, run_data: RunData, final_sha_short: str
|
|
434
|
-
) -> str:
|
|
435
|
-
"""Return the verdict banner with the verdict line and a Python-computed sub-line.
|
|
436
|
-
|
|
437
|
-
Args:
|
|
438
|
-
convergence_summary: A structurally valid convergence summary.
|
|
439
|
-
run_data: Aggregated metrics from the journal.
|
|
440
|
-
final_sha_short: First eight characters of the final commit sha.
|
|
441
|
-
|
|
442
|
-
Returns:
|
|
443
|
-
An HTML .verdict banner string with a check circle, vtext, and vsub.
|
|
444
|
-
"""
|
|
445
|
-
verdict_line = str(convergence_summary.get(SUMMARY_FIELD_VERDICT_LINE, ""))
|
|
446
|
-
fix_commit_phrase = _pluralize(
|
|
447
|
-
run_data.fix_commit_count, "fix commit", "fix commits"
|
|
448
|
-
)
|
|
449
|
-
vsub = f"{fix_commit_phrase} · final commit {html.escape(final_sha_short)}"
|
|
450
|
-
return (
|
|
451
|
-
'<div class="verdict">'
|
|
452
|
-
'<div class="check">✓</div>'
|
|
453
|
-
"<div>"
|
|
454
|
-
f'<div class="vtext">{html.escape(verdict_line)}</div>'
|
|
455
|
-
f'<div class="vsub">{vsub}</div>'
|
|
456
|
-
"</div>"
|
|
457
|
-
"</div>"
|
|
458
|
-
)
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
def _render_scorecard(run_data: RunData, round_count: int) -> str:
|
|
462
|
-
"""Return the at-a-glance scorecard: findings caught, rounds, and zero remaining.
|
|
463
|
-
|
|
464
|
-
Args:
|
|
465
|
-
run_data: Aggregated metrics from the journal.
|
|
466
|
-
round_count: Total number of convergence rounds.
|
|
467
|
-
|
|
468
|
-
Returns:
|
|
469
|
-
An HTML .scorecard grid of three .stat tiles; the remaining tile is marked
|
|
470
|
-
.good and reads zero, since the report renders only on a converged run.
|
|
471
|
-
"""
|
|
472
|
-
remaining_count = 0
|
|
473
|
-
return (
|
|
474
|
-
'<div class="scorecard">'
|
|
475
|
-
f'<div class="stat"><div class="stat-num">{run_data.total_finding_count}</div>'
|
|
476
|
-
f'<div class="stat-label">{SCORECARD_LABEL_CAUGHT}</div></div>'
|
|
477
|
-
f'<div class="stat"><div class="stat-num">{round_count}</div>'
|
|
478
|
-
f'<div class="stat-label">{SCORECARD_LABEL_ROUNDS}</div></div>'
|
|
479
|
-
f'<div class="stat good"><div class="stat-num">{remaining_count}</div>'
|
|
480
|
-
f'<div class="stat-label">{SCORECARD_LABEL_REMAINING}</div></div>'
|
|
481
|
-
"</div>"
|
|
482
|
-
)
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
def _render_scene_row(scene: dict, is_problem: bool) -> str:
|
|
486
|
-
"""Return one .scene row plus its caption for a problem-or-fix scene.
|
|
487
|
-
|
|
488
|
-
Args:
|
|
489
|
-
scene: A scene dict with trigger, condition, result, and caption.
|
|
490
|
-
is_problem: True for a problem scene (bad result), False for a fix scene.
|
|
491
|
-
|
|
492
|
-
Returns:
|
|
493
|
-
An HTML .scene row followed by a .scene-cap caption line.
|
|
494
|
-
"""
|
|
495
|
-
trigger = html.escape(str(scene.get(SCENE_FIELD_TRIGGER, "")))
|
|
496
|
-
condition = str(scene.get(SCENE_FIELD_CONDITION, "")).strip()
|
|
497
|
-
result_text = html.escape(str(scene.get(SCENE_FIELD_RESULT, "")))
|
|
498
|
-
caption = html.escape(str(scene.get(SCENE_FIELD_CAPTION, "")))
|
|
499
|
-
|
|
500
|
-
result_class = "res-bad" if is_problem else "res-good"
|
|
501
|
-
result_mark = "✗" if is_problem else "✓"
|
|
502
|
-
|
|
503
|
-
parts = [f'<span class="chip">{trigger}</span>']
|
|
504
|
-
if condition:
|
|
505
|
-
parts.append('<span class="arrow">→</span>')
|
|
506
|
-
parts.append(f'<span class="note">{html.escape(condition)}</span>')
|
|
507
|
-
parts.append('<span class="arrow">→</span>')
|
|
508
|
-
parts.append(f'<span class="{result_class}">{result_mark} {result_text}</span>')
|
|
509
|
-
|
|
510
|
-
scene_row = f'<div class="scene">{"".join(parts)}</div>'
|
|
511
|
-
caption_row = f'<div class="scene-cap">{caption}</div>'
|
|
512
|
-
return scene_row + caption_row
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
def _render_pf_card(convergence_summary: dict, is_problem: bool) -> str:
|
|
516
|
-
"""Return a single problem-or-fix card drawing its scenes or a fallback caption.
|
|
517
|
-
|
|
518
|
-
Args:
|
|
519
|
-
convergence_summary: A structurally valid convergence summary.
|
|
520
|
-
is_problem: True for the problem card, False for the fix card.
|
|
521
|
-
|
|
522
|
-
Returns:
|
|
523
|
-
An HTML .pf card string with scene rows or a single fallback caption.
|
|
524
|
-
"""
|
|
525
|
-
if is_problem:
|
|
526
|
-
card_class = "problem"
|
|
527
|
-
tag_label = "Problem"
|
|
528
|
-
scenes_field = SUMMARY_FIELD_PROBLEM_SCENES
|
|
529
|
-
fallback_field = SUMMARY_FIELD_PR_PROBLEM
|
|
530
|
-
else:
|
|
531
|
-
card_class = "fix"
|
|
532
|
-
tag_label = "Fix"
|
|
533
|
-
scenes_field = SUMMARY_FIELD_FIX_SCENES
|
|
534
|
-
fallback_field = SUMMARY_FIELD_PR_FIX
|
|
535
|
-
|
|
536
|
-
scenes = [
|
|
537
|
-
each_scene
|
|
538
|
-
for each_scene in convergence_summary.get(scenes_field, [])
|
|
539
|
-
if isinstance(each_scene, dict)
|
|
540
|
-
]
|
|
541
|
-
if scenes:
|
|
542
|
-
body = "".join(
|
|
543
|
-
_render_scene_row(each_scene, is_problem) for each_scene in scenes
|
|
544
|
-
)
|
|
545
|
-
else:
|
|
546
|
-
fallback_text = html.escape(str(convergence_summary.get(fallback_field, "")))
|
|
547
|
-
body = f'<div class="scene-cap">{fallback_text}</div>'
|
|
548
|
-
|
|
549
|
-
return (
|
|
550
|
-
f'<div class="pf {card_class}">'
|
|
551
|
-
f'<span class="pf-tag">{tag_label}</span>'
|
|
552
|
-
f"{body}"
|
|
553
|
-
"</div>"
|
|
554
|
-
)
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
def _render_pf_grid(convergence_summary: dict) -> str:
|
|
558
|
-
"""Return the 'What this PR does' grid with a problem card and a fix card.
|
|
559
|
-
|
|
560
|
-
Args:
|
|
561
|
-
convergence_summary: A structurally valid convergence summary.
|
|
562
|
-
|
|
563
|
-
Returns:
|
|
564
|
-
An HTML .pf-grid string with both cards.
|
|
565
|
-
"""
|
|
566
|
-
problem_card = _render_pf_card(convergence_summary, is_problem=True)
|
|
567
|
-
fix_card = _render_pf_card(convergence_summary, is_problem=False)
|
|
568
|
-
return f'<div class="pf-grid">{problem_card}{fix_card}</div>'
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
def _render_panel_body(lines: list[str], medium: str) -> str:
|
|
572
|
-
"""Return the inner HTML for a before-or-after panel body.
|
|
573
|
-
|
|
574
|
-
Args:
|
|
575
|
-
lines: The literal short lines to show, joined with line breaks.
|
|
576
|
-
medium: One of 'terminal', 'code', or 'text'.
|
|
577
|
-
|
|
578
|
-
Returns:
|
|
579
|
-
An HTML panel-body string escaped and joined with <br>.
|
|
580
|
-
"""
|
|
581
|
-
escaped_lines = [html.escape(str(each_line)) for each_line in lines]
|
|
582
|
-
joined = "<br>".join(escaped_lines)
|
|
583
|
-
if medium == MEDIUM_TERMINAL:
|
|
584
|
-
return (
|
|
585
|
-
'<div class="terminal">'
|
|
586
|
-
'<div class="term-bar"><i class="r"></i><i class="y"></i><i class="g"></i>'
|
|
587
|
-
f"<span>{TIMELINE_TERMINAL_BAR_LABEL}</span></div>"
|
|
588
|
-
f'<div class="term-body">{joined}</div>'
|
|
589
|
-
"</div>"
|
|
590
|
-
)
|
|
591
|
-
panel_class = "code-panel" if medium == MEDIUM_CODE else "text-panel"
|
|
592
|
-
return f'<div class="{panel_class}">{joined}</div>'
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
def _render_term_grid(issue_class: dict, medium: str) -> str:
|
|
596
|
-
"""Return the before/after panel grid for one issue class.
|
|
597
|
-
|
|
598
|
-
Args:
|
|
599
|
-
issue_class: One issue-class dict from the summary.
|
|
600
|
-
medium: The medium that styles both panels.
|
|
601
|
-
|
|
602
|
-
Returns:
|
|
603
|
-
An HTML .term-grid string with a before panel and an after panel.
|
|
604
|
-
"""
|
|
605
|
-
before_lines = list(issue_class.get(ISSUE_CLASS_FIELD_BEFORE_LINES, []))
|
|
606
|
-
after_lines = list(issue_class.get(ISSUE_CLASS_FIELD_AFTER_LINES, []))
|
|
607
|
-
before_body = _render_panel_body(before_lines, medium)
|
|
608
|
-
after_body = _render_panel_body(after_lines, medium)
|
|
609
|
-
return (
|
|
610
|
-
'<div class="term-grid">'
|
|
611
|
-
'<div class="term-wrap before">'
|
|
612
|
-
f'<div class="tlabel">{TIMELINE_BEFORE_LABEL} '
|
|
613
|
-
f'<span class="pill-x">{TIMELINE_BEFORE_PILL}</span></div>'
|
|
614
|
-
f"{before_body}"
|
|
615
|
-
"</div>"
|
|
616
|
-
'<div class="term-wrap after">'
|
|
617
|
-
f'<div class="tlabel">{TIMELINE_AFTER_LABEL} '
|
|
618
|
-
f'<span class="pill-c">{TIMELINE_AFTER_PILL}</span></div>'
|
|
619
|
-
f"{after_body}"
|
|
620
|
-
"</div>"
|
|
621
|
-
"</div>"
|
|
622
|
-
)
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
def _render_cause_line(issue_class: dict) -> str:
|
|
626
|
-
"""Return the cause line plus a muted severity/category/count/status parenthetical.
|
|
627
|
-
|
|
628
|
-
Args:
|
|
629
|
-
issue_class: One issue-class dict from the summary.
|
|
630
|
-
|
|
631
|
-
Returns:
|
|
632
|
-
An HTML .cause string.
|
|
633
|
-
"""
|
|
634
|
-
cause = html.escape(str(issue_class.get(ISSUE_CLASS_FIELD_CAUSE, "")))
|
|
635
|
-
severity = str(issue_class.get(ISSUE_CLASS_FIELD_SEVERITY, DEFAULT_FINDING_SEVERITY))
|
|
636
|
-
category = str(issue_class.get(ISSUE_CLASS_FIELD_CATEGORY, CATEGORY_BUG))
|
|
637
|
-
count = _coerce_count(issue_class.get(ISSUE_CLASS_FIELD_COUNT, 0))
|
|
638
|
-
status = str(issue_class.get(ISSUE_CLASS_FIELD_STATUS, ""))
|
|
639
|
-
|
|
640
|
-
category_label = CATEGORY_LABEL_BY_VALUE.get(category, category)
|
|
641
|
-
status_label = STATUS_LABEL_BY_VALUE.get(status, status)
|
|
642
|
-
parenthetical = (
|
|
643
|
-
f"({html.escape(severity)} {html.escape(category_label)} "
|
|
644
|
-
f"· ×{count} · {html.escape(status_label)})"
|
|
645
|
-
)
|
|
646
|
-
return (
|
|
647
|
-
'<div class="cause">'
|
|
648
|
-
f"<b>Why it happened:</b> {cause} "
|
|
649
|
-
f'<span style="{CAUSE_MUTED_STYLE}">{parenthetical}</span>'
|
|
650
|
-
"</div>"
|
|
651
|
-
)
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
def _issue_class_sort_key(issue_class: dict) -> tuple[int, int]:
|
|
655
|
-
"""Return a sort key ordering bug classes first, then by severity.
|
|
656
|
-
|
|
657
|
-
Args:
|
|
658
|
-
issue_class: One issue-class dict from the summary.
|
|
659
|
-
|
|
660
|
-
Returns:
|
|
661
|
-
A tuple of (category rank, severity rank); lower sorts first.
|
|
662
|
-
"""
|
|
663
|
-
category = issue_class.get(ISSUE_CLASS_FIELD_CATEGORY, CATEGORY_BUG)
|
|
664
|
-
severity = issue_class.get(ISSUE_CLASS_FIELD_SEVERITY, DEFAULT_FINDING_SEVERITY)
|
|
665
|
-
category_rank = CATEGORY_SORT_ORDER.get(category, len(CATEGORY_SORT_ORDER))
|
|
666
|
-
severity_rank = SEVERITY_SORT_RANK.get(severity, len(SEVERITY_SORT_RANK))
|
|
667
|
-
return (category_rank, severity_rank)
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
def _render_issue_class_heading(issue_class: dict) -> str:
|
|
671
|
-
"""Return the per-class heading with a category icon, plain name, and count.
|
|
672
|
-
|
|
673
|
-
Args:
|
|
674
|
-
issue_class: One issue-class dict from the summary.
|
|
675
|
-
|
|
676
|
-
Returns:
|
|
677
|
-
An HTML .bug-head block: a category icon and the plain bug name grouped in
|
|
678
|
-
a .bug-title span, plus a finding-count chip.
|
|
679
|
-
"""
|
|
680
|
-
plain_name = html.escape(str(issue_class.get(ISSUE_CLASS_FIELD_PLAINNAME, "")))
|
|
681
|
-
count = _coerce_count(issue_class.get(ISSUE_CLASS_FIELD_COUNT, 0))
|
|
682
|
-
count_phrase = html.escape(_pluralize(count, "finding", "findings"))
|
|
683
|
-
category = str(issue_class.get(ISSUE_CLASS_FIELD_CATEGORY, CATEGORY_BUG))
|
|
684
|
-
icon = ISSUE_ICON_BY_CATEGORY.get(category, DEFAULT_ISSUE_ICON)
|
|
685
|
-
return (
|
|
686
|
-
'<div class="bug-head">'
|
|
687
|
-
'<span class="bug-title">'
|
|
688
|
-
f'<span class="bug-icon">{icon}</span>'
|
|
689
|
-
f'<span class="bug-name">{plain_name}</span>'
|
|
690
|
-
"</span>"
|
|
691
|
-
f'<span class="bug-count">{count_phrase}</span>'
|
|
692
|
-
"</div>"
|
|
693
|
-
)
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
def _render_issue_class_block(issue_class: dict) -> str:
|
|
697
|
-
"""Return one issue-class block: a name heading, before/after panels, a cause line.
|
|
698
|
-
|
|
699
|
-
Args:
|
|
700
|
-
issue_class: One issue-class dict from the summary.
|
|
701
|
-
|
|
702
|
-
Returns:
|
|
703
|
-
An HTML fragment that opens with the .bug-head name heading, then a
|
|
704
|
-
.term-grid when panel lines exist, then the .cause line; the heading and
|
|
705
|
-
cause line alone when both before and after lines are empty.
|
|
706
|
-
"""
|
|
707
|
-
heading = _render_issue_class_heading(issue_class)
|
|
708
|
-
medium = str(issue_class.get(ISSUE_CLASS_FIELD_MEDIUM, MEDIUM_TERMINAL))
|
|
709
|
-
before_lines = list(issue_class.get(ISSUE_CLASS_FIELD_BEFORE_LINES, []))
|
|
710
|
-
after_lines = list(issue_class.get(ISSUE_CLASS_FIELD_AFTER_LINES, []))
|
|
711
|
-
|
|
712
|
-
cause_line = _render_cause_line(issue_class)
|
|
713
|
-
if not before_lines and not after_lines:
|
|
714
|
-
return heading + cause_line
|
|
715
|
-
|
|
716
|
-
term_grid = _render_term_grid(issue_class, medium)
|
|
717
|
-
return heading + term_grid + cause_line
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
def _render_issue_class_panels(convergence_summary: dict) -> str:
|
|
721
|
-
"""Return the per-issue-class before/after panels and cause lines.
|
|
722
|
-
|
|
723
|
-
Args:
|
|
724
|
-
convergence_summary: A structurally valid convergence summary.
|
|
725
|
-
|
|
726
|
-
Returns:
|
|
727
|
-
An HTML fragment with one block per issue class, ordered bug classes
|
|
728
|
-
first then by severity.
|
|
729
|
-
"""
|
|
730
|
-
issue_classes = [
|
|
731
|
-
each_class
|
|
732
|
-
for each_class in convergence_summary.get(SUMMARY_FIELD_ISSUE_CLASSES, [])
|
|
733
|
-
if isinstance(each_class, dict)
|
|
734
|
-
]
|
|
735
|
-
if not issue_classes:
|
|
736
|
-
return (
|
|
737
|
-
'<p class="subtitle">No issues were caught — '
|
|
738
|
-
"every review lens was clean.</p>"
|
|
739
|
-
)
|
|
740
|
-
sorted_classes = sorted(issue_classes, key=_issue_class_sort_key)
|
|
741
|
-
return "".join(
|
|
742
|
-
_render_issue_class_block(each_class) for each_class in sorted_classes
|
|
743
|
-
)
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
def _render_caught_lead(
|
|
747
|
-
convergence_summary: dict, run_data: RunData, round_count: int
|
|
748
|
-
) -> str:
|
|
749
|
-
"""Return the run-stats lead line that opens the caught section.
|
|
750
|
-
|
|
751
|
-
Args:
|
|
752
|
-
convergence_summary: A structurally valid convergence summary.
|
|
753
|
-
run_data: Aggregated metrics from the journal.
|
|
754
|
-
round_count: Total number of convergence rounds.
|
|
755
|
-
|
|
756
|
-
Returns:
|
|
757
|
-
An HTML .subtitle line stating bug-class, finding, round, and fix counts.
|
|
758
|
-
"""
|
|
759
|
-
issue_classes = [
|
|
760
|
-
each_class
|
|
761
|
-
for each_class in convergence_summary.get(SUMMARY_FIELD_ISSUE_CLASSES, [])
|
|
762
|
-
if isinstance(each_class, dict)
|
|
763
|
-
]
|
|
764
|
-
class_phrase = _pluralize(len(issue_classes), "bug class", "bug classes")
|
|
765
|
-
finding_phrase = _pluralize(run_data.total_finding_count, "finding", "findings")
|
|
766
|
-
round_phrase = _pluralize(round_count, "round", "rounds")
|
|
767
|
-
fix_phrase = _pluralize(run_data.fix_commit_count, "fix commit", "fix commits")
|
|
768
|
-
return (
|
|
769
|
-
f'<p class="subtitle">{class_phrase} ({finding_phrase} in all), '
|
|
770
|
-
f"caught and fixed across {round_phrase} in {fix_phrase}.</p>"
|
|
771
|
-
)
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
def _appendix_finding_sort_key(finding: RawFinding) -> tuple[int, int, str, int]:
|
|
775
|
-
"""Return a sort key grouping appendix findings by category then severity.
|
|
776
|
-
|
|
777
|
-
Args:
|
|
778
|
-
finding: One distinct finding.
|
|
779
|
-
|
|
780
|
-
Returns:
|
|
781
|
-
A tuple of (category rank, severity rank, file, line).
|
|
782
|
-
"""
|
|
783
|
-
category_rank = CATEGORY_SORT_ORDER.get(finding.category, len(CATEGORY_SORT_ORDER))
|
|
784
|
-
severity_rank = SEVERITY_SORT_RANK.get(finding.severity, len(SEVERITY_SORT_RANK))
|
|
785
|
-
return (category_rank, severity_rank, finding.file, finding.line)
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
def _render_appendix(findings: list[RawFinding]) -> str:
|
|
789
|
-
"""Return a collapsed <details> appendix of raw distinct findings.
|
|
790
|
-
|
|
791
|
-
Args:
|
|
792
|
-
findings: The distinct findings to list, grouped by category then severity.
|
|
793
|
-
|
|
794
|
-
Returns:
|
|
795
|
-
An HTML <details> string listing each finding as 'file:line — P# — title'.
|
|
796
|
-
"""
|
|
797
|
-
if not findings:
|
|
798
|
-
return ""
|
|
799
|
-
sorted_findings = sorted(findings, key=_appendix_finding_sort_key)
|
|
800
|
-
items = "".join(
|
|
801
|
-
f'<div class="appendix-item">'
|
|
802
|
-
f"{html.escape(each_finding.file)}:{each_finding.line} — "
|
|
803
|
-
f"{html.escape(each_finding.severity)} — "
|
|
804
|
-
f"{html.escape(each_finding.title)}"
|
|
805
|
-
f"</div>"
|
|
806
|
-
for each_finding in sorted_findings
|
|
807
|
-
)
|
|
808
|
-
return (
|
|
809
|
-
'<details class="appendix">'
|
|
810
|
-
f"<summary>Raw findings ({len(findings)})</summary>"
|
|
811
|
-
f'<div class="appendix-body">{items}</div>'
|
|
812
|
-
"</details>"
|
|
813
|
-
)
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
def _render_summary_body(
|
|
817
|
-
run_data: RunData, round_count: int, final_sha_short: str
|
|
818
|
-
) -> str:
|
|
819
|
-
"""Return the body for a run that carries a valid convergence summary.
|
|
820
|
-
|
|
821
|
-
Args:
|
|
822
|
-
run_data: Aggregated metrics from the journal.
|
|
823
|
-
round_count: Total number of convergence rounds.
|
|
824
|
-
final_sha_short: First eight characters of the final commit sha.
|
|
825
|
-
|
|
826
|
-
Returns:
|
|
827
|
-
An HTML body fragment: verdict banner, an at-a-glance scorecard,
|
|
828
|
-
problem/fix cards, then a single caught section that opens with a
|
|
829
|
-
run-stats lead line and holds the issue-class before/after panels,
|
|
830
|
-
followed by the collapsed appendix.
|
|
831
|
-
"""
|
|
832
|
-
convergence_summary = run_data.convergence_summary
|
|
833
|
-
if convergence_summary is None:
|
|
834
|
-
return _render_degraded_body(run_data, round_count, final_sha_short)
|
|
835
|
-
|
|
836
|
-
verdict_banner = _render_verdict_banner(
|
|
837
|
-
convergence_summary, run_data, final_sha_short
|
|
838
|
-
)
|
|
839
|
-
scorecard = _render_scorecard(run_data, round_count)
|
|
840
|
-
pf_grid = _render_pf_grid(convergence_summary)
|
|
841
|
-
caught_lead = _render_caught_lead(convergence_summary, run_data, round_count)
|
|
842
|
-
issue_panels = _render_issue_class_panels(convergence_summary)
|
|
843
|
-
appendix = _render_appendix(run_data.all_distinct_findings)
|
|
844
|
-
return (
|
|
845
|
-
f"{verdict_banner}"
|
|
846
|
-
f"{scorecard}"
|
|
847
|
-
f"<h2>What this PR does</h2>{pf_grid}"
|
|
848
|
-
f"<h2>What was caught — and how it looked</h2>{caught_lead}{issue_panels}"
|
|
849
|
-
f"{appendix}"
|
|
850
|
-
)
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
def _render_degraded_body(
|
|
854
|
-
run_data: RunData, round_count: int, final_sha_short: str
|
|
855
|
-
) -> str:
|
|
856
|
-
"""Return the minimal degraded body for a run with no valid summary.
|
|
857
|
-
|
|
858
|
-
Args:
|
|
859
|
-
run_data: Aggregated metrics from the journal.
|
|
860
|
-
round_count: Total number of convergence rounds.
|
|
861
|
-
final_sha_short: First eight characters of the final commit sha.
|
|
862
|
-
|
|
863
|
-
Returns:
|
|
864
|
-
An HTML body fragment: a plain run-stats note and the collapsed
|
|
865
|
-
raw-findings appendix, with no scene, table, rollup, or timeline markup.
|
|
866
|
-
"""
|
|
867
|
-
fix_commit_phrase = _pluralize(
|
|
868
|
-
run_data.fix_commit_count, "fix commit", "fix commits"
|
|
869
|
-
)
|
|
870
|
-
note = (
|
|
871
|
-
'<p class="subtitle">'
|
|
872
|
-
f"{run_data.total_finding_count} distinct findings across {round_count} "
|
|
873
|
-
f"rounds, resolved in {fix_commit_phrase}. "
|
|
874
|
-
f"Final commit <code>{html.escape(final_sha_short)}</code>."
|
|
875
|
-
"</p>"
|
|
876
|
-
)
|
|
877
|
-
appendix = _render_appendix(run_data.all_distinct_findings)
|
|
878
|
-
return f"{note}{appendix}"
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
def render_report_html(
|
|
882
|
-
run_data: RunData, pr_metadata: PrMetadata, generated_date: str
|
|
883
|
-
) -> str:
|
|
884
|
-
"""Render the convergence summary report as an HTML string.
|
|
885
|
-
|
|
886
|
-
Args:
|
|
887
|
-
run_data: Aggregated metrics from the workflow journal and transcripts.
|
|
888
|
-
pr_metadata: Owner, repo, number, final sha, and round count for the PR.
|
|
889
|
-
generated_date: ISO date string derived from the journal timestamp.
|
|
890
|
-
|
|
891
|
-
Returns:
|
|
892
|
-
A page-content HTML string (a title, a style block, and the body
|
|
893
|
-
markup) with no surrounding <!DOCTYPE>, <html>, <head>, or <body>
|
|
894
|
-
tags, ready for the Artifact tool to wrap and publish.
|
|
895
|
-
"""
|
|
896
|
-
short_sha_length = SHORT_SHA_LENGTH
|
|
897
|
-
pr_number = pr_metadata.number
|
|
898
|
-
owner = html.escape(pr_metadata.owner)
|
|
899
|
-
repo = html.escape(pr_metadata.repo)
|
|
900
|
-
final_sha_short = pr_metadata.final_sha[:short_sha_length]
|
|
901
|
-
round_count = pr_metadata.round_count
|
|
902
|
-
|
|
903
|
-
title_and_style_html = HTML_TITLE_AND_STYLE_TEMPLATE.format(
|
|
904
|
-
pr_number=pr_number,
|
|
905
|
-
style_block=HTML_STYLE_BLOCK,
|
|
906
|
-
)
|
|
907
|
-
|
|
908
|
-
subtitle = (
|
|
909
|
-
f'<p class="subtitle">{owner}/{repo} · '
|
|
910
|
-
f"{run_data.total_finding_count} findings over {round_count} rounds "
|
|
911
|
-
f"· {html.escape(generated_date)}</p>"
|
|
912
|
-
)
|
|
913
|
-
|
|
914
|
-
if _is_summary_structurally_valid(run_data.convergence_summary):
|
|
915
|
-
body_main = _render_summary_body(run_data, round_count, final_sha_short)
|
|
916
|
-
else:
|
|
917
|
-
body_main = _render_degraded_body(run_data, round_count, final_sha_short)
|
|
918
|
-
|
|
919
|
-
fix_commit_phrase = _pluralize(
|
|
920
|
-
run_data.fix_commit_count, "fix commit", "fix commits"
|
|
921
|
-
)
|
|
922
|
-
footer = (
|
|
923
|
-
f"<footer>{owner}/{repo} · PR #{pr_number} · "
|
|
924
|
-
f"{run_data.total_finding_count} findings · {round_count} rounds "
|
|
925
|
-
f"· {fix_commit_phrase} · "
|
|
926
|
-
f"generated {html.escape(generated_date)} from the autoconverge run journal.</footer>"
|
|
927
|
-
)
|
|
928
|
-
|
|
929
|
-
body_content = (
|
|
930
|
-
f"<h1>PR #{pr_number} Convergence Summary</h1>"
|
|
931
|
-
f"{subtitle}"
|
|
932
|
-
f"{body_main}"
|
|
933
|
-
f"{footer}"
|
|
934
|
-
)
|
|
935
|
-
|
|
936
|
-
return (
|
|
937
|
-
f"{title_and_style_html}\n"
|
|
938
|
-
f'<div class="container">\n'
|
|
939
|
-
f"{body_content}\n"
|
|
940
|
-
f"</div>"
|
|
941
|
-
)
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
def _parse_pr_arg(pr_arg: str, err_stream: TextIO) -> tuple[str, str, int] | None:
|
|
945
|
-
"""Parse an 'owner/repo#number' string into its three components.
|
|
946
|
-
|
|
947
|
-
Args:
|
|
948
|
-
pr_arg: A string in the form 'owner/repo#number'.
|
|
949
|
-
err_stream: Stream to write error messages to.
|
|
950
|
-
|
|
951
|
-
Returns:
|
|
952
|
-
A tuple of (owner, repo, pr_number_int), or None on parse failure.
|
|
953
|
-
"""
|
|
954
|
-
match = re.fullmatch(r"([^/]+)/([^#]+)#(\d+)", pr_arg)
|
|
955
|
-
if not match:
|
|
956
|
-
err_stream.write(
|
|
957
|
-
f"Invalid --pr format: {pr_arg!r}. Expected owner/repo#number.\n"
|
|
958
|
-
)
|
|
959
|
-
return None
|
|
960
|
-
return match.group(1), match.group(2), int(match.group(3))
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
def main(out_stream: TextIO = sys.stdout, err_stream: TextIO = sys.stderr) -> int:
|
|
964
|
-
"""Parse CLI arguments, load run data, render HTML, write the file, emit the path.
|
|
965
|
-
|
|
966
|
-
Args:
|
|
967
|
-
out_stream: Stream to write the output file path to on success.
|
|
968
|
-
err_stream: Stream to write error messages to.
|
|
969
|
-
|
|
970
|
-
Returns:
|
|
971
|
-
Exit code (0 on success, 1 on argument error).
|
|
972
|
-
"""
|
|
973
|
-
argument_parser = argparse.ArgumentParser(
|
|
974
|
-
description="Render autoconverge convergence summary HTML."
|
|
975
|
-
)
|
|
976
|
-
argument_parser.add_argument(
|
|
977
|
-
"--journal", required=True, help="Path to wf_<runId>.json"
|
|
978
|
-
)
|
|
979
|
-
argument_parser.add_argument("--out", required=True, help="Output HTML file path")
|
|
980
|
-
argument_parser.add_argument("--pr", required=True, help="owner/repo#number")
|
|
981
|
-
argument_parser.add_argument("--final-sha", required=True, help="Final commit SHA")
|
|
982
|
-
argument_parser.add_argument(
|
|
983
|
-
"--rounds", required=True, type=int, help="Total round count"
|
|
984
|
-
)
|
|
985
|
-
argument_parser.add_argument(
|
|
986
|
-
"--summary-file",
|
|
987
|
-
default=None,
|
|
988
|
-
help="Path to a JSON file with the convergence summary to inject",
|
|
989
|
-
)
|
|
990
|
-
|
|
991
|
-
parsed_args = argument_parser.parse_args()
|
|
992
|
-
|
|
993
|
-
journal_path = Path(parsed_args.journal).resolve()
|
|
994
|
-
out_path = Path(parsed_args.out)
|
|
995
|
-
|
|
996
|
-
parsed_pr = _parse_pr_arg(parsed_args.pr, err_stream)
|
|
997
|
-
if parsed_pr is None:
|
|
998
|
-
return 1
|
|
999
|
-
|
|
1000
|
-
owner, repo, pr_number = parsed_pr
|
|
1001
|
-
|
|
1002
|
-
pr_metadata = PrMetadata(
|
|
1003
|
-
owner=owner,
|
|
1004
|
-
repo=repo,
|
|
1005
|
-
number=pr_number,
|
|
1006
|
-
final_sha=parsed_args.final_sha,
|
|
1007
|
-
round_count=parsed_args.rounds,
|
|
1008
|
-
)
|
|
1009
|
-
|
|
1010
|
-
run_data = load_run_data(journal_path)
|
|
1011
|
-
if parsed_args.summary_file:
|
|
1012
|
-
run_data.convergence_summary = json.loads(
|
|
1013
|
-
Path(parsed_args.summary_file).read_text(encoding="utf-8")
|
|
1014
|
-
)
|
|
1015
|
-
html_content = render_report_html(run_data, pr_metadata, run_data.generated_date)
|
|
1016
|
-
|
|
1017
|
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1018
|
-
out_path.write_text(html_content, encoding="utf-8")
|
|
1019
|
-
out_stream.write(str(out_path) + "\n")
|
|
1020
|
-
return 0
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
if __name__ == "__main__":
|
|
1024
|
-
sys.exit(main())
|