devcouncil 0.3.1 → 0.4.1
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 +92 -33
- 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 +56 -40
- 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 +964 -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 +281 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -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 +87 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +103 -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 +69 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -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 +53 -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 +325 -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/summary.py +21 -3
- 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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Compiled per-criterion acceptance check orchestration extracted from Verifier."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Awaitable, Callable, Dict, List, Tuple
|
|
8
|
+
|
|
9
|
+
from devcouncil.domain.evidence import CommandResult
|
|
10
|
+
from devcouncil.domain.gap import Gap
|
|
11
|
+
from devcouncil.domain.requirement import Requirement
|
|
12
|
+
from devcouncil.domain.task import Task
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class CompiledAcceptanceResult:
|
|
19
|
+
gaps: List[Gap] = field(default_factory=list)
|
|
20
|
+
compiled_pass: Dict[str, bool] = field(default_factory=dict)
|
|
21
|
+
compiled_cmds_by_ac: Dict[str, List[str]] = field(default_factory=dict)
|
|
22
|
+
failing_results_by_ac: Dict[str, List[CommandResult]] = field(default_factory=dict)
|
|
23
|
+
compiled_vote: Dict[str, Tuple[int, int, bool]] = field(default_factory=dict)
|
|
24
|
+
inconclusive_acs: set[str] = field(default_factory=set)
|
|
25
|
+
command_results: List[CommandResult] = field(default_factory=list)
|
|
26
|
+
evidence: List[Any] = field(default_factory=list)
|
|
27
|
+
genuine_failure: bool = False
|
|
28
|
+
had_unrunnable: bool = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def run_compiled_acceptance_checks(
|
|
32
|
+
*,
|
|
33
|
+
task: Task,
|
|
34
|
+
requirements: List[Requirement],
|
|
35
|
+
compile_future: Any | None,
|
|
36
|
+
diff_content: str,
|
|
37
|
+
ac_repair_attempts: int,
|
|
38
|
+
acceptance_compiler: Any | None,
|
|
39
|
+
command_applicable: Callable[[str], Tuple[bool, str]],
|
|
40
|
+
run_command: Callable[[str], CommandResult],
|
|
41
|
+
command_is_malformed: Callable[[CommandResult], bool],
|
|
42
|
+
failure_location: Callable[[CommandResult], Tuple[str | None, int | None]],
|
|
43
|
+
next_gap_id: Callable[[str, str], str],
|
|
44
|
+
repair_command: Callable[
|
|
45
|
+
[str, str, str, str, str],
|
|
46
|
+
Awaitable[str | None],
|
|
47
|
+
] | None = None,
|
|
48
|
+
had_unrunnable: bool = False,
|
|
49
|
+
genuine_failure: bool = False,
|
|
50
|
+
) -> CompiledAcceptanceResult:
|
|
51
|
+
"""Run DevCouncil-owned per-criterion checks from the acceptance compiler."""
|
|
52
|
+
result = CompiledAcceptanceResult(
|
|
53
|
+
had_unrunnable=had_unrunnable,
|
|
54
|
+
genuine_failure=genuine_failure,
|
|
55
|
+
)
|
|
56
|
+
if compile_future is None:
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
compiled = await compile_future
|
|
61
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
62
|
+
logger.warning("Acceptance compiler failed for %s: %s", task.id, exc)
|
|
63
|
+
compiled = {}
|
|
64
|
+
|
|
65
|
+
ac_meta = {ac.id: ac for req in requirements for ac in req.acceptance_criteria}
|
|
66
|
+
for ac_id, raw_cmds in compiled.items():
|
|
67
|
+
candidates = [c for c in raw_cmds if command_applicable(c)[0]]
|
|
68
|
+
result.compiled_cmds_by_ac[ac_id] = list(candidates)
|
|
69
|
+
if not candidates:
|
|
70
|
+
result.had_unrunnable = True
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
passes = 0
|
|
74
|
+
genuine_fails = 0
|
|
75
|
+
repaired = False
|
|
76
|
+
fail_results: List[Tuple[str, CommandResult]] = []
|
|
77
|
+
for cmd in candidates:
|
|
78
|
+
cmd_result = run_command(cmd)
|
|
79
|
+
result.command_results.append(cmd_result)
|
|
80
|
+
result.evidence.append(cmd_result)
|
|
81
|
+
attempts = 0
|
|
82
|
+
while (
|
|
83
|
+
cmd_result.exit_code != 0
|
|
84
|
+
and command_is_malformed(cmd_result)
|
|
85
|
+
and attempts < ac_repair_attempts
|
|
86
|
+
and repair_command is not None
|
|
87
|
+
):
|
|
88
|
+
attempts += 1
|
|
89
|
+
ac_desc = ac_meta[ac_id].description if ac_id in ac_meta else ac_id
|
|
90
|
+
try:
|
|
91
|
+
fixed = await repair_command(
|
|
92
|
+
ac_id, ac_desc, cmd, cmd_result.summary[:800], diff_content
|
|
93
|
+
)
|
|
94
|
+
except Exception:
|
|
95
|
+
fixed = None
|
|
96
|
+
if not fixed or not command_applicable(fixed)[0]:
|
|
97
|
+
break
|
|
98
|
+
cmd = fixed
|
|
99
|
+
result.compiled_cmds_by_ac[ac_id].append(cmd)
|
|
100
|
+
cmd_result = run_command(cmd)
|
|
101
|
+
result.command_results.append(cmd_result)
|
|
102
|
+
result.evidence.append(cmd_result)
|
|
103
|
+
if cmd_result.exit_code == 0:
|
|
104
|
+
passes += 1
|
|
105
|
+
if attempts > 0:
|
|
106
|
+
repaired = True
|
|
107
|
+
elif command_is_malformed(cmd_result):
|
|
108
|
+
result.had_unrunnable = True
|
|
109
|
+
result.failing_results_by_ac.setdefault(ac_id, []).append(cmd_result)
|
|
110
|
+
else:
|
|
111
|
+
genuine_fails += 1
|
|
112
|
+
fail_results.append((cmd, cmd_result))
|
|
113
|
+
result.failing_results_by_ac.setdefault(ac_id, []).append(cmd_result)
|
|
114
|
+
|
|
115
|
+
decisive = passes + genuine_fails
|
|
116
|
+
if decisive == 0:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
ac_proven = passes > genuine_fails
|
|
120
|
+
result.compiled_pass[ac_id] = ac_proven
|
|
121
|
+
if ac_proven:
|
|
122
|
+
result.compiled_vote[ac_id] = (passes, decisive, repaired)
|
|
123
|
+
continue
|
|
124
|
+
if passes == 0 and genuine_fails > 0:
|
|
125
|
+
result.genuine_failure = True
|
|
126
|
+
cmd, cmd_result = fail_results[0]
|
|
127
|
+
fail_file, fail_line = failure_location(cmd_result)
|
|
128
|
+
agree = (
|
|
129
|
+
f" {genuine_fails}/{decisive} independent checks agreed it fails."
|
|
130
|
+
if decisive > 1 else ""
|
|
131
|
+
)
|
|
132
|
+
result.gaps.append(Gap(
|
|
133
|
+
id=next_gap_id(task.id, f"ACCHK-{ac_id}"),
|
|
134
|
+
severity="high",
|
|
135
|
+
gap_type="test_failed",
|
|
136
|
+
task_id=task.id,
|
|
137
|
+
description=(
|
|
138
|
+
f"Acceptance check for {ac_id} failed: '{cmd}' "
|
|
139
|
+
f"(exit {cmd_result.exit_code}).{agree}"
|
|
140
|
+
),
|
|
141
|
+
evidence=[cmd_result.summary[:500]],
|
|
142
|
+
recommended_fix=f"Fix the implementation so acceptance criterion {ac_id} holds.",
|
|
143
|
+
blocking=True,
|
|
144
|
+
acceptance_criterion_id=ac_id,
|
|
145
|
+
suggested_command=cmd,
|
|
146
|
+
file=fail_file,
|
|
147
|
+
line=fail_line,
|
|
148
|
+
stdout_path=cmd_result.stdout_path or None,
|
|
149
|
+
stderr_path=cmd_result.stderr_path or None,
|
|
150
|
+
))
|
|
151
|
+
elif passes > 0 and genuine_fails > 0:
|
|
152
|
+
result.inconclusive_acs.add(ac_id)
|
|
153
|
+
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def promote_demoted_failures_when_compiler_incomplete(
|
|
158
|
+
*,
|
|
159
|
+
task: Task,
|
|
160
|
+
compiler_active: bool,
|
|
161
|
+
compiled_cmds_by_ac: Dict[str, List[str]],
|
|
162
|
+
demoted_failures: List[Gap],
|
|
163
|
+
genuine_failure: bool,
|
|
164
|
+
) -> bool:
|
|
165
|
+
"""Re-promote demoted planner test failures when compile coverage is partial."""
|
|
166
|
+
compiler_covered_all = bool(task.acceptance_criterion_ids) and all(
|
|
167
|
+
compiled_cmds_by_ac.get(ac_id) for ac_id in task.acceptance_criterion_ids
|
|
168
|
+
)
|
|
169
|
+
if compiler_active and not compiler_covered_all and demoted_failures:
|
|
170
|
+
for gap in demoted_failures:
|
|
171
|
+
gap.blocking = True
|
|
172
|
+
gap.severity = "high"
|
|
173
|
+
genuine_failure = True
|
|
174
|
+
logger.info(
|
|
175
|
+
"Re-promoted demoted test failure %s to blocking: acceptance compiler "
|
|
176
|
+
"did not produce a check for every criterion of task %s.",
|
|
177
|
+
gap.id, task.id,
|
|
178
|
+
)
|
|
179
|
+
return genuine_failure
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Corpus freshness gate for tasks touching docs/corpus paths."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Callable, List, Optional
|
|
8
|
+
|
|
9
|
+
from devcouncil.domain.gap import Gap
|
|
10
|
+
from devcouncil.domain.task import Task
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _norm(path: str) -> str:
|
|
16
|
+
return path.replace("\\", "/").lstrip("./")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _task_touches_corpus(
|
|
20
|
+
*,
|
|
21
|
+
task: Task,
|
|
22
|
+
changed_files: List[str],
|
|
23
|
+
corpus_paths: List[str],
|
|
24
|
+
) -> bool:
|
|
25
|
+
corpus_norm = {_norm(p) for p in corpus_paths}
|
|
26
|
+
for pf in task.planned_files:
|
|
27
|
+
rel = _norm(pf.path)
|
|
28
|
+
if rel in corpus_norm or any(rel.startswith(c + "/") for c in corpus_norm):
|
|
29
|
+
return True
|
|
30
|
+
if rel.endswith(".md") or rel.startswith("docs/"):
|
|
31
|
+
return True
|
|
32
|
+
for path in changed_files:
|
|
33
|
+
rel = _norm(path)
|
|
34
|
+
if rel in corpus_norm or any(rel.startswith(c + "/") for c in corpus_norm):
|
|
35
|
+
return True
|
|
36
|
+
if rel.endswith(".md") or rel.startswith("docs/"):
|
|
37
|
+
return True
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _corpus_is_stale(project_root: Path) -> tuple[bool, str]:
|
|
42
|
+
try:
|
|
43
|
+
from devcouncil.indexing.wiring import corpus_graph_path, load_corpus_settings
|
|
44
|
+
from devcouncil.indexing.repo_mapper import RepoMapper
|
|
45
|
+
|
|
46
|
+
settings = load_corpus_settings(project_root)
|
|
47
|
+
if not settings.enabled:
|
|
48
|
+
return False, "corpus disabled"
|
|
49
|
+
graph_path = corpus_graph_path(project_root)
|
|
50
|
+
if not graph_path.is_file():
|
|
51
|
+
return True, "corpus graph missing"
|
|
52
|
+
mapper = RepoMapper(project_root)
|
|
53
|
+
map_path = project_root / ".devcouncil" / "repo_map.json"
|
|
54
|
+
if map_path.is_file():
|
|
55
|
+
from devcouncil.utils.json_persist import read_json
|
|
56
|
+
|
|
57
|
+
loaded = read_json(map_path)
|
|
58
|
+
if isinstance(loaded, dict) and mapper.map_is_stale(loaded):
|
|
59
|
+
return True, "repo map stale vs corpus fingerprint"
|
|
60
|
+
return False, "fresh"
|
|
61
|
+
except Exception as exc:
|
|
62
|
+
logger.debug("corpus stale check failed: %s", exc)
|
|
63
|
+
return True, "corpus check error"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def detect_corpus_stale_gaps(
|
|
67
|
+
*,
|
|
68
|
+
task: Task,
|
|
69
|
+
project_root: Path,
|
|
70
|
+
changed_files: List[str],
|
|
71
|
+
next_gap_id: Callable[[str, str], str],
|
|
72
|
+
corpus_stale_enabled: bool = True,
|
|
73
|
+
corpus_stale_blocking: bool = False,
|
|
74
|
+
corpus_paths: Optional[List[str]] = None,
|
|
75
|
+
) -> List[Gap]:
|
|
76
|
+
if not corpus_stale_enabled:
|
|
77
|
+
return []
|
|
78
|
+
try:
|
|
79
|
+
from devcouncil.indexing.wiring import load_corpus_settings
|
|
80
|
+
|
|
81
|
+
settings = load_corpus_settings(project_root)
|
|
82
|
+
paths = corpus_paths if corpus_paths is not None else list(settings.paths)
|
|
83
|
+
if not _task_touches_corpus(task=task, changed_files=changed_files, corpus_paths=paths):
|
|
84
|
+
return []
|
|
85
|
+
stale, reason = _corpus_is_stale(project_root)
|
|
86
|
+
if not stale:
|
|
87
|
+
return []
|
|
88
|
+
return [Gap(
|
|
89
|
+
id=next_gap_id(task.id, "CORPUSSTALE"),
|
|
90
|
+
severity="high" if corpus_stale_blocking else "medium",
|
|
91
|
+
gap_type="corpus_stale",
|
|
92
|
+
task_id=task.id,
|
|
93
|
+
description=(
|
|
94
|
+
f"Corpus index is stale or missing ({reason}). "
|
|
95
|
+
"Doc/corpus navigation and doc-code-ref gates may be wrong."
|
|
96
|
+
),
|
|
97
|
+
evidence=[".devcouncil/corpus/graph.json", reason],
|
|
98
|
+
recommended_fix="Run `dev map` (corpus rebuild) or enable auto_refresh_on_verify.",
|
|
99
|
+
blocking=corpus_stale_blocking,
|
|
100
|
+
suggested_command="dev map",
|
|
101
|
+
)]
|
|
102
|
+
except Exception:
|
|
103
|
+
logger.debug("detect_corpus_stale_gaps failed", exc_info=True)
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def refresh_corpus_on_verify_if_needed(project_root: Path) -> bool:
|
|
108
|
+
"""Best-effort corpus rebuild when enabled and stale."""
|
|
109
|
+
try:
|
|
110
|
+
from devcouncil.indexing.wiring import load_corpus_settings
|
|
111
|
+
|
|
112
|
+
settings = load_corpus_settings(project_root)
|
|
113
|
+
if not settings.enabled or not settings.auto_refresh_on_verify:
|
|
114
|
+
return False
|
|
115
|
+
stale, _ = _corpus_is_stale(project_root)
|
|
116
|
+
if not stale:
|
|
117
|
+
return False
|
|
118
|
+
from devcouncil.indexing.wiring import build_corpus
|
|
119
|
+
|
|
120
|
+
build_corpus(project_root)
|
|
121
|
+
return True
|
|
122
|
+
except Exception:
|
|
123
|
+
logger.debug("corpus verify refresh failed", exc_info=True)
|
|
124
|
+
return False
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Corpus verification gate facade (re-exports split gate modules)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from devcouncil.verification.checks.acceptance_corpus import detect_acceptance_corpus_gaps
|
|
6
|
+
from devcouncil.verification.checks.corpus_stale import detect_corpus_stale_gaps
|
|
7
|
+
from devcouncil.verification.checks.doc_code_ref import detect_doc_code_ref_gaps
|
|
8
|
+
|
|
9
|
+
__all__ = ["detect_acceptance_corpus_gaps", "detect_corpus_stale_gaps", "detect_doc_code_ref_gaps"]
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Dead-symbol gate: flag newly added public top-level symbols nothing references.
|
|
2
|
+
|
|
3
|
+
Diff-scoped via ``added_lines_by_file`` + AST/span intersection. Test references
|
|
4
|
+
and intent-text naming clear a symbol; files already flagged ``unwired_file`` are
|
|
5
|
+
skipped. Never raises.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Callable, List, Optional, Set, Tuple
|
|
15
|
+
|
|
16
|
+
from devcouncil.domain.gap import Gap
|
|
17
|
+
from devcouncil.domain.requirement import Requirement
|
|
18
|
+
from devcouncil.domain.task import Task
|
|
19
|
+
from devcouncil.indexing.wiring import (
|
|
20
|
+
ALLOW_UNWIRED,
|
|
21
|
+
decorator_names,
|
|
22
|
+
is_liveness_code_file,
|
|
23
|
+
is_private_symbol,
|
|
24
|
+
is_test_path,
|
|
25
|
+
is_vendored_path,
|
|
26
|
+
is_wiring_decorated,
|
|
27
|
+
iter_js_export_symbols,
|
|
28
|
+
parse_python_all_exports,
|
|
29
|
+
parse_python_reexport_names,
|
|
30
|
+
strip_js_comments,
|
|
31
|
+
strip_py_comments,
|
|
32
|
+
strip_string_literals,
|
|
33
|
+
)
|
|
34
|
+
from devcouncil.verification.checks.semantic_diff import task_intent_text
|
|
35
|
+
from devcouncil.verification.stub_detector import added_lines_by_file, task_allows_scaffolding
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
_IDENT_RE = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)\b")
|
|
40
|
+
_CODE_SUFFIXES = {".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"}
|
|
41
|
+
|
|
42
|
+
# (path, start_line, end_line, name)
|
|
43
|
+
_SymbolCand = Tuple[str, int, int, str]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _norm(path: str) -> str:
|
|
47
|
+
s = path.replace("\\", "/")
|
|
48
|
+
while s.startswith("./"):
|
|
49
|
+
s = s[2:]
|
|
50
|
+
return s
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _python_candidates(
|
|
54
|
+
project_root: Path,
|
|
55
|
+
path: str,
|
|
56
|
+
added_lines: Set[int],
|
|
57
|
+
) -> List[_SymbolCand]:
|
|
58
|
+
"""Return (path, start, end, name) for public top-level defs intersecting added lines."""
|
|
59
|
+
try:
|
|
60
|
+
source = (project_root / path).read_text(encoding="utf-8", errors="replace")
|
|
61
|
+
tree = ast.parse(source)
|
|
62
|
+
except Exception:
|
|
63
|
+
return []
|
|
64
|
+
all_exports = parse_python_all_exports(source)
|
|
65
|
+
reexports = parse_python_reexport_names(path, source)
|
|
66
|
+
protected = all_exports | reexports
|
|
67
|
+
out: List[_SymbolCand] = []
|
|
68
|
+
for node in tree.body:
|
|
69
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
70
|
+
continue
|
|
71
|
+
name = node.name
|
|
72
|
+
if is_private_symbol(name):
|
|
73
|
+
continue
|
|
74
|
+
# ``__all__`` / barrel re-exports = public API surface (parity with graph).
|
|
75
|
+
if name in protected:
|
|
76
|
+
continue
|
|
77
|
+
start = getattr(node, "lineno", None)
|
|
78
|
+
end = getattr(node, "end_lineno", start) or start
|
|
79
|
+
if start is None:
|
|
80
|
+
continue
|
|
81
|
+
span = set(range(start, (end or start) + 1))
|
|
82
|
+
if not span & added_lines:
|
|
83
|
+
continue
|
|
84
|
+
if is_wiring_decorated(decorator_names(node)):
|
|
85
|
+
continue
|
|
86
|
+
out.append((path, start, end or start, name))
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _js_definition_span(source: str, name: str) -> Optional[Tuple[int, int]]:
|
|
91
|
+
"""Best-effort start/end lines for a JS/TS binding named ``name``."""
|
|
92
|
+
patterns = (
|
|
93
|
+
rf"(?m)^\s*(?:export\s+)?(?:async\s+)?function\s+{re.escape(name)}\b",
|
|
94
|
+
rf"(?m)^\s*(?:export\s+)?class\s+{re.escape(name)}\b",
|
|
95
|
+
rf"(?m)^\s*(?:export\s+)?(?:const|let|var)\s+{re.escape(name)}\b",
|
|
96
|
+
)
|
|
97
|
+
for pat in patterns:
|
|
98
|
+
m = re.search(pat, source)
|
|
99
|
+
if m:
|
|
100
|
+
line = source[: m.start()].count("\n") + 1
|
|
101
|
+
# Single-line approx is enough for token outside-span checks.
|
|
102
|
+
return line, line
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _js_candidates(
|
|
107
|
+
project_root: Path,
|
|
108
|
+
path: str,
|
|
109
|
+
added_lines: Set[int],
|
|
110
|
+
) -> List[_SymbolCand]:
|
|
111
|
+
try:
|
|
112
|
+
source = (project_root / path).read_text(encoding="utf-8", errors="replace")
|
|
113
|
+
except OSError:
|
|
114
|
+
return []
|
|
115
|
+
out: List[_SymbolCand] = []
|
|
116
|
+
seen: Set[str] = set()
|
|
117
|
+
export_hits = iter_js_export_symbols(source)
|
|
118
|
+
for line, name in export_hits:
|
|
119
|
+
if name in seen:
|
|
120
|
+
continue
|
|
121
|
+
span = _js_definition_span(source, name)
|
|
122
|
+
if span is not None:
|
|
123
|
+
start, end = span
|
|
124
|
+
else:
|
|
125
|
+
start = end = line
|
|
126
|
+
# Fold export-list lines into the defining span so `export { name }` is
|
|
127
|
+
# not treated as an external reference that clears the symbol.
|
|
128
|
+
for eline, ename in export_hits:
|
|
129
|
+
if ename == name:
|
|
130
|
+
start = min(start, eline)
|
|
131
|
+
end = max(end, eline)
|
|
132
|
+
if line not in added_lines and not (set(range(start, end + 1)) & added_lines):
|
|
133
|
+
continue
|
|
134
|
+
lines = source.splitlines()
|
|
135
|
+
if 0 < line <= len(lines):
|
|
136
|
+
prev_lines = lines[: line - 1]
|
|
137
|
+
if prev_lines:
|
|
138
|
+
prev = prev_lines[-1].strip()
|
|
139
|
+
if prev.startswith("@"):
|
|
140
|
+
continue
|
|
141
|
+
seen.add(name)
|
|
142
|
+
out.append((path, start, end, name))
|
|
143
|
+
return out
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _build_token_index(
|
|
147
|
+
project_root: Path,
|
|
148
|
+
files: List[str],
|
|
149
|
+
*,
|
|
150
|
+
exclude: Set[str],
|
|
151
|
+
) -> tuple[dict[str, Set[str]], dict[str, dict[str, Set[int]]]]:
|
|
152
|
+
"""Map identifier token -> files, and token -> file -> line numbers."""
|
|
153
|
+
index: dict[str, Set[str]] = {}
|
|
154
|
+
lines_index: dict[str, dict[str, Set[int]]] = {}
|
|
155
|
+
try:
|
|
156
|
+
for rel in files:
|
|
157
|
+
norm = _norm(rel)
|
|
158
|
+
if norm in exclude:
|
|
159
|
+
continue
|
|
160
|
+
if is_vendored_path(norm):
|
|
161
|
+
continue
|
|
162
|
+
if Path(norm).suffix.lower() not in _CODE_SUFFIXES:
|
|
163
|
+
continue
|
|
164
|
+
path = project_root / norm
|
|
165
|
+
if not path.is_file():
|
|
166
|
+
continue
|
|
167
|
+
try:
|
|
168
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
169
|
+
except OSError:
|
|
170
|
+
continue
|
|
171
|
+
if path.suffix.lower() == ".py":
|
|
172
|
+
cleaned = strip_string_literals(strip_py_comments(text))
|
|
173
|
+
else:
|
|
174
|
+
cleaned = strip_string_literals(strip_js_comments(text))
|
|
175
|
+
for lineno, line in enumerate(cleaned.splitlines(), 1):
|
|
176
|
+
for tok in _IDENT_RE.findall(line):
|
|
177
|
+
if len(tok) < 2:
|
|
178
|
+
continue
|
|
179
|
+
index.setdefault(tok, set()).add(norm)
|
|
180
|
+
lines_index.setdefault(tok, {}).setdefault(norm, set()).add(lineno)
|
|
181
|
+
except Exception:
|
|
182
|
+
logger.debug("token index build failed", exc_info=True)
|
|
183
|
+
return index, lines_index
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _symbol_is_referenced(
|
|
187
|
+
name: str,
|
|
188
|
+
path: str,
|
|
189
|
+
start: int,
|
|
190
|
+
end: int,
|
|
191
|
+
token_index: dict[str, Set[str]],
|
|
192
|
+
lines_index: dict[str, dict[str, Set[int]]],
|
|
193
|
+
) -> bool:
|
|
194
|
+
"""True when ``name`` is used outside its defining span (other file or same-file)."""
|
|
195
|
+
refs = token_index.get(name, set())
|
|
196
|
+
if refs - {path}:
|
|
197
|
+
return True
|
|
198
|
+
same_lines = lines_index.get(name, {}).get(path, set())
|
|
199
|
+
return any(ln < start or ln > end for ln in same_lines)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def detect_dead_symbol_gaps(
|
|
203
|
+
*,
|
|
204
|
+
task: Task,
|
|
205
|
+
project_root: Path,
|
|
206
|
+
diff_content: str,
|
|
207
|
+
next_gap_id: Callable[[str, str], str],
|
|
208
|
+
dead_symbol_enabled: bool = True,
|
|
209
|
+
dead_symbol_blocking: bool = False,
|
|
210
|
+
requirements: Optional[List[Requirement]] = None,
|
|
211
|
+
unwired_files: Optional[Set[str]] = None,
|
|
212
|
+
git_files: Optional[List[str]] = None,
|
|
213
|
+
lsp_refs: Optional[bool] = None,
|
|
214
|
+
) -> List[Gap]:
|
|
215
|
+
"""Flag diff-added public top-level symbols with zero external references.
|
|
216
|
+
|
|
217
|
+
When ``lsp_refs`` is True (or config ``indexing.lsp_refs`` when ``None``),
|
|
218
|
+
token-scan dead candidates are confirmed via the optional live LSP client
|
|
219
|
+
before becoming gaps — external references clear false positives.
|
|
220
|
+
"""
|
|
221
|
+
gaps: List[Gap] = []
|
|
222
|
+
if not dead_symbol_enabled or not diff_content:
|
|
223
|
+
return gaps
|
|
224
|
+
try:
|
|
225
|
+
by_file = added_lines_by_file(diff_content)
|
|
226
|
+
if not by_file:
|
|
227
|
+
return gaps
|
|
228
|
+
|
|
229
|
+
scaffolding_ok = task_allows_scaffolding(task)
|
|
230
|
+
intent = task_intent_text(task, requirements) if requirements is not None else (
|
|
231
|
+
f"{task.title} {task.description}"
|
|
232
|
+
)
|
|
233
|
+
unwired = {_norm(p) for p in (unwired_files or set())}
|
|
234
|
+
|
|
235
|
+
candidates: List[_SymbolCand] = []
|
|
236
|
+
for path, added in by_file.items():
|
|
237
|
+
norm = _norm(path)
|
|
238
|
+
if not is_liveness_code_file(norm) or is_test_path(norm):
|
|
239
|
+
continue
|
|
240
|
+
if norm in unwired:
|
|
241
|
+
continue
|
|
242
|
+
line_nums = {ln for ln, _ in added}
|
|
243
|
+
if Path(norm).suffix.lower() == ".py":
|
|
244
|
+
candidates.extend(_python_candidates(project_root, norm, line_nums))
|
|
245
|
+
else:
|
|
246
|
+
candidates.extend(_js_candidates(project_root, norm, line_nums))
|
|
247
|
+
|
|
248
|
+
if not candidates:
|
|
249
|
+
return gaps
|
|
250
|
+
|
|
251
|
+
candidates.sort(key=lambda item: (item[0], item[1], item[3]))
|
|
252
|
+
|
|
253
|
+
if git_files is None:
|
|
254
|
+
try:
|
|
255
|
+
from devcouncil.indexing.repo_mapper import RepoMapper
|
|
256
|
+
|
|
257
|
+
tracked = RepoMapper(project_root).get_git_files()
|
|
258
|
+
except Exception:
|
|
259
|
+
tracked = []
|
|
260
|
+
else:
|
|
261
|
+
tracked = list(git_files)
|
|
262
|
+
token_index, lines_index = _build_token_index(project_root, tracked, exclude=set())
|
|
263
|
+
|
|
264
|
+
use_lsp = lsp_refs
|
|
265
|
+
if use_lsp is None:
|
|
266
|
+
try:
|
|
267
|
+
from devcouncil.indexing.lsp_client import lsp_refs_enabled
|
|
268
|
+
|
|
269
|
+
use_lsp = lsp_refs_enabled(project_root)
|
|
270
|
+
except Exception:
|
|
271
|
+
use_lsp = False
|
|
272
|
+
lsp_pool = None
|
|
273
|
+
if use_lsp:
|
|
274
|
+
try:
|
|
275
|
+
from devcouncil.indexing.lsp_client import LspSessionPool
|
|
276
|
+
|
|
277
|
+
lsp_pool = LspSessionPool(project_root)
|
|
278
|
+
except Exception:
|
|
279
|
+
lsp_pool = None
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
for path, start, end, name in candidates:
|
|
283
|
+
# Intent-text naming = deliberate API addition.
|
|
284
|
+
if re.search(rf"\b{re.escape(name)}\b", intent or ""):
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
# allow-unwired on the defining line/file with scaffolding parity.
|
|
288
|
+
try:
|
|
289
|
+
source = (project_root / path).read_text(encoding="utf-8", errors="replace")
|
|
290
|
+
src_lines = source.splitlines()
|
|
291
|
+
line_text = src_lines[start - 1] if 0 < start <= len(src_lines) else ""
|
|
292
|
+
except OSError:
|
|
293
|
+
line_text = ""
|
|
294
|
+
source = ""
|
|
295
|
+
if ALLOW_UNWIRED in line_text or ALLOW_UNWIRED in source:
|
|
296
|
+
gaps.append(Gap(
|
|
297
|
+
id=next_gap_id(task.id, f"DEADDECL-{path}:{start}:{name}"),
|
|
298
|
+
severity="medium",
|
|
299
|
+
gap_type="dead_symbol",
|
|
300
|
+
task_id=task.id,
|
|
301
|
+
description=(
|
|
302
|
+
f"Intentional unused symbol `{name}` declared at {path}:{start}."
|
|
303
|
+
),
|
|
304
|
+
evidence=[f"{path}:{start}", f"symbol:{name}", ALLOW_UNWIRED],
|
|
305
|
+
recommended_fix=(
|
|
306
|
+
f"Wire `{name}` into its caller when scaffolding is complete, "
|
|
307
|
+
"or remove it."
|
|
308
|
+
),
|
|
309
|
+
blocking=False,
|
|
310
|
+
file=path,
|
|
311
|
+
line=start,
|
|
312
|
+
))
|
|
313
|
+
if scaffolding_ok:
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
if _symbol_is_referenced(name, path, start, end, token_index, lines_index):
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
from devcouncil.indexing.graph.query import symbol_has_non_test_inbound
|
|
321
|
+
|
|
322
|
+
if symbol_has_non_test_inbound(project_root, path, name):
|
|
323
|
+
continue
|
|
324
|
+
except Exception:
|
|
325
|
+
logger.debug("graph inbound check failed for %s", name, exc_info=True)
|
|
326
|
+
|
|
327
|
+
if lsp_pool is not None:
|
|
328
|
+
try:
|
|
329
|
+
confirmed = lsp_pool.confirm_unreferenced(path, start, name)
|
|
330
|
+
if confirmed is False:
|
|
331
|
+
continue
|
|
332
|
+
except Exception:
|
|
333
|
+
logger.debug("LSP dead-symbol confirm failed for %s", name, exc_info=True)
|
|
334
|
+
|
|
335
|
+
gaps.append(Gap(
|
|
336
|
+
id=next_gap_id(task.id, f"DEAD-{path}:{start}:{name}"),
|
|
337
|
+
severity="high" if dead_symbol_blocking else "medium",
|
|
338
|
+
gap_type="dead_symbol",
|
|
339
|
+
task_id=task.id,
|
|
340
|
+
description=(
|
|
341
|
+
f"New public symbol `{name}` at {path}:{start} is never referenced "
|
|
342
|
+
"outside its own definition."
|
|
343
|
+
),
|
|
344
|
+
evidence=[f"{path}:{start}", f"symbol:{name}"],
|
|
345
|
+
recommended_fix=(
|
|
346
|
+
f"Call or register `{name}` from the code that needs it "
|
|
347
|
+
f"(use `dev scope update {task.id} --lease-token <token> "
|
|
348
|
+
f"--planned-file <caller>` if the caller is out of scope), or remove it."
|
|
349
|
+
),
|
|
350
|
+
blocking=dead_symbol_blocking,
|
|
351
|
+
file=path,
|
|
352
|
+
line=start,
|
|
353
|
+
))
|
|
354
|
+
finally:
|
|
355
|
+
if lsp_pool is not None:
|
|
356
|
+
lsp_pool.close()
|
|
357
|
+
except Exception:
|
|
358
|
+
logger.debug("detect_dead_symbol_gaps failed; degrading to zero gaps", exc_info=True)
|
|
359
|
+
return []
|
|
360
|
+
return gaps
|