devcouncil 0.3.1 → 0.4.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/README.md +46 -30
- package/package.json +6 -2
- package/packages/codeintel-grammars/hatch_build.py +43 -0
- package/packages/codeintel-grammars/pyproject.toml +16 -0
- package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
- package/pyproject.toml +99 -4
- package/src/devcouncil/app/config.py +512 -20
- package/src/devcouncil/app/events.py +4 -23
- package/src/devcouncil/app/orchestrator.py +5 -0
- package/src/devcouncil/app/run_context.py +3 -3
- package/src/devcouncil/assets/__init__.py +4 -1
- package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
- package/src/devcouncil/campaign/__init__.py +71 -0
- package/src/devcouncil/campaign/bloom.py +137 -0
- package/src/devcouncil/campaign/dashboard.py +123 -0
- package/src/devcouncil/campaign/mailbox.py +305 -0
- package/src/devcouncil/campaign/notify.py +91 -0
- package/src/devcouncil/campaign/orchestrator.py +592 -0
- package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
- package/src/devcouncil/campaign/prompts/director.md +21 -0
- package/src/devcouncil/campaign/prompts/protocol.md +46 -0
- package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
- package/src/devcouncil/campaign/prompts/worker.md +24 -0
- package/src/devcouncil/campaign/roles.py +202 -0
- package/src/devcouncil/campaign/watcher.py +153 -0
- package/src/devcouncil/cli/commands/agents.py +24 -17
- package/src/devcouncil/cli/commands/artifacts.py +36 -27
- package/src/devcouncil/cli/commands/ast.py +12 -3
- package/src/devcouncil/cli/commands/baseline.py +21 -12
- package/src/devcouncil/cli/commands/boot.py +218 -0
- package/src/devcouncil/cli/commands/campaign.py +302 -0
- package/src/devcouncil/cli/commands/check.py +225 -12
- package/src/devcouncil/cli/commands/config.py +221 -74
- package/src/devcouncil/cli/commands/cost.py +137 -28
- package/src/devcouncil/cli/commands/dashboard.py +12 -4
- package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
- package/src/devcouncil/cli/commands/design.py +27 -17
- package/src/devcouncil/cli/commands/doctor.py +790 -8
- package/src/devcouncil/cli/commands/evidence.py +41 -20
- package/src/devcouncil/cli/commands/export.py +73 -0
- package/src/devcouncil/cli/commands/gaps.py +175 -0
- package/src/devcouncil/cli/commands/gated_write.py +76 -0
- package/src/devcouncil/cli/commands/go.py +220 -68
- package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
- package/src/devcouncil/cli/commands/handoff.py +45 -34
- package/src/devcouncil/cli/commands/hook.py +630 -85
- package/src/devcouncil/cli/commands/init.py +89 -30
- package/src/devcouncil/cli/commands/integrate.py +296 -1385
- package/src/devcouncil/cli/commands/lease.py +120 -0
- package/src/devcouncil/cli/commands/logs.py +12 -5
- package/src/devcouncil/cli/commands/lsp.py +40 -5
- package/src/devcouncil/cli/commands/map.py +317 -74
- package/src/devcouncil/cli/commands/mcp_server.py +12 -2
- package/src/devcouncil/cli/commands/okf.py +44 -6
- package/src/devcouncil/cli/commands/plan.py +184 -69
- package/src/devcouncil/cli/commands/prompt.py +26 -17
- package/src/devcouncil/cli/commands/provenance.py +79 -0
- package/src/devcouncil/cli/commands/repair.py +60 -49
- package/src/devcouncil/cli/commands/report.py +148 -40
- package/src/devcouncil/cli/commands/requirements.py +104 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
- package/src/devcouncil/cli/commands/rollback.py +46 -35
- package/src/devcouncil/cli/commands/run.py +173 -8
- package/src/devcouncil/cli/commands/runs.py +298 -68
- package/src/devcouncil/cli/commands/scaffold.py +33 -12
- package/src/devcouncil/cli/commands/semantic.py +29 -14
- package/src/devcouncil/cli/commands/setup.py +103 -93
- package/src/devcouncil/cli/commands/shell.py +51 -42
- package/src/devcouncil/cli/commands/show.py +56 -42
- package/src/devcouncil/cli/commands/skills.py +29 -20
- package/src/devcouncil/cli/commands/status.py +80 -67
- package/src/devcouncil/cli/commands/task_gate.py +295 -0
- package/src/devcouncil/cli/commands/tasks.py +248 -19
- package/src/devcouncil/cli/commands/trace.py +14 -8
- package/src/devcouncil/cli/commands/verify.py +33 -8
- package/src/devcouncil/cli/commands/version.py +14 -6
- package/src/devcouncil/cli/commands/watch.py +49 -30
- package/src/devcouncil/cli/commands/watch_fs.py +30 -19
- package/src/devcouncil/cli/commands/wiki.py +278 -0
- package/src/devcouncil/cli/main.py +58 -1
- package/src/devcouncil/codeintel/__init__.py +16 -0
- package/src/devcouncil/codeintel/build_control.py +429 -0
- package/src/devcouncil/codeintel/build_worker.py +78 -0
- package/src/devcouncil/codeintel/debug/__init__.py +17 -0
- package/src/devcouncil/codeintel/debug/broker.py +114 -0
- package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
- package/src/devcouncil/codeintel/debug/consent.py +36 -0
- package/src/devcouncil/codeintel/debug/discovery.py +132 -0
- package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
- package/src/devcouncil/codeintel/debug/protocol.py +259 -0
- package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
- package/src/devcouncil/codeintel/debug/session.py +238 -0
- package/src/devcouncil/codeintel/debug/tracing.py +201 -0
- package/src/devcouncil/codeintel/languages/__init__.py +17 -0
- package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
- package/src/devcouncil/codeintel/languages/registry.py +149 -0
- package/src/devcouncil/codeintel/languages/workers.py +245 -0
- package/src/devcouncil/codeintel/query/__init__.py +5 -0
- package/src/devcouncil/codeintel/query/engine.py +289 -0
- package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
- package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
- package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
- package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
- package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
- package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
- package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
- package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
- package/src/devcouncil/codeintel/service.py +104 -0
- package/src/devcouncil/codeintel/store/__init__.py +15 -0
- package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
- package/src/devcouncil/codeintel/sync/__init__.py +19 -0
- package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
- package/src/devcouncil/codeintel/sync/incremental.py +484 -0
- package/src/devcouncil/codeintel/sync/lease.py +96 -0
- package/src/devcouncil/codeintel/sync/scope.py +98 -0
- package/src/devcouncil/council/__init__.py +4 -0
- package/src/devcouncil/council/prompts/__init__.py +4 -0
- package/src/devcouncil/domain/checkpoint_refs.py +17 -0
- package/src/devcouncil/domain/evidence.py +1 -0
- package/src/devcouncil/domain/gap.py +10 -0
- package/src/devcouncil/domain/requirement.py +5 -1
- package/src/devcouncil/domain/task.py +43 -2
- package/src/devcouncil/execution/checkpoints.py +25 -31
- package/src/devcouncil/execution/context_builder.py +15 -44
- package/src/devcouncil/execution/fs_watcher.py +64 -0
- package/src/devcouncil/execution/gated_write.py +203 -0
- package/src/devcouncil/execution/handoff.py +2 -1
- package/src/devcouncil/execution/hook_policy.py +19 -5
- package/src/devcouncil/execution/lease_ops.py +177 -0
- package/src/devcouncil/execution/lease_validation.py +71 -0
- package/src/devcouncil/execution/patch.py +3 -0
- package/src/devcouncil/execution/permissions.py +1 -0
- package/src/devcouncil/execution/policy_engine.py +205 -10
- package/src/devcouncil/execution/prompt_builder.py +278 -33
- package/src/devcouncil/execution/run_trace.py +356 -0
- package/src/devcouncil/execution/shell_session.py +46 -5
- package/src/devcouncil/execution/stop_gate.py +746 -0
- package/src/devcouncil/execution/stop_gate_history.py +113 -0
- package/src/devcouncil/execution/stop_gate_state.py +54 -0
- package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
- package/src/devcouncil/execution/task_gate_ops.py +590 -0
- package/src/devcouncil/execution/task_runner.py +19 -0
- package/src/devcouncil/executors/advisor_tool.py +315 -0
- package/src/devcouncil/executors/agent_registry.py +125 -17
- package/src/devcouncil/executors/claude_sdk.py +376 -0
- package/src/devcouncil/executors/coding_cli.py +724 -25
- package/src/devcouncil/executors/mini_swe.py +50 -8
- package/src/devcouncil/executors/native/agent.py +224 -19
- package/src/devcouncil/executors/openhands.py +50 -8
- package/src/devcouncil/executors/transient_retry.py +99 -0
- package/src/devcouncil/gating/checks/clean_git.py +5 -2
- package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
- package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
- package/src/devcouncil/gating/policy.py +46 -2
- package/src/devcouncil/indexing/ast_matcher.py +41 -4
- package/src/devcouncil/indexing/graph/__init__.py +78 -0
- package/src/devcouncil/indexing/graph/api_routes.py +522 -0
- package/src/devcouncil/indexing/graph/build.py +862 -0
- package/src/devcouncil/indexing/graph/cache.py +329 -0
- package/src/devcouncil/indexing/graph/communities.py +28 -0
- package/src/devcouncil/indexing/graph/cypher.py +107 -0
- package/src/devcouncil/indexing/graph/embeddings.py +194 -0
- package/src/devcouncil/indexing/graph/export.py +381 -0
- package/src/devcouncil/indexing/graph/export_links.py +81 -0
- package/src/devcouncil/indexing/graph/extract_python.py +307 -0
- package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
- package/src/devcouncil/indexing/graph/intel.py +668 -0
- package/src/devcouncil/indexing/graph/liveness.py +992 -0
- package/src/devcouncil/indexing/graph/okf_export.py +65 -0
- package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
- package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
- package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
- package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
- package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
- package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
- package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
- package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
- package/src/devcouncil/indexing/graph/query.py +302 -0
- package/src/devcouncil/indexing/graph/resolve.py +1020 -0
- package/src/devcouncil/indexing/graph/schema.py +103 -0
- package/src/devcouncil/indexing/graph_index.py +20 -29
- package/src/devcouncil/indexing/lsp.py +57 -25
- package/src/devcouncil/indexing/lsp_client.py +577 -0
- package/src/devcouncil/indexing/map_artifacts.py +355 -0
- package/src/devcouncil/indexing/map_refresh.py +141 -0
- package/src/devcouncil/indexing/repo_mapper.py +1509 -138
- package/src/devcouncil/indexing/semantic_index.py +12 -6
- package/src/devcouncil/indexing/subsystem_map.py +163 -0
- package/src/devcouncil/indexing/ts_imports.py +343 -0
- package/src/devcouncil/indexing/viz.py +960 -0
- package/src/devcouncil/indexing/walk.py +52 -0
- package/src/devcouncil/indexing/wiring.py +1776 -0
- package/src/devcouncil/integrations/actions.py +27 -4
- package/src/devcouncil/integrations/check.py +211 -16
- package/src/devcouncil/integrations/claude_assets.py +209 -12
- package/src/devcouncil/integrations/clients/__init__.py +1 -0
- package/src/devcouncil/integrations/clients/aider.py +52 -0
- package/src/devcouncil/integrations/clients/antigravity.py +87 -0
- package/src/devcouncil/integrations/clients/claude.py +339 -0
- package/src/devcouncil/integrations/clients/codex.py +39 -0
- package/src/devcouncil/integrations/clients/common.py +332 -0
- package/src/devcouncil/integrations/clients/cursor.py +164 -0
- package/src/devcouncil/integrations/clients/gemini.py +49 -0
- package/src/devcouncil/integrations/clients/grok.py +105 -0
- package/src/devcouncil/integrations/clients/hooks.py +500 -0
- package/src/devcouncil/integrations/clients/opencode.py +96 -0
- package/src/devcouncil/integrations/clients/warp.py +75 -0
- package/src/devcouncil/integrations/code_review_graph.py +2 -2
- package/src/devcouncil/integrations/github.py +73 -7
- package/src/devcouncil/integrations/integration_cli.py +197 -0
- package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
- package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
- package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
- package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
- package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
- package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
- package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/git.py +99 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -0
- package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
- package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
- package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
- package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
- package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
- package/src/devcouncil/integrations/mcp/handlers/provenance.py +101 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +74 -0
- package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/runs.py +84 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -0
- package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
- package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
- package/src/devcouncil/integrations/mcp/handlers/wiki.py +60 -0
- package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
- package/src/devcouncil/integrations/mcp/server.py +265 -2391
- package/src/devcouncil/integrations/mcp/util.py +303 -0
- package/src/devcouncil/integrations/setup.py +152 -0
- package/src/devcouncil/knowledge/fetch.py +4 -0
- package/src/devcouncil/knowledge/knowledge_select.py +38 -0
- package/src/devcouncil/knowledge/okf.py +2 -1
- package/src/devcouncil/knowledge/resource_discovery.py +40 -0
- package/src/devcouncil/knowledge/wiki.py +643 -0
- package/src/devcouncil/knowledge/wiki_read.py +87 -0
- package/src/devcouncil/live/cards.py +7 -7
- package/src/devcouncil/live/models.py +4 -1
- package/src/devcouncil/live/reviewer.py +90 -11
- package/src/devcouncil/live/signals.py +4 -2
- package/src/devcouncil/live/tasks.py +12 -3
- package/src/devcouncil/live/transcripts.py +69 -2
- package/src/devcouncil/llm/cache.py +5 -6
- package/src/devcouncil/llm/model_defaults.yaml +10 -10
- package/src/devcouncil/llm/provider.py +647 -73
- package/src/devcouncil/llm/router.py +271 -46
- package/src/devcouncil/llm/semantic_bridge.py +614 -0
- package/src/devcouncil/optimization/gepa_agent.py +6 -4
- package/src/devcouncil/optimization/skillopt.py +9 -5
- package/src/devcouncil/planning/arbiter_service.py +12 -3
- package/src/devcouncil/planning/correction_manifest.py +107 -10
- package/src/devcouncil/planning/plan_difficulty.py +69 -0
- package/src/devcouncil/planning/plan_service.py +5 -2
- package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
- package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
- package/src/devcouncil/planning/question_conversion.py +56 -0
- package/src/devcouncil/planning/spec_service.py +9 -3
- package/src/devcouncil/repo/ci_scaffold.py +197 -1
- package/src/devcouncil/repo/gitignore.py +1 -2
- package/src/devcouncil/reporting/evidence_export.py +124 -0
- package/src/devcouncil/reporting/evidence_html.py +210 -0
- package/src/devcouncil/reporting/json_report.py +16 -12
- package/src/devcouncil/reporting/markdown_report.py +38 -9
- package/src/devcouncil/reporting/mcp_resources.py +142 -0
- package/src/devcouncil/reporting/report_builder.py +40 -4
- package/src/devcouncil/reporting/task_provenance.py +42 -0
- package/src/devcouncil/reporting/verdict.py +75 -0
- package/src/devcouncil/skills/library/README.md +1 -0
- package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
- package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
- package/src/devcouncil/skills/library/devcouncil.md +93 -0
- package/src/devcouncil/skills/registry.py +43 -12
- package/src/devcouncil/storage/db.py +57 -11
- package/src/devcouncil/storage/models.py +6 -0
- package/src/devcouncil/storage/native.py +5 -3
- package/src/devcouncil/storage/repositories.py +50 -18
- package/src/devcouncil/telemetry/context.py +28 -0
- package/src/devcouncil/telemetry/cost.py +4 -5
- package/src/devcouncil/telemetry/logging_setup.py +78 -11
- package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
- package/src/devcouncil/telemetry/stages.py +27 -2
- package/src/devcouncil/telemetry/tracker.py +50 -13
- package/src/devcouncil/ui/dashboard.py +120 -8
- package/src/devcouncil/utils/fsio.py +58 -0
- package/src/devcouncil/utils/git_snapshot.py +112 -0
- package/src/devcouncil/utils/json_persist.py +53 -0
- package/src/devcouncil/utils/proc.py +89 -0
- package/src/devcouncil/verification/acceptance_compiler.py +36 -13
- package/src/devcouncil/verification/ad_hoc_check.py +95 -3
- package/src/devcouncil/verification/checks/__init__.py +41 -0
- package/src/devcouncil/verification/checks/acceptance.py +39 -0
- package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
- package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
- package/src/devcouncil/verification/checks/command_evidence.py +148 -0
- package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
- package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
- package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
- package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
- package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
- package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
- package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
- package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
- package/src/devcouncil/verification/checks/planned_files.py +98 -0
- package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
- package/src/devcouncil/verification/checks/stale_map.py +80 -0
- package/src/devcouncil/verification/checks/stub_scan.py +71 -0
- package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
- package/src/devcouncil/verification/checks/wiring.py +216 -0
- package/src/devcouncil/verification/claims/__init__.py +23 -0
- package/src/devcouncil/verification/claims/checks.py +395 -0
- package/src/devcouncil/verification/claims/mapper.py +168 -0
- package/src/devcouncil/verification/claims/models.py +39 -0
- package/src/devcouncil/verification/claims/transcript.py +92 -0
- package/src/devcouncil/verification/claims/verdict.py +88 -0
- package/src/devcouncil/verification/command_evidence.py +170 -0
- package/src/devcouncil/verification/command_malformation.py +147 -0
- package/src/devcouncil/verification/command_runner.py +164 -0
- package/src/devcouncil/verification/coverage_measurement.py +292 -0
- package/src/devcouncil/verification/diff_coverage.py +151 -0
- package/src/devcouncil/verification/difficulty.py +296 -0
- package/src/devcouncil/verification/effort_heuristics.py +178 -0
- package/src/devcouncil/verification/gap_ids.py +63 -0
- package/src/devcouncil/verification/gate_cache.py +194 -0
- package/src/devcouncil/verification/gate_selector.py +344 -0
- package/src/devcouncil/verification/git_diff_fallback.py +272 -0
- package/src/devcouncil/verification/implementation_reviewer.py +13 -0
- package/src/devcouncil/verification/incremental_check.py +241 -0
- package/src/devcouncil/verification/next_actions.py +60 -1
- package/src/devcouncil/verification/rigor_analytics.py +130 -0
- package/src/devcouncil/verification/sandbox.py +38 -11
- package/src/devcouncil/verification/stub_detector.py +369 -0
- package/src/devcouncil/verification/test_resolver.py +67 -1
- package/src/devcouncil/verification/verifier.py +137 -1666
- package/src/devcouncil/verification/verify_orchestration.py +610 -0
- package/src/devcouncil/verification/verify_setup.py +176 -0
- package/src/devcouncil/verification/wiki_refresh.py +208 -0
- package/src/semantic_layer/__init__.py +58 -0
- package/src/semantic_layer/benchmark.py +75 -0
- package/src/semantic_layer/cache.py +290 -0
- package/src/semantic_layer/compressor.py +137 -0
- package/src/semantic_layer/config.py +75 -0
- package/src/semantic_layer/embeddings.py +69 -0
- package/src/semantic_layer/llm_backends.py +99 -0
- package/src/semantic_layer/pipeline.py +111 -0
- package/src/semantic_layer/router.py +128 -0
- package/src/semantic_layer/tuner.py +72 -0
- package/uv.lock +973 -9
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/integrations/gitnexus.py +0 -70
- package/src/devcouncil/integrations/graphify.py +0 -34
|
@@ -12,15 +12,19 @@ from pathlib import Path
|
|
|
12
12
|
from devcouncil.app.config import DevCouncilConfig, load_config
|
|
13
13
|
|
|
14
14
|
WORKFLOW_RELPATH = Path(".github") / "workflows" / "devcouncil.yml"
|
|
15
|
+
EVIDENCE_WORKFLOW_RELPATH = Path(".github") / "workflows" / "devcouncil-evidence.yml"
|
|
15
16
|
|
|
16
17
|
_PYTHON_TOOLS = {
|
|
17
18
|
"pytest", "flake8", "ruff", "mypy", "tox", "python", "python3", "uv",
|
|
18
|
-
"poetry", "black", "isort", "pyright",
|
|
19
|
+
"poetry", "black", "isort", "pyright", "pip-audit",
|
|
19
20
|
}
|
|
20
21
|
_NODE_TOOLS = {
|
|
21
22
|
"npm", "npx", "pnpm", "yarn", "bun", "eslint", "tsc", "jest", "vitest", "node",
|
|
22
23
|
}
|
|
24
|
+
_GO_TOOLS = {"go", "govulncheck"}
|
|
25
|
+
_RUST_TOOLS = {"cargo", "rustc", "rustfmt", "clippy-driver"}
|
|
23
26
|
_PYTHON_MARKERS = ("pyproject.toml", "requirements.txt", "setup.py", "setup.cfg", "Pipfile")
|
|
27
|
+
_UV_LOCK = "uv.lock"
|
|
24
28
|
|
|
25
29
|
|
|
26
30
|
def detect_stacks(project_root: Path) -> set[str]:
|
|
@@ -28,8 +32,14 @@ def detect_stacks(project_root: Path) -> set[str]:
|
|
|
28
32
|
stacks: set[str] = set()
|
|
29
33
|
if any((project_root / marker).exists() for marker in _PYTHON_MARKERS):
|
|
30
34
|
stacks.add("python")
|
|
35
|
+
if (project_root / _UV_LOCK).exists():
|
|
36
|
+
stacks.add("python")
|
|
31
37
|
if (project_root / "package.json").exists():
|
|
32
38
|
stacks.add("node")
|
|
39
|
+
if (project_root / "go.mod").exists():
|
|
40
|
+
stacks.add("go")
|
|
41
|
+
if (project_root / "Cargo.toml").exists():
|
|
42
|
+
stacks.add("rust")
|
|
33
43
|
return stacks
|
|
34
44
|
|
|
35
45
|
|
|
@@ -39,6 +49,10 @@ def _command_stack(command: str) -> str | None:
|
|
|
39
49
|
return "python"
|
|
40
50
|
if tool in _NODE_TOOLS:
|
|
41
51
|
return "node"
|
|
52
|
+
if tool in _GO_TOOLS:
|
|
53
|
+
return "go"
|
|
54
|
+
if tool in _RUST_TOOLS:
|
|
55
|
+
return "rust"
|
|
42
56
|
return None
|
|
43
57
|
|
|
44
58
|
|
|
@@ -68,6 +82,16 @@ _AUDIT_STEPS: dict[str, list[str]] = {
|
|
|
68
82
|
" continue-on-error: true",
|
|
69
83
|
" run: npm audit --audit-level=high",
|
|
70
84
|
],
|
|
85
|
+
"go": [
|
|
86
|
+
" - name: Dependency audit (govulncheck)",
|
|
87
|
+
" continue-on-error: true",
|
|
88
|
+
" run: govulncheck ./...",
|
|
89
|
+
],
|
|
90
|
+
"rust": [
|
|
91
|
+
" - name: Dependency audit (cargo audit)",
|
|
92
|
+
" continue-on-error: true",
|
|
93
|
+
" run: cargo audit",
|
|
94
|
+
],
|
|
71
95
|
}
|
|
72
96
|
|
|
73
97
|
|
|
@@ -86,6 +110,45 @@ def _python_version(project_root: Path) -> str:
|
|
|
86
110
|
return "3.12"
|
|
87
111
|
|
|
88
112
|
|
|
113
|
+
def _uses_uv(project_root: Path) -> bool:
|
|
114
|
+
return (project_root / _UV_LOCK).exists()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _default_install_command(project_root: Path) -> str:
|
|
118
|
+
if _uses_uv(project_root):
|
|
119
|
+
return f"uv sync --all-groups --python {_python_version(project_root)}"
|
|
120
|
+
return "pip install -e ."
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _default_dev_command(project_root: Path) -> str:
|
|
124
|
+
if _uses_uv(project_root):
|
|
125
|
+
return f"uv run --python {_python_version(project_root)} dev"
|
|
126
|
+
return "dev"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _evidence_python_setup_steps(project_root: Path, *, install_command: str) -> list[str]:
|
|
130
|
+
py_version = _python_version(project_root)
|
|
131
|
+
if _uses_uv(project_root):
|
|
132
|
+
return [
|
|
133
|
+
" - name: Install uv",
|
|
134
|
+
" uses: astral-sh/setup-uv@v7",
|
|
135
|
+
" - name: Set up Python",
|
|
136
|
+
" uses: actions/setup-python@v6",
|
|
137
|
+
" with:",
|
|
138
|
+
f' python-version: "{py_version}"',
|
|
139
|
+
" - name: Install DevCouncil",
|
|
140
|
+
f" run: {install_command}",
|
|
141
|
+
]
|
|
142
|
+
return [
|
|
143
|
+
" - name: Set up Python",
|
|
144
|
+
" uses: actions/setup-python@v5",
|
|
145
|
+
" with:",
|
|
146
|
+
f' python-version: "{py_version}"',
|
|
147
|
+
" - name: Install DevCouncil",
|
|
148
|
+
f" run: {install_command}",
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
|
|
89
152
|
def render_workflow(
|
|
90
153
|
project_root: Path,
|
|
91
154
|
default_branch: str = "main",
|
|
@@ -115,6 +178,20 @@ def render_workflow(
|
|
|
115
178
|
" with:",
|
|
116
179
|
' node-version: "20"',
|
|
117
180
|
]
|
|
181
|
+
if "go" in stacks:
|
|
182
|
+
steps += [
|
|
183
|
+
" - name: Set up Go",
|
|
184
|
+
" uses: actions/setup-go@v5",
|
|
185
|
+
" with:",
|
|
186
|
+
' go-version: "stable"',
|
|
187
|
+
]
|
|
188
|
+
if "rust" in stacks:
|
|
189
|
+
steps += [
|
|
190
|
+
" - name: Set up Rust",
|
|
191
|
+
" uses: dtolnay/rust-toolchain@stable",
|
|
192
|
+
" with:",
|
|
193
|
+
" components: clippy",
|
|
194
|
+
]
|
|
118
195
|
|
|
119
196
|
def add_command_steps(label: str, raw_commands: list[str]) -> None:
|
|
120
197
|
for command in _applicable_commands(raw_commands, stacks):
|
|
@@ -146,6 +223,104 @@ def render_workflow(
|
|
|
146
223
|
)
|
|
147
224
|
|
|
148
225
|
|
|
226
|
+
def render_evidence_workflow(
|
|
227
|
+
project_root: Path,
|
|
228
|
+
default_branch: str = "main",
|
|
229
|
+
*,
|
|
230
|
+
install_command: str | None = None,
|
|
231
|
+
dev_command: str | None = None,
|
|
232
|
+
) -> str:
|
|
233
|
+
"""Render a PR evidence workflow: verify → JSON/HTML artifacts → optional GitHub integrations.
|
|
234
|
+
|
|
235
|
+
Expects ``GITHUB_TOKEN`` (for Checks/comments), ``GITHUB_REPOSITORY``, ``GITHUB_SHA``,
|
|
236
|
+
and ``GITHUB_PR_NUMBER`` (or ``PR_NUMBER``) when posting PR comments. The Check
|
|
237
|
+
conclusion is blocking-only (``--fail-on-blocking``); advisory gaps stay in artifacts.
|
|
238
|
+
"""
|
|
239
|
+
if install_command is None:
|
|
240
|
+
install_command = _default_install_command(project_root)
|
|
241
|
+
if dev_command is None:
|
|
242
|
+
dev_command = _default_dev_command(project_root)
|
|
243
|
+
|
|
244
|
+
stacks = detect_stacks(project_root)
|
|
245
|
+
steps: list[str] = [
|
|
246
|
+
" - name: Checkout",
|
|
247
|
+
" uses: actions/checkout@v4",
|
|
248
|
+
" with:",
|
|
249
|
+
" fetch-depth: 0",
|
|
250
|
+
]
|
|
251
|
+
if "python" in stacks or not stacks:
|
|
252
|
+
steps += _evidence_python_setup_steps(project_root, install_command=install_command)
|
|
253
|
+
steps += [
|
|
254
|
+
" - name: DevCouncil verify (scoped diff)",
|
|
255
|
+
" env:",
|
|
256
|
+
" # pull_request → base branch SHA; push → previous commit (github.event.before).",
|
|
257
|
+
" # New branch / first push: before is all zeros — skip verify (no diff base).",
|
|
258
|
+
" VERIFY_BASE: ${{ github.event.pull_request.base.sha || github.event.before }}",
|
|
259
|
+
" run: |",
|
|
260
|
+
' ZERO_SHA="0000000000000000000000000000000000000000"',
|
|
261
|
+
' if [ "$VERIFY_BASE" = "$ZERO_SHA" ]; then',
|
|
262
|
+
' VERIFY_BASE=""',
|
|
263
|
+
" fi",
|
|
264
|
+
' if [ -n "$VERIFY_BASE" ]; then',
|
|
265
|
+
f" {dev_command} check --verify --base \"$VERIFY_BASE\" --persist --project-root .",
|
|
266
|
+
" else",
|
|
267
|
+
' echo "::notice::Skipping DevCouncil verify — no diff base (first push / new branch). Push again or open a PR for scoped verify."',
|
|
268
|
+
" exit 0",
|
|
269
|
+
" fi",
|
|
270
|
+
" - name: Write evidence JSON artifact",
|
|
271
|
+
f" run: {dev_command} report --evidence-json .devcouncil/evidence.json --fail-on-blocking --project-root .",
|
|
272
|
+
" - name: Write evidence HTML artifact",
|
|
273
|
+
f" run: {dev_command} report --evidence-html .devcouncil/evidence.html --fail-on-blocking --project-root .",
|
|
274
|
+
" - name: Upload evidence artifacts",
|
|
275
|
+
" uses: actions/upload-artifact@v4",
|
|
276
|
+
" with:",
|
|
277
|
+
' name: devcouncil-evidence',
|
|
278
|
+
" path: |",
|
|
279
|
+
" .devcouncil/evidence.json",
|
|
280
|
+
" .devcouncil/evidence.html",
|
|
281
|
+
" if-no-files-found: error",
|
|
282
|
+
" - name: Post GitHub Check (blocking gaps only)",
|
|
283
|
+
" if: ${{ secrets.GITHUB_TOKEN != '' }}",
|
|
284
|
+
" env:",
|
|
285
|
+
" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}",
|
|
286
|
+
" GITHUB_REPOSITORY: ${{ github.repository }}",
|
|
287
|
+
" GITHUB_SHA: ${{ github.sha }}",
|
|
288
|
+
f" run: {dev_command} report --github --fail-on-blocking --project-root .",
|
|
289
|
+
" - name: Post PR comment (includes advisory gaps)",
|
|
290
|
+
" if: github.event_name == 'pull_request'",
|
|
291
|
+
" env:",
|
|
292
|
+
" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}",
|
|
293
|
+
" GITHUB_REPOSITORY: ${{ github.repository }}",
|
|
294
|
+
" GITHUB_SHA: ${{ github.sha }}",
|
|
295
|
+
" GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}",
|
|
296
|
+
f" run: {dev_command} report --github-pr-comment --fail-on-blocking --project-root .",
|
|
297
|
+
]
|
|
298
|
+
body = "\n".join(steps)
|
|
299
|
+
return (
|
|
300
|
+
"# DevCouncil evidence workflow — verify, export artifacts, optional GitHub Check/comment.\n"
|
|
301
|
+
"# Generated by DevCouncil; adjust install/dev commands for your stack.\n"
|
|
302
|
+
"# Check conclusion fails only on blocking gaps; advisory gaps appear in artifacts/comments.\n"
|
|
303
|
+
"name: DevCouncil Evidence\n"
|
|
304
|
+
"\n"
|
|
305
|
+
"permissions:\n"
|
|
306
|
+
" contents: read\n"
|
|
307
|
+
" checks: write\n"
|
|
308
|
+
" pull-requests: write\n"
|
|
309
|
+
"\n"
|
|
310
|
+
"on:\n"
|
|
311
|
+
" push:\n"
|
|
312
|
+
f' branches: ["{default_branch}"]\n'
|
|
313
|
+
" pull_request:\n"
|
|
314
|
+
f' branches: ["{default_branch}"]\n'
|
|
315
|
+
"\n"
|
|
316
|
+
"jobs:\n"
|
|
317
|
+
" evidence:\n"
|
|
318
|
+
" runs-on: ubuntu-latest\n"
|
|
319
|
+
" steps:\n"
|
|
320
|
+
f"{body}\n"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
149
324
|
def scaffold_ci(project_root: Path, force: bool = False) -> Path | None:
|
|
150
325
|
"""Write the starter workflow. Returns the path, or None if one already exists.
|
|
151
326
|
|
|
@@ -163,3 +338,24 @@ def scaffold_ci(project_root: Path, force: bool = False) -> Path | None:
|
|
|
163
338
|
render_workflow(project_root, default_branch, config), encoding="utf-8"
|
|
164
339
|
)
|
|
165
340
|
return target
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def scaffold_evidence_ci(project_root: Path, force: bool = False) -> Path | None:
|
|
344
|
+
"""Write the evidence export workflow. Returns the path, or None if one already exists."""
|
|
345
|
+
project_root = project_root.resolve()
|
|
346
|
+
target = project_root / EVIDENCE_WORKFLOW_RELPATH
|
|
347
|
+
if target.exists() and not force:
|
|
348
|
+
return None
|
|
349
|
+
config = load_config(project_root)
|
|
350
|
+
default_branch = config.project.default_branch or "main"
|
|
351
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
352
|
+
target.write_text(
|
|
353
|
+
render_evidence_workflow(
|
|
354
|
+
project_root,
|
|
355
|
+
default_branch,
|
|
356
|
+
install_command=_default_install_command(project_root),
|
|
357
|
+
dev_command=_default_dev_command(project_root),
|
|
358
|
+
),
|
|
359
|
+
encoding="utf-8",
|
|
360
|
+
)
|
|
361
|
+
return target
|
|
@@ -10,7 +10,6 @@ GITIGNORE_SECTIONS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
|
10
10
|
".devcouncil/*",
|
|
11
11
|
"!.devcouncil/",
|
|
12
12
|
"!.devcouncil/config.yaml",
|
|
13
|
-
"!.devcouncil/graphify.yaml",
|
|
14
13
|
),
|
|
15
14
|
),
|
|
16
15
|
(
|
|
@@ -28,7 +27,7 @@ GITIGNORE_SECTIONS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
|
28
27
|
".conducor/",
|
|
29
28
|
".antigravity/",
|
|
30
29
|
".warp/",
|
|
31
|
-
".
|
|
30
|
+
".grok/",
|
|
32
31
|
),
|
|
33
32
|
),
|
|
34
33
|
(
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Reviewer-facing export of the requirement→task→diff→evidence graph.
|
|
2
|
+
|
|
3
|
+
This is a pure serializer over an already-loaded :class:`ArtifactGraph` (no new
|
|
4
|
+
analysis): it flattens what the standard report loads into a single JSON document a
|
|
5
|
+
PR reviewer can read — or attach as a CI artifact — without installing DevCouncil.
|
|
6
|
+
Written by ``dev report --evidence-json PATH``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import Any, Dict, List
|
|
12
|
+
|
|
13
|
+
from devcouncil.artifacts.graph import ArtifactGraph
|
|
14
|
+
from devcouncil.reporting.verdict import classify_verdict
|
|
15
|
+
|
|
16
|
+
_SEVERITY_RANK = {"low": 0, "medium": 1, "high": 2, "critical": 3}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EvidenceExportGenerator:
|
|
20
|
+
"""Serializes the artifact graph as a single reviewer-readable JSON file."""
|
|
21
|
+
|
|
22
|
+
FORMAT = "devcouncil-evidence-export"
|
|
23
|
+
VERSION = 1
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def generate(graph: ArtifactGraph, live_review: dict | None = None, wiki_refresh: dict | None = None) -> str:
|
|
27
|
+
summary = graph.coverage_summary()
|
|
28
|
+
live_blockers = len((live_review or {}).get("blocking_cards", []))
|
|
29
|
+
|
|
30
|
+
verdict, incomplete_kind = classify_verdict(graph, live_blockers=live_blockers)
|
|
31
|
+
|
|
32
|
+
# Index evidence and diffs once so requirements/tasks can nest their own slices.
|
|
33
|
+
evidence_by_ac: Dict[str, List[Dict[str, Any]]] = {}
|
|
34
|
+
for ev in graph.test_evidence:
|
|
35
|
+
evidence_by_ac.setdefault(ev.acceptance_criterion_id, []).append(
|
|
36
|
+
{
|
|
37
|
+
"requirement_id": ev.requirement_id,
|
|
38
|
+
"command": ev.command,
|
|
39
|
+
"status": ev.status, # passed | failed | not_run
|
|
40
|
+
"mode": getattr(ev, "mode", "") or "unspecified",
|
|
41
|
+
"summary": ev.evidence_summary,
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
diffs_by_task: Dict[str, List[Dict[str, Any]]] = {}
|
|
46
|
+
for de in graph.diff_evidence:
|
|
47
|
+
diffs_by_task.setdefault(de.task_id, []).append(
|
|
48
|
+
{
|
|
49
|
+
"changed_files": de.changed_files,
|
|
50
|
+
"added_files": de.added_files,
|
|
51
|
+
"deleted_files": de.deleted_files,
|
|
52
|
+
"diff_summary": de.diff_summary,
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
tasks_by_requirement: Dict[str, List[str]] = {}
|
|
57
|
+
for task in graph.tasks.values():
|
|
58
|
+
for req_id in task.requirement_ids:
|
|
59
|
+
tasks_by_requirement.setdefault(req_id, []).append(task.id)
|
|
60
|
+
|
|
61
|
+
unproven_ac_ids = {ac.id for _, ac in graph.acceptance_criteria_without_evidence()}
|
|
62
|
+
|
|
63
|
+
requirements = [
|
|
64
|
+
{
|
|
65
|
+
"id": req.id,
|
|
66
|
+
"title": req.title,
|
|
67
|
+
"description": req.description,
|
|
68
|
+
"priority": req.priority,
|
|
69
|
+
"task_ids": sorted(tasks_by_requirement.get(req.id, [])),
|
|
70
|
+
"acceptance_criteria": [
|
|
71
|
+
{
|
|
72
|
+
"id": ac.id,
|
|
73
|
+
"description": ac.description,
|
|
74
|
+
"verification_method": ac.verification_method,
|
|
75
|
+
"required": ac.required,
|
|
76
|
+
"proven": ac.id not in unproven_ac_ids,
|
|
77
|
+
"evidence": evidence_by_ac.get(ac.id, []),
|
|
78
|
+
}
|
|
79
|
+
for ac in req.acceptance_criteria
|
|
80
|
+
],
|
|
81
|
+
}
|
|
82
|
+
for req in sorted(graph.requirements.values(), key=lambda r: r.id)
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
tasks = [
|
|
86
|
+
{
|
|
87
|
+
"id": task.id,
|
|
88
|
+
"title": task.title,
|
|
89
|
+
"status": task.status,
|
|
90
|
+
"requirement_ids": task.requirement_ids,
|
|
91
|
+
"acceptance_criterion_ids": task.acceptance_criterion_ids,
|
|
92
|
+
"diffs": diffs_by_task.get(task.id, []),
|
|
93
|
+
}
|
|
94
|
+
for task in sorted(graph.tasks.values(), key=lambda t: t.id)
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
# All gaps (each carries its own ``blocking`` flag), blocking-first then by
|
|
98
|
+
# severity so a reviewer reads the show-stoppers before the advisories.
|
|
99
|
+
gaps = [
|
|
100
|
+
gap.model_dump()
|
|
101
|
+
for gap in sorted(
|
|
102
|
+
graph.gaps.values(),
|
|
103
|
+
key=lambda g: (not g.blocking, -_SEVERITY_RANK.get(g.severity, 0), g.id),
|
|
104
|
+
)
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
report: Dict[str, Any] = {
|
|
108
|
+
"format": EvidenceExportGenerator.FORMAT,
|
|
109
|
+
"version": EvidenceExportGenerator.VERSION,
|
|
110
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
111
|
+
"verdict": verdict,
|
|
112
|
+
"coverage_summary": summary,
|
|
113
|
+
"requirements": requirements,
|
|
114
|
+
"tasks": tasks,
|
|
115
|
+
"gaps": gaps,
|
|
116
|
+
}
|
|
117
|
+
if incomplete_kind is not None:
|
|
118
|
+
report["incomplete_kind"] = incomplete_kind
|
|
119
|
+
if live_review is not None:
|
|
120
|
+
report["live_review"] = live_review
|
|
121
|
+
if wiki_refresh is not None:
|
|
122
|
+
report["wiki_refresh"] = wiki_refresh
|
|
123
|
+
|
|
124
|
+
return json.dumps(report, indent=2)
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Self-contained HTML renderer for the requirement→task→diff→evidence graph.
|
|
2
|
+
|
|
3
|
+
Companion to :mod:`devcouncil.reporting.evidence_export` — same reviewer-facing
|
|
4
|
+
content, browsable in CI artifact previews without installing DevCouncil.
|
|
5
|
+
Written by ``dev report --evidence-html PATH``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import html
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from devcouncil.artifacts.graph import ArtifactGraph
|
|
15
|
+
from devcouncil.reporting.evidence_export import EvidenceExportGenerator
|
|
16
|
+
|
|
17
|
+
_STYLE = """
|
|
18
|
+
body{font:16px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
|
19
|
+
max-width:64rem;margin:2rem auto;padding:0 1rem;color:#1a1a1a;background:#fff}
|
|
20
|
+
h1,h2,h3{line-height:1.25}a{color:#0b66c3}table{border-collapse:collapse;width:100%;margin:1rem 0}
|
|
21
|
+
th,td{border:1px solid #ddd;padding:.45rem .6rem;text-align:left;vertical-align:top}
|
|
22
|
+
th{background:#f6f8fa}.meta{color:#555;font-size:.92em;margin:.5rem 0}
|
|
23
|
+
.verdict-passed{color:#116329}.verdict-incomplete{color:#9a6700}.verdict-blocked{color:#cf222e}
|
|
24
|
+
.badge{display:inline-block;border-radius:999px;padding:.1em .55em;font-size:.78em;font-weight:600}
|
|
25
|
+
.badge-pass{background:#dafbe1;color:#116329}.badge-fail{background:#ffebe9;color:#cf222e}
|
|
26
|
+
.badge-warn{background:#fff8c5;color:#9a6700}.badge-advisory{background:#eef3fb;color:#0b66c3}
|
|
27
|
+
.gap-blocking{font-weight:600}.section{margin:2rem 0 1rem}
|
|
28
|
+
ul.compact{margin:.3rem 0;padding-left:1.2rem}code{background:#f3f3f3;padding:.1em .3em;border-radius:3px}
|
|
29
|
+
""".strip()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _esc(text: object) -> str:
|
|
33
|
+
return html.escape(str(text), quote=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _verdict_class(verdict: str) -> str:
|
|
37
|
+
return {
|
|
38
|
+
"passed": "verdict-passed",
|
|
39
|
+
"incomplete": "verdict-incomplete",
|
|
40
|
+
"blocked": "verdict-blocked",
|
|
41
|
+
}.get(verdict, "")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _status_badge(status: str) -> str:
|
|
45
|
+
cls = {
|
|
46
|
+
"passed": "badge-pass",
|
|
47
|
+
"failed": "badge-fail",
|
|
48
|
+
"not_run": "badge-warn",
|
|
49
|
+
}.get(status, "badge-warn")
|
|
50
|
+
return f'<span class="badge {cls}">{_esc(status)}</span>'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _proven_badge(proven: bool) -> str:
|
|
54
|
+
if proven:
|
|
55
|
+
return '<span class="badge badge-pass">proven</span>'
|
|
56
|
+
return '<span class="badge badge-warn">unproven</span>'
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _render_gaps(gaps: list[dict[str, Any]]) -> str:
|
|
60
|
+
if not gaps:
|
|
61
|
+
return "<p>No gaps recorded.</p>"
|
|
62
|
+
rows: list[str] = []
|
|
63
|
+
for gap in gaps:
|
|
64
|
+
blocking = gap.get("blocking", False)
|
|
65
|
+
severity = gap.get("severity", "")
|
|
66
|
+
row_cls = "gap-blocking" if blocking else ""
|
|
67
|
+
kind = "blocking" if blocking else "advisory"
|
|
68
|
+
badge = "badge-fail" if blocking else "badge-advisory"
|
|
69
|
+
rows.append(
|
|
70
|
+
"<tr>"
|
|
71
|
+
f'<td class="{row_cls}"><span class="badge {badge}">{_esc(kind)}</span></td>'
|
|
72
|
+
f"<td>{_esc(gap.get('id', ''))}</td>"
|
|
73
|
+
f"<td>{_esc(severity)}</td>"
|
|
74
|
+
f"<td>{_esc(gap.get('description', ''))}</td>"
|
|
75
|
+
f"<td>{_esc(gap.get('task_id', '') or '—')}</td>"
|
|
76
|
+
"</tr>"
|
|
77
|
+
)
|
|
78
|
+
return (
|
|
79
|
+
"<table><thead><tr><th>Kind</th><th>ID</th><th>Severity</th>"
|
|
80
|
+
"<th>Description</th><th>Task</th></tr></thead><tbody>"
|
|
81
|
+
+ "".join(rows)
|
|
82
|
+
+ "</tbody></table>"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _render_requirements(requirements: list[dict[str, Any]]) -> str:
|
|
87
|
+
parts: list[str] = []
|
|
88
|
+
for req in requirements:
|
|
89
|
+
parts.append(f"<h3 id=\"{_esc(req.get('id', ''))}\">{_esc(req.get('id', ''))}: {_esc(req.get('title', ''))}</h3>")
|
|
90
|
+
parts.append(f"<p class=\"meta\">{_esc(req.get('description', ''))}</p>")
|
|
91
|
+
task_ids = req.get("task_ids") or []
|
|
92
|
+
if task_ids:
|
|
93
|
+
links = ", ".join(f'<a href="#task-{_esc(tid)}">{_esc(tid)}</a>' for tid in task_ids)
|
|
94
|
+
parts.append(f"<p class=\"meta\">Tasks: {links}</p>")
|
|
95
|
+
parts.append(
|
|
96
|
+
"<table><thead><tr><th>AC</th><th>Description</th><th>Method</th>"
|
|
97
|
+
"<th>Status</th><th>Evidence</th></tr></thead><tbody>"
|
|
98
|
+
)
|
|
99
|
+
for ac in req.get("acceptance_criteria") or []:
|
|
100
|
+
evidence_items = ac.get("evidence") or []
|
|
101
|
+
if evidence_items:
|
|
102
|
+
ev_html = "<ul class=\"compact\">" + "".join(
|
|
103
|
+
"<li>"
|
|
104
|
+
f"{_status_badge(ev.get('status', ''))} "
|
|
105
|
+
f"<code>{_esc(ev.get('command', ''))}</code> — {_esc(ev.get('summary', ''))}"
|
|
106
|
+
"</li>"
|
|
107
|
+
for ev in evidence_items
|
|
108
|
+
) + "</ul>"
|
|
109
|
+
else:
|
|
110
|
+
ev_html = "<em>none</em>"
|
|
111
|
+
parts.append(
|
|
112
|
+
"<tr>"
|
|
113
|
+
f"<td>{_esc(ac.get('id', ''))}</td>"
|
|
114
|
+
f"<td>{_esc(ac.get('description', ''))}</td>"
|
|
115
|
+
f"<td>{_esc(ac.get('verification_method', ''))}</td>"
|
|
116
|
+
f"<td>{_proven_badge(bool(ac.get('proven')))}</td>"
|
|
117
|
+
f"<td>{ev_html}</td>"
|
|
118
|
+
"</tr>"
|
|
119
|
+
)
|
|
120
|
+
parts.append("</tbody></table>")
|
|
121
|
+
return "\n".join(parts)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _render_tasks(tasks: list[dict[str, Any]]) -> str:
|
|
125
|
+
parts: list[str] = []
|
|
126
|
+
for task in tasks:
|
|
127
|
+
tid = task.get("id", "")
|
|
128
|
+
parts.append(
|
|
129
|
+
f"<h3 id=\"task-{_esc(tid)}\">{_esc(tid)}: {_esc(task.get('title', ''))}</h3>"
|
|
130
|
+
)
|
|
131
|
+
parts.append(
|
|
132
|
+
f"<p class=\"meta\">Status: {_esc(task.get('status', ''))} · "
|
|
133
|
+
f"Requirements: {_esc(', '.join(task.get('requirement_ids') or []))}</p>"
|
|
134
|
+
)
|
|
135
|
+
diffs = task.get("diffs") or []
|
|
136
|
+
if not diffs:
|
|
137
|
+
parts.append("<p class=\"meta\">No diff evidence linked.</p>")
|
|
138
|
+
continue
|
|
139
|
+
parts.append("<ul class=\"compact\">")
|
|
140
|
+
for diff in diffs:
|
|
141
|
+
changed = diff.get("changed_files") or []
|
|
142
|
+
added = diff.get("added_files") or []
|
|
143
|
+
deleted = diff.get("deleted_files") or []
|
|
144
|
+
parts.append("<li>")
|
|
145
|
+
if changed:
|
|
146
|
+
parts.append(f"Changed: {_esc(', '.join(changed))}<br>")
|
|
147
|
+
if added:
|
|
148
|
+
parts.append(f"Added: {_esc(', '.join(added))}<br>")
|
|
149
|
+
if deleted:
|
|
150
|
+
parts.append(f"Deleted: {_esc(', '.join(deleted))}<br>")
|
|
151
|
+
parts.append(_esc(diff.get("diff_summary", "")))
|
|
152
|
+
parts.append("</li>")
|
|
153
|
+
parts.append("</ul>")
|
|
154
|
+
return "\n".join(parts)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class EvidenceHtmlGenerator:
|
|
158
|
+
"""Renders the artifact graph as a single self-contained HTML document."""
|
|
159
|
+
|
|
160
|
+
FORMAT = "devcouncil-evidence-html"
|
|
161
|
+
VERSION = 1
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def generate(
|
|
165
|
+
cls,
|
|
166
|
+
graph: ArtifactGraph,
|
|
167
|
+
live_review: dict | None = None,
|
|
168
|
+
wiki_refresh: dict | None = None,
|
|
169
|
+
) -> str:
|
|
170
|
+
payload = EvidenceExportGenerator.generate(
|
|
171
|
+
graph, live_review=live_review, wiki_refresh=wiki_refresh
|
|
172
|
+
)
|
|
173
|
+
import json
|
|
174
|
+
|
|
175
|
+
data = json.loads(payload)
|
|
176
|
+
verdict = data.get("verdict", "incomplete")
|
|
177
|
+
summary = data.get("coverage_summary") or {}
|
|
178
|
+
generated_at = data.get("generated_at") or datetime.now(timezone.utc).isoformat()
|
|
179
|
+
|
|
180
|
+
body = [
|
|
181
|
+
"<!DOCTYPE html>",
|
|
182
|
+
'<html lang="en">',
|
|
183
|
+
"<head>",
|
|
184
|
+
'<meta charset="utf-8">',
|
|
185
|
+
"<title>DevCouncil Evidence Report</title>",
|
|
186
|
+
f"<style>{_STYLE}</style>",
|
|
187
|
+
"</head>",
|
|
188
|
+
"<body>",
|
|
189
|
+
"<h1>DevCouncil Evidence Report</h1>",
|
|
190
|
+
f'<p class="meta">Generated {_esc(generated_at)} · format {_esc(cls.FORMAT)} v{cls.VERSION}</p>',
|
|
191
|
+
f'<p class="{_verdict_class(verdict)}"><strong>Verdict:</strong> {_esc(verdict)}</p>',
|
|
192
|
+
"<p class=\"meta\">"
|
|
193
|
+
f"Requirements: {_esc(summary.get('total_requirements', 0))} · "
|
|
194
|
+
f"Tasks: {_esc(summary.get('total_tasks', 0))} · "
|
|
195
|
+
f"Blocking gaps: {_esc(summary.get('blocking_gaps', 0))} · "
|
|
196
|
+
f"AC without evidence: {_esc(summary.get('ac_without_evidence', 0))}"
|
|
197
|
+
"</p>",
|
|
198
|
+
'<div class="section"><h2>Gaps</h2>',
|
|
199
|
+
"<p class=\"meta\">GitHub Checks fail only on blocking gaps; advisory gaps appear here for reviewers.</p>",
|
|
200
|
+
_render_gaps(data.get("gaps") or []),
|
|
201
|
+
"</div>",
|
|
202
|
+
'<div class="section"><h2>Requirements & Acceptance Criteria</h2>',
|
|
203
|
+
_render_requirements(data.get("requirements") or []),
|
|
204
|
+
"</div>",
|
|
205
|
+
'<div class="section"><h2>Tasks & Diffs</h2>',
|
|
206
|
+
_render_tasks(data.get("tasks") or []),
|
|
207
|
+
"</div>",
|
|
208
|
+
"</body></html>",
|
|
209
|
+
]
|
|
210
|
+
return "\n".join(body)
|
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from devcouncil.artifacts.coverage import (
|
|
5
|
+
acceptance_criteria_evidence_matrix,
|
|
6
|
+
requirement_task_matrix,
|
|
7
|
+
)
|
|
2
8
|
from devcouncil.artifacts.graph import ArtifactGraph
|
|
9
|
+
from devcouncil.reporting.verdict import classify_verdict
|
|
3
10
|
|
|
4
11
|
class JsonReportGenerator:
|
|
5
12
|
"""Generates a JSON evidence report."""
|
|
6
13
|
|
|
7
14
|
@staticmethod
|
|
8
|
-
def generate(graph: ArtifactGraph, live_review: dict | None = None) -> str:
|
|
15
|
+
def generate(graph: ArtifactGraph, live_review: dict | None = None, wiki_refresh: dict | None = None) -> str:
|
|
9
16
|
summary = graph.coverage_summary()
|
|
10
17
|
live_blockers = len((live_review or {}).get("blocking_cards", []))
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
# blocked - positive evidence of a problem.
|
|
14
|
-
# incomplete - nothing failing, but not every AC has passing evidence.
|
|
15
|
-
# passed - no blocking gaps and every AC proven.
|
|
16
|
-
if summary["blocking_gaps"] > 0 or live_blockers > 0:
|
|
17
|
-
verdict = "blocked"
|
|
18
|
-
elif summary["ac_without_evidence"] > 0:
|
|
19
|
-
verdict = "incomplete"
|
|
20
|
-
else:
|
|
21
|
-
verdict = "passed"
|
|
19
|
+
verdict, incomplete_kind = classify_verdict(graph, live_blockers=live_blockers)
|
|
22
20
|
# Proof-rigor breakdown: of the criteria that ARE proven, how were they proven?
|
|
23
21
|
# ``compiled``/``vote`` are precise per-criterion checks (trustworthy); ``coarse``
|
|
24
22
|
# means proven only by a passing acceptance-capable command (weak). Surfacing this
|
|
@@ -29,13 +27,19 @@ class JsonReportGenerator:
|
|
|
29
27
|
if getattr(ev, "status", "") == "passed":
|
|
30
28
|
key = getattr(ev, "mode", "") or "unspecified"
|
|
31
29
|
proof_modes[key] = proof_modes.get(key, 0) + 1
|
|
32
|
-
report = {
|
|
30
|
+
report: dict[str, Any] = {
|
|
33
31
|
"verdict": verdict,
|
|
34
32
|
"coverage_summary": summary,
|
|
35
33
|
"proof_modes": proof_modes,
|
|
34
|
+
"requirement_task_matrix": requirement_task_matrix(graph),
|
|
35
|
+
"acceptance_criteria_evidence_matrix": acceptance_criteria_evidence_matrix(graph),
|
|
36
36
|
"blocking_gaps": [g.model_dump() for g in graph.blocking_gaps()]
|
|
37
37
|
}
|
|
38
|
+
if incomplete_kind is not None:
|
|
39
|
+
report["incomplete_kind"] = incomplete_kind
|
|
38
40
|
if live_review is not None:
|
|
39
41
|
report["live_review"] = live_review
|
|
42
|
+
if wiki_refresh is not None:
|
|
43
|
+
report["wiki_refresh"] = wiki_refresh
|
|
40
44
|
|
|
41
45
|
return json.dumps(report, indent=2)
|