i dont like commits

This commit is contained in:
Tomas Dvorak
2026-02-24 12:10:13 +01:00
parent 898a3c303f
commit 1d72a1cc01
109 changed files with 43586 additions and 8484 deletions
+22
View File
@@ -0,0 +1,22 @@
{
"target_strict_score": 95,
"review_max_age_days": 30,
"holistic_max_age_days": 30,
"generate_scorecard": true,
"badge_path": "scorecard.png",
"exclude": [
".venv",
"devour_data",
"cmd/devour_data",
"desloppify"
],
"ignore": [],
"ignore_metadata": {},
"zone_overrides": {},
"review_dimensions": [],
"large_files_threshold": 0,
"props_threshold": 0,
"finding_noise_budget": 10,
"finding_noise_global_budget": 0,
"languages": {}
}
+328 -283
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6464 -1799
View File
File diff suppressed because one or more lines are too long
+6464 -1799
View File
File diff suppressed because one or more lines are too long
@@ -0,0 +1,467 @@
{
"assessments": {
"abstraction_fitness": {
"score": 45.8,
"components": [
"Abstraction Leverage",
"Indirection Cost",
"Interface Honesty"
],
"component_scores": {
"Abstraction Leverage": 65.5,
"Indirection Cost": 62.1,
"Interface Honesty": 71.5
}
},
"cross_module_architecture": 56.0,
"design_coherence": 49.3,
"error_consistency": 44.4,
"test_strategy": 46.3
},
"dimension_notes": {
"cross_module_architecture": {
"evidence": [
"`internal/quality/enhanced_types.go` centralizes many unrelated boundary contracts (scoring metrics, detector transparency, narrative strategy/tools, debt tracking, config) in one package-level type hub.",
"`internal/quality/types.go` also acts as a second broad contract hub (findings, scan result, scorecard, language/config, extraction structs), overlapping the same ownership area as `enhanced_types.go` instead of separating by subdomain.",
"`internal/quality/scoring_test.go` consumes these shared package-level contracts directly, which reinforces coupling to a wide internal surface rather than narrower scorer-specific interfaces/types."
],
"impact_scope": "module",
"fix_scope": "multi_file_refactor",
"confidence": "high",
"unreported_risk": ""
},
"abstraction_fitness": {
"evidence": [
"Four external scrapers (Astro, Docker, Cloudflare, Nuxt) each reimplement the same transport/change-detection skeleton (`fetchPage`, `DetectChanges`, `generateHash`, per-page `Document` assembly) with only parser/model differences.",
"`cmd/serve.go` RPC path for `devour_scrape` mutates CLI globals (`scrapeFormat`, `scrapeOutput`, `scrapeAllowEmpty`) to call `scrapeOne` from `cmd/scrape.go`, showing a leaky abstraction where server flow depends on CLI stateful wiring.",
"`vector.Store` advertises interchangeable backends, but `NewStore` can return `ChromemStore` whose methods are all unimplemented runtime errors, so the abstraction surface overstates usable implementations."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high",
"unreported_risk": "",
"sub_axes": {
"abstraction_leverage": 68.0,
"indirection_cost": 73.0,
"interface_honesty": 70.0
}
},
"test_strategy": {
"evidence": [
"internal/quality/scoring_test.go is heavily unit-focused and validates scorer internals, but does not validate public contract stability between quality types and README-documented behavior.",
"README.md publishes CLI and JSON-RPC command contracts, but assigned tests do not exercise those published interfaces as compatibility gates.",
"pkg/rustdocs/parser_test.go and internal/quality/scoring_test.go include brittle assertions (fixed positional result indexing and strict output text matching) that couple tests to implementation/presentation details."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high",
"unreported_risk": ""
},
"design_coherence": {
"evidence": [
"Command passthrough wrappers add non-functional indirection in language command modules: `languages/dart/commands.py` defines `_cmd_*_impl` plus thin `cmd_*` forwarders for each command (`cmd_large`, `cmd_complexity`, `cmd_deps`, `cmd_cycles`, `cmd_orphaned`, `cmd_dupes`).",
"C# deps module mixes parsing, graph construction, CLI arg resolution, and terminal rendering in one file (`languages/csharp/detectors/deps.py` contains core graph builders and also `cmd_deps`/`cmd_cycles` UI handlers).",
"Status rendering duplicates score-bar construction logic in two loops in `app/commands/status_parts/render.py` (same threshold/color/filled computation blocks around lines ~191-199 and ~223-231), increasing drift risk."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high",
"unreported_risk": ""
},
"error_consistency": {
"evidence": [
"RPC paths mix strict and lax decode behavior: `cmd/serve.go` returns decode errors for `devour_scrape`/`devour_ask` but ignores decode errors for `devour_query` and `devour_sync` (`_ = json.Unmarshal(...)`).",
"`cmd/serve.go` `devour_status` ignores errors from `LoadSourceState` and `EnsureIndexed` then dereferences `idxStats.Documents`, which can panic on nil when indexing fails.",
"`internal/server/server.go` exposes raw internal errors to clients (`Message: err.Error()`), while parse/invalid-request errors are normalized strings; HTTP transport always emits 400 for any RPC error, collapsing error classes.",
"`internal/ai/openai.go` does not check HTTP status before JSON decode, unlike scraper fetchers in `internal/scraper/external/*.go` and `internal/scraper/openapi.go` that explicitly gate on status codes.",
"`internal/search/engine.go` and `internal/scraper/openapi.go` mix wrapped and passthrough errors in adjacent paths, producing uneven context for operators."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high",
"unreported_risk": ""
}
},
"findings": [
{
"dimension": "cross_module_architecture",
"identifier": "quality_package_contract_hub_coupling",
"summary": "Quality contracts are concentrated in broad type hubs, creating a coupling hotspot.",
"related_files": [
"internal/quality/enhanced_types.go",
"internal/quality/types.go",
"internal/quality/scoring_test.go"
],
"evidence": [
"`enhanced_types.go` and `types.go` both define large cross-cutting models for multiple concerns (analysis narrative, scoring, detector stats, config, extraction metadata) under one package boundary.",
"The two files blur subdomain ownership, so edits to one concern can force unrelated consumers in the same package to track evolving shared structs.",
"Scoring tests rely on shared package contracts rather than a tighter scorer-local contract, indicating broad internal API exposure."
],
"suggestion": "Split `internal/quality` contracts by bounded concern (for example: `qualitycore` findings/scan types, `qualityscore` scorecard/metrics, `qualitynarrative` narrative/report DTOs) and keep scorer-facing types in a narrow subpackage/API. Migrate tests to import only scorer-relevant types to reduce transitive coupling.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "abstraction_fitness",
"identifier": "duplicated_external_scraper_skeleton",
"summary": "External docs scrapers duplicate the same orchestration instead of sharing one adapter base",
"related_files": [
"internal/scraper/external/astrodocs.go",
"internal/scraper/external/cloudflaredocs.go",
"internal/scraper/external/dockerdocs.go",
"internal/scraper/external/nuxtdocs.go"
],
"evidence": [
"Each scraper defines near-identical `fetchPage`, `DetectChanges`, and `generateHash` logic.",
"Each `Scrape` method repeats the same flow: validate URL -> fetch HTML -> parser call -> append main doc + sub-doc loop(s).",
"Differences are mostly parser/model-specific mapping, but transport/error/hash logic is copy-pasted."
],
"suggestion": "Introduce a shared docs-scraper base (or helper pipeline) for HTTP fetch + hashing + standard error handling, and keep only parser-specific mapping in per-provider adapters.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "abstraction_fitness",
"identifier": "scrape_api_leaks_cli_state",
"summary": "Server scrape RPC depends on mutable CLI globals to reuse scrape pipeline",
"related_files": [
"cmd/serve.go",
"cmd/scrape.go",
"cmd/get.go"
],
"evidence": [
"`handleServeMethod` temporarily rewrites `scrapeFormat`, `scrapeOutput`, and `scrapeAllowEmpty` before calling `scrapeOne`, then restores them.",
"`scrapeOne` is not a pure service API; it is coupled to CLI-level shared state and output behavior.",
"`cmd/get.go` also routes through `runScrape`, reinforcing that scraping orchestration is command-centric rather than a reusable application service."
],
"suggestion": "Extract a stateless scrape service function (explicit request struct + options) used by both CLI commands and RPC handlers; keep CLI flags as translation-only at command boundary.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
},
{
"dimension": "abstraction_fitness",
"identifier": "vector_store_interface_overpromises",
"summary": "Vector store abstraction exposes backends that are selectable but not actually implemented",
"related_files": [
"internal/vector/store.go",
"internal/indexer/indexer.go"
],
"evidence": [
"`NewStore` can return `ChromemStore` when config type is `chromem`.",
"All `ChromemStore` interface methods currently return `not implemented` errors.",
"`Indexer` depends only on `vector.Store`, so backend failure appears at runtime after abstraction selection instead of at wiring/validation time."
],
"suggestion": "Make backend capabilities explicit: either remove/guard `chromem` selection until complete, or return an initialization error type from store construction and enforce backend readiness before indexing starts.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "cross_module_architecture",
"identifier": "docs_runtime_quality_contract_drift",
"summary": "Quality dimensions/statuses drift between public docs and runtime model contracts.",
"related_files": [
"README.md",
"internal/quality/enhanced_types.go",
"internal/quality/types.go"
],
"evidence": [
"README.md frames score interpretation around a specific dimension set and user workflow.",
"internal/quality/enhanced_types.go includes extra dimension constants not represented in README contract text.",
"internal/quality/types.go includes StatusIgnored while README resolution examples only expose fixed/wontfix/false_positive paths."
],
"suggestion": "Define a single versioned public quality contract (dimensions + statuses) as source-of-truth, generate README contract tables from it, and add a CI check that fails when exported enums and published docs diverge.",
"confidence": "medium",
"impact_scope": "codebase",
"fix_scope": "architectural_change"
},
{
"dimension": "test_strategy",
"identifier": "missing_public_contract_compat_tests",
"summary": "Published CLI/RPC governance contracts are not protected by compatibility tests.",
"related_files": [
"README.md",
"internal/quality/scoring_test.go",
"internal/quality/types.go"
],
"evidence": [
"README.md documents stable command and RPC method surfaces for users.",
"internal/quality/scoring_test.go covers scoring behavior but not end-to-end contract assertions tied to documented surfaces.",
"No assigned test verifies that externally documented score/status semantics remain compatible with exported model types."
],
"suggestion": "Add contract tests that parse/validate documented command and RPC surface claims against runtime registration/types, and add schema-level golden tests for serialized quality/status payloads.",
"confidence": "high",
"impact_scope": "codebase",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "test_strategy",
"identifier": "brittle_assertions_on_order_and_formatting",
"summary": "Tests are fragile due to strict ordering and presentation-string coupling.",
"related_files": [
"internal/quality/scoring_test.go",
"pkg/rustdocs/parser_test.go"
],
"evidence": [
"pkg/rustdocs/parser_test.go asserts semantic expectations through fixed indexes (results[0], results[1], results[2]) rather than identity-based matching.",
"internal/quality/scoring_test.go asserts many exact output substrings in FormatScorecard, coupling tests to formatting text that can change without behavioral regressions."
],
"suggestion": "Refactor tests to assert semantic invariants (presence by key/kind, normalized structured fields) and reserve strict golden-output checks for explicitly versioned presentation contracts.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "design_coherence",
"identifier": "passthrough_command_wrappers_add_low_leverage_layers",
"summary": "Language command modules include thin forwarding wrappers with no behavioral value.",
"related_files": [
"desloppify/desloppify/desloppify/languages/dart/commands.py",
"desloppify/desloppify/desloppify/languages/csharp/commands.py",
"desloppify/desloppify/desloppify/languages/framework/commands_base.py"
],
"evidence": [
"`languages/dart/commands.py` creates `_cmd_*_impl` callables and then defines six `cmd_*` functions that only call the corresponding impl.",
"`languages/csharp/commands.py` also keeps thin wrappers (`cmd_large`, `cmd_complexity`, `cmd_deps`, `cmd_cycles`) that forward directly to other callables.",
"The framework already provides factory-returned callables; wrapper layers increase symbol count and call depth without adding policy."
],
"suggestion": "Assign factory outputs directly to exported command names (or build the registry directly from generated callables) and keep wrappers only where they add language-specific behavior, validation, or formatting.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "design_coherence",
"identifier": "csharp_deps_module_blends_core_and_cli_responsibilities",
"summary": "C# dependency detector file combines analysis engine logic with CLI presentation.",
"related_files": [
"desloppify/desloppify/desloppify/languages/csharp/detectors/deps.py",
"desloppify/desloppify/desloppify/languages/csharp/commands.py",
"desloppify/desloppify/desloppify/languages/_shared/scaffold_detect_commands.py"
],
"evidence": [
"`languages/csharp/detectors/deps.py` includes internal graph/parsing functions (`_parse_project_assets_references`, `_build_dep_graph_roslyn`, `build_dep_graph`) and command handlers (`cmd_deps`, `cmd_cycles`) in the same module.",
"`languages/csharp/commands.py` forwards to `cmd_deps_direct`/`cmd_cycles_deps`, meaning command routing depends on detector-module UI functions instead of a clean detector API.",
"Other language paths use shared command scaffolding (`languages/_shared/scaffold_detect_commands.py`) that separates command shell concerns from graph/detection logic."
],
"suggestion": "Split C# deps into `detectors/deps_core.py` (graph/parsing only) and command-facing adapters in `commands.py` (or scaffold factories), so detectors expose data and command modules own JSON/table rendering.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
},
{
"dimension": "design_coherence",
"identifier": "status_bar_render_logic_duplicated_in_two_paths",
"summary": "Status renderer repeats identical score-bar construction for mechanical and subjective rows.",
"related_files": [
"desloppify/desloppify/desloppify/app/commands/status_parts/render.py",
"desloppify/desloppify/desloppify/app/output/scorecard_parts/projection.py"
],
"evidence": [
"`show_dimension_table` in `status_parts/render.py` computes `filled`, score thresholds, and colored bar in one loop for mechanical dimensions and repeats the same logic in a second loop for subjective entries.",
"Duplicated threshold constants (`>=98`, `>=93`) and color composition appear in both blocks, creating a two-site maintenance point for one rendering policy."
],
"suggestion": "Extract a shared `render_score_bar(score_val, bar_len)` helper (or move the row formatting policy into scorecard projection/output helpers) and reuse it for both loops to keep display semantics consistent.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "single_edit"
},
{
"dimension": "error_consistency",
"identifier": "rpc_decode_and_response_contract_drift",
"summary": "RPC methods use inconsistent decode and error-response contracts",
"related_files": [
"cmd/serve.go",
"internal/server/server.go"
],
"evidence": [
"`cmd/serve.go` ignores JSON decode errors in `devour_query`/`devour_sync` but returns decode errors in `devour_scrape`/`devour_ask`.",
"`internal/server/server.go` returns raw `err.Error()` in RPC payloads and maps all RPC errors to HTTP 400 in `writeRPC`."
],
"suggestion": "Define one RPC error policy: always validate/decode params the same way, map known classes to stable RPC codes/messages, and map transport HTTP statuses by error class (client input vs internal failure).",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "serve_status_swallows_errors_then_dereferences_nil",
"summary": "Status handler ignores errors and can panic from nil stats",
"related_files": [
"cmd/serve.go",
"internal/search/engine.go"
],
"evidence": [
"`cmd/serve.go` uses `state, _ := projectstate.LoadSourceState(...)` and `idxStats, _ := engine.EnsureIndexed(ctx)` in `devour_status`.",
"The same block unconditionally reads `idxStats.Documents` and `idxStats.LastIndexedAt`, which is unsafe if `EnsureIndexed` returned an error."
],
"suggestion": "Handle both errors explicitly in `devour_status`; return structured partial-status with an error field or fail the method uniformly, but do not ignore and dereference.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "single_edit"
},
{
"dimension": "error_consistency",
"identifier": "openai_http_error_path_not_normalized",
"summary": "OpenAI client lacks explicit HTTP status error handling",
"related_files": [
"internal/ai/openai.go",
"internal/scraper/openapi.go",
"internal/scraper/external/astrodocs.go"
],
"evidence": [
"`internal/ai/openai.go` decodes response bodies directly and only checks `embeddingResp.Error`/`chatResp.Error`; non-2xx responses without expected JSON become generic decode errors.",
"`internal/scraper/openapi.go` and external scrapers explicitly validate HTTP status before body parsing and return explicit HTTP status errors."
],
"suggestion": "Add explicit non-2xx handling in both OpenAI request paths: include status code, bounded response body excerpt, and endpoint context before JSON decode.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "mixed_wrapping_vs_passthrough_in_core_flows",
"summary": "Adjacent paths alternate between wrapped and raw error returns",
"related_files": [
"internal/search/engine.go",
"internal/scraper/openapi.go",
"internal/config/config.go"
],
"evidence": [
"`internal/search/engine.go` frequently returns raw errors (`return nil, err`) from filesystem/index operations.",
"`internal/scraper/openapi.go` similarly passes through request/file errors raw in `readSpec`, while other branches wrap with operation context.",
"`internal/config/config.go` demonstrates contextual wrapping style (`read config`, `parse config`), creating drift against less-informative paths."
],
"suggestion": "Adopt a package-level rule: wrap external boundary failures with operation context (read/write/parse/network) and preserve `%w`; apply consistently across search/openapi paths.",
"confidence": "medium",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "scanner_pipeline_fail_open_is_inconsistent",
"summary": "Quality scan path mixes fail-open and fail-fast behaviors",
"related_files": [
"internal/quality/scanner.go",
"internal/quality/plugins/go/analyzers/test_coverage.go",
"internal/quality/plugins/go/analyzers/detectors.go"
],
"evidence": [
"`internal/quality/scanner.go` logs detector failures and continues, silently reducing coverage of reported findings.",
"`test_coverage.go` returns `(nil, nil)` for missing `go` tool or missing generated coverage file, while other detector failures return explicit errors.",
"`detectors.go` has parse/count paths that silently `continue` on per-file errors, further mixing behavior."
],
"suggestion": "Standardize detector error semantics with typed outcomes (hard error, soft-skip with reason, per-file skip count) and surface these in scan results so degraded scans are explicit.",
"confidence": "medium",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
},
{
"dimension": "cross_module_architecture",
"identifier": "rpc_cli_global_state_coupling",
"summary": "RPC handlers mutate CLI global flags to execute workflows across command boundaries.",
"related_files": [
"cmd/serve.go",
"cmd/scrape.go",
"cmd/sync.go"
],
"evidence": [
"cmd/serve.go sets scrapeFormat/scrapeOutput/scrapeAllowEmpty before calling scrapeOne and restores afterward.",
"cmd/serve.go sets syncForce/syncRebuild/syncSource before calling runSync and restores afterward."
],
"suggestion": "Extract scrape/sync/query use-cases into stateless service functions (input structs + return structs), call them from both Cobra commands and RPC handlers, and remove shared mutable command globals from runtime execution paths.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
},
{
"dimension": "error_consistency",
"identifier": "silent_error_drops_in_rpc_paths",
"summary": "Several RPC paths ignore parse/index/state errors, producing inconsistent failure contracts.",
"related_files": [
"cmd/serve.go",
"internal/search/engine.go",
"internal/projectstate/state.go"
],
"evidence": [
"cmd/serve.go ignores JSON unmarshal errors for devour_query/devour_sync (`_ = json.Unmarshal(...)`).",
"cmd/serve.go ignores errors from LoadSourceState and EnsureIndexed, yet reads fields from returned values.",
"Other command paths generally propagate wrapped errors with `%w`."
],
"suggestion": "Treat request decode, source-state load, and index-check failures uniformly: return explicit wrapped errors from each branch and avoid underscore-discarded errors in RPC handlers.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "single_edit"
},
{
"dimension": "abstraction_fitness",
"identifier": "external_scraper_transport_duplication",
"summary": "External scrapers duplicate the same HTTP fetch/hash/change-detection boilerplate.",
"related_files": [
"internal/scraper/external/reactdocs.go",
"internal/scraper/external/tsdocs.go",
"internal/scraper/external/godocs.go",
"internal/scraper/external/rustdocs.go",
"internal/scraper/external/cloudflaredocs.go"
],
"evidence": [
"Each scraper reimplements equivalent `fetchPage` with identical request/header/response-body flow.",
"Each scraper reimplements nearly identical `DetectChanges` and `generateHash` logic."
],
"suggestion": "Introduce a shared base helper (e.g., `fetchPage`, `contentHash`, `detectChanges`) in internal/scraper/external/types.go or a dedicated internal transport module, then keep per-scraper files focused on parser-to-document mapping.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "test_strategy",
"identifier": "missing_tests_on_orchestration_and_adapters",
"summary": "Critical orchestration commands and most external adapters have no direct tests.",
"related_files": [
"cmd/serve.go",
"cmd/sync.go",
"cmd/query.go",
"internal/scraper/external/godocs.go",
"internal/scraper/external/reactdocs.go"
],
"evidence": [
"No *_test.go peers for cmd/serve.go, cmd/sync.go, cmd/query.go despite high-risk orchestration behavior.",
"Only one external scraper has direct tests while many adapter implementations contain custom mapping and error-path logic."
],
"suggestion": "Add table-driven tests for RPC method branches and error propagation in cmd/serve.go, plus httptest-based adapter contract tests covering fetch failures, parser errors, and document mapping for each external scraper family.",
"confidence": "high",
"impact_scope": "codebase",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "design_coherence",
"identifier": "serve_handler_multi_responsibility_switch",
"summary": "Single handler function mixes transport, parsing, orchestration, and mutable state management.",
"related_files": [
"cmd/serve.go",
"cmd/scrape.go",
"cmd/sync.go",
"cmd/query.go"
],
"evidence": [
"handleServeMethod contains method routing plus per-method request schemas, domain calls, and response shaping in one body.",
"The function coordinates temporary mutation of command globals to reuse CLI code paths."
],
"suggestion": "Split each RPC method into dedicated handler functions (e.g., handleQueryRPC, handleScrapeRPC, handleSyncRPC) that depend on explicit service interfaces instead of command globals.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
}
],
"review_quality": {
"batch_count": 6,
"dimension_coverage": 0.367,
"evidence_density": 1.667,
"high_score_without_risk": 0,
"finding_pressure": 48.928,
"dimensions_with_findings": 5
}
}
@@ -0,0 +1,49 @@
You are a focused subagent reviewer for a single holistic investigation batch.
Repository root: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour
Immutable packet: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour/.desloppify/review_packets/holistic_packet_20260224_101201.json
Batch index: 1
Batch name: Architecture & Coupling
Batch dimensions: cross_module_architecture
Batch rationale: god modules, import-time side effects
Files assigned:
- internal/quality/enhanced_types.go
- internal/quality/scoring_test.go
- internal/quality/types.go
- pkg/rustdocs/parser_test.go
Task requirements:
1. Read the immutable packet and follow `system_prompt` constraints exactly.
2. Evaluate ONLY listed files and ONLY listed dimensions for this batch.
3. Return 0-10 high-quality findings for this batch (empty array allowed).
4. Score/finding consistency is required: broader or more severe findings MUST lower dimension scores.
5. Every finding must include `related_files` with at least 2 files when possible.
6. Every finding must include `impact_scope` and `fix_scope`.
7. Every scored dimension MUST include dimension_notes with concrete evidence.
8. If a dimension score is >85, include `unreported_risk` in dimension_notes.
9. Use exactly one decimal place for every assessment and abstraction sub-axis score.
10. Do not edit repository files.
11. Return ONLY valid JSON, no markdown fences.
Scope enums:
- impact_scope: "local" | "module" | "subsystem" | "codebase"
- fix_scope: "single_edit" | "multi_file_refactor" | "architectural_change"
Output schema:
{
"batch": "Architecture & Coupling",
"batch_index": 1,
"assessments": {"<dimension>": <0-100 with one decimal place>},
"dimension_notes": {
"<dimension>": {
"evidence": ["specific code observations"],
"impact_scope": "local|module|subsystem|codebase",
"fix_scope": "single_edit|multi_file_refactor|architectural_change",
"confidence": "high|medium|low",
"unreported_risk": "required when score >85",
"sub_axes": {"abstraction_leverage": 0-100 with one decimal place, "indirection_cost": 0-100 with one decimal place, "interface_honesty": 0-100 with one decimal place} // required for abstraction_fitness when evidence supports it
}
},
"findings": []
}
@@ -0,0 +1,76 @@
You are a focused subagent reviewer for a single holistic investigation batch.
Repository root: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour
Immutable packet: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour/.desloppify/review_packets/holistic_packet_20260224_101201.json
Batch index: 2
Batch name: Abstractions & Dependencies
Batch dimensions: abstraction_fitness
Batch rationale: abstraction hotspots (wrappers/interfaces/param bags), dep cycles
Files assigned:
- internal/config/config.go
- cmd/scrape.go
- internal/quality/plugins/go/analyzers/detectors.go
- internal/quality/plugins/go/analyzers/advanced.go
- internal/scraper/web.go
- internal/quality/plugins/go/plugin.go
- internal/scheduler/scheduler.go
- cmd/push.go
- internal/scraper/localsearch_test.go
- cmd/ask.go
- internal/ai/openai.go
- internal/server/server.go
- cmd/get.go
- internal/quality/analyzers/controlflow.go
- internal/vector/store.go
- examples/demo_scrapers.go
- internal/indexer/indexer.go
- internal/scraper/openapi.go
- pkg/pythondocs/parser.go
- cmd/get_test.go
- internal/quality/scanner_test.go
- internal/scraper/localsearch.go
- internal/scraper/external/nuxtdocs.go
- internal/quality/plugins/go/analyzers/test_coverage.go
- internal/quality/scanner.go
- internal/search/engine.go
- cmd/serve.go
- internal/scraper/github.go
- internal/scraper/external/astrodocs.go
- internal/scraper/external/cloudflaredocs.go
- internal/scraper/external/dockerdocs.go
Task requirements:
1. Read the immutable packet and follow `system_prompt` constraints exactly.
2. Evaluate ONLY listed files and ONLY listed dimensions for this batch.
3. Return 0-10 high-quality findings for this batch (empty array allowed).
4. Score/finding consistency is required: broader or more severe findings MUST lower dimension scores.
5. Every finding must include `related_files` with at least 2 files when possible.
6. Every finding must include `impact_scope` and `fix_scope`.
7. Every scored dimension MUST include dimension_notes with concrete evidence.
8. If a dimension score is >85, include `unreported_risk` in dimension_notes.
9. Use exactly one decimal place for every assessment and abstraction sub-axis score.
10. Do not edit repository files.
11. Return ONLY valid JSON, no markdown fences.
Scope enums:
- impact_scope: "local" | "module" | "subsystem" | "codebase"
- fix_scope: "single_edit" | "multi_file_refactor" | "architectural_change"
Output schema:
{
"batch": "Abstractions & Dependencies",
"batch_index": 2,
"assessments": {"<dimension>": <0-100 with one decimal place>},
"dimension_notes": {
"<dimension>": {
"evidence": ["specific code observations"],
"impact_scope": "local|module|subsystem|codebase",
"fix_scope": "single_edit|multi_file_refactor|architectural_change",
"confidence": "high|medium|low",
"unreported_risk": "required when score >85",
"sub_axes": {"abstraction_leverage": 0-100 with one decimal place, "indirection_cost": 0-100 with one decimal place, "interface_honesty": 0-100 with one decimal place} // required for abstraction_fitness when evidence supports it
}
},
"findings": []
}
@@ -0,0 +1,50 @@
You are a focused subagent reviewer for a single holistic investigation batch.
Repository root: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour
Immutable packet: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour/.desloppify/review_packets/holistic_packet_20260224_101201.json
Batch index: 3
Batch name: Governance & Contracts
Batch dimensions: cross_module_architecture, test_strategy
Batch rationale: architecture contracts, compatibility policy, docs-vs-runtime scope, and quality-gate coverage
Files assigned:
- README.md
- internal/quality/enhanced_types.go
- internal/quality/scoring_test.go
- internal/quality/types.go
- pkg/rustdocs/parser_test.go
Task requirements:
1. Read the immutable packet and follow `system_prompt` constraints exactly.
2. Evaluate ONLY listed files and ONLY listed dimensions for this batch.
3. Return 0-10 high-quality findings for this batch (empty array allowed).
4. Score/finding consistency is required: broader or more severe findings MUST lower dimension scores.
5. Every finding must include `related_files` with at least 2 files when possible.
6. Every finding must include `impact_scope` and `fix_scope`.
7. Every scored dimension MUST include dimension_notes with concrete evidence.
8. If a dimension score is >85, include `unreported_risk` in dimension_notes.
9. Use exactly one decimal place for every assessment and abstraction sub-axis score.
10. Do not edit repository files.
11. Return ONLY valid JSON, no markdown fences.
Scope enums:
- impact_scope: "local" | "module" | "subsystem" | "codebase"
- fix_scope: "single_edit" | "multi_file_refactor" | "architectural_change"
Output schema:
{
"batch": "Governance & Contracts",
"batch_index": 3,
"assessments": {"<dimension>": <0-100 with one decimal place>},
"dimension_notes": {
"<dimension>": {
"evidence": ["specific code observations"],
"impact_scope": "local|module|subsystem|codebase",
"fix_scope": "single_edit|multi_file_refactor|architectural_change",
"confidence": "high|medium|low",
"unreported_risk": "required when score >85",
"sub_axes": {"abstraction_leverage": 0-100 with one decimal place, "indirection_cost": 0-100 with one decimal place, "interface_honesty": 0-100 with one decimal place} // required for abstraction_fitness when evidence supports it
}
},
"findings": []
}
@@ -0,0 +1,139 @@
You are a focused subagent reviewer for a single holistic investigation batch.
Repository root: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour
Immutable packet: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour/.desloppify/review_packets/holistic_packet_20260224_101201.json
Batch index: 4
Batch name: Design Coherence — Mechanical Concern Signals
Batch dimensions: design_coherence
Batch rationale: mechanical detectors identified structural patterns needing judgment; concern types: design_concern, duplication_design
Files assigned:
- desloppify/desloppify/desloppify/app/commands/_show_terminal.py
- desloppify/desloppify/desloppify/app/commands/fix/apply_flow.py
- desloppify/desloppify/desloppify/app/commands/issues_cmd.py
- desloppify/desloppify/desloppify/app/commands/next.py
- desloppify/desloppify/desloppify/app/commands/resolve/selection.py
- desloppify/desloppify/desloppify/app/commands/scan/scan_reporting_llm.py
- desloppify/desloppify/desloppify/app/commands/status_parts/render.py
- desloppify/desloppify/desloppify/app/output/scorecard_parts/projection.py
- desloppify/desloppify/desloppify/engine/detectors/security/rules.py
- desloppify/desloppify/desloppify/engine/scoring_internal/subjective/core.py
- desloppify/desloppify/desloppify/engine/state_internal/resolution.py
- desloppify/desloppify/desloppify/intelligence/review/__init__.py
- desloppify/desloppify/desloppify/intelligence/review/context_internal/structure.py
- desloppify/desloppify/desloppify/intelligence/review/dimensions/data.py
- desloppify/desloppify/desloppify/intelligence/review/importing/holistic.py
- desloppify/desloppify/desloppify/languages/_shared/phases_common.py
- desloppify/desloppify/desloppify/languages/_shared/review_data/dimensions.json
- desloppify/desloppify/desloppify/languages/_shared/scaffold_detect_commands.py
- desloppify/desloppify/desloppify/languages/csharp/_parse_helpers.py
- desloppify/desloppify/desloppify/languages/csharp/commands.py
- desloppify/desloppify/desloppify/languages/csharp/deps/cli.py
- desloppify/desloppify/desloppify/languages/csharp/deps/fallback.py
- desloppify/desloppify/desloppify/languages/csharp/detectors/deps.py
- desloppify/desloppify/desloppify/languages/csharp/phases.py
- desloppify/desloppify/desloppify/languages/csharp/test_coverage.py
- desloppify/desloppify/desloppify/languages/dart/__init__.py
- desloppify/desloppify/desloppify/languages/dart/commands.py
- desloppify/desloppify/desloppify/languages/dart/detectors/deps.py
- desloppify/desloppify/desloppify/languages/dart/extractors.py
- desloppify/desloppify/desloppify/languages/dart/move.py
- desloppify/desloppify/desloppify/languages/framework/commands_base.py
- desloppify/desloppify/desloppify/languages/gdscript/__init__.py
- desloppify/desloppify/desloppify/languages/gdscript/detectors/deps.py
- desloppify/desloppify/desloppify/languages/python/__init__.py
- desloppify/desloppify/desloppify/languages/python/commands.py
- desloppify/desloppify/desloppify/languages/python/detectors/security.py
- desloppify/desloppify/desloppify/languages/python/detectors/smells.py
- desloppify/desloppify/desloppify/languages/python/move.py
- desloppify/desloppify/desloppify/languages/python/phases.py
- desloppify/desloppify/desloppify/languages/python/test_coverage.py
- desloppify/desloppify/desloppify/languages/python/tests/test_py_facade.py
- desloppify/desloppify/desloppify/languages/typescript/detectors/_smell_detectors.py
- desloppify/desloppify/desloppify/languages/typescript/detectors/_smell_effects.py
- desloppify/desloppify/desloppify/languages/typescript/detectors/deps.py
- desloppify/desloppify/desloppify/languages/typescript/detectors/exports.py
- desloppify/desloppify/desloppify/languages/typescript/detectors/react.py
- desloppify/desloppify/desloppify/languages/typescript/detectors/unused.py
- desloppify/desloppify/desloppify/languages/typescript/fixers/common.py
- desloppify/desloppify/desloppify/languages/typescript/fixers/if_chain.py
- desloppify/desloppify/desloppify/languages/typescript/fixers/logs.py
- desloppify/desloppify/desloppify/languages/typescript/tests/test_ts_concerns.py
- desloppify/desloppify/desloppify/languages/typescript/tests/test_ts_deprecated.py
- desloppify/desloppify/desloppify/languages/typescript/tests/test_ts_deps.py
- desloppify/desloppify/desloppify/languages/typescript/tests/test_ts_exports.py
- desloppify/desloppify/desloppify/languages/typescript/tests/test_ts_fixers.py
- desloppify/desloppify/desloppify/languages/typescript/tests/test_ts_logs.py
- desloppify/desloppify/desloppify/languages/typescript/tests/test_ts_react.py
- desloppify/desloppify/desloppify/tests/commands/fix/test_cmd_fix_review.py
- desloppify/desloppify/desloppify/tests/commands/test_cmd_detect.py
- desloppify/desloppify/desloppify/tests/commands/test_cmd_fix.py
- desloppify/desloppify/desloppify/tests/commands/test_cmd_next.py
- desloppify/desloppify/desloppify/tests/commands/test_cmd_scan.py
- desloppify/desloppify/desloppify/tests/commands/test_cmd_show.py
- desloppify/desloppify/desloppify/tests/commands/test_config_cmd.py
- desloppify/desloppify/desloppify/tests/detectors/test_architecture_boundaries.py
- desloppify/desloppify/desloppify/tests/detectors/test_complexity.py
- desloppify/desloppify/desloppify/tests/detectors/test_coupling.py
- desloppify/desloppify/desloppify/tests/detectors/test_gods.py
- desloppify/desloppify/desloppify/tests/detectors/test_naming.py
- desloppify/desloppify/desloppify/tests/detectors/test_orphaned.py
- desloppify/desloppify/desloppify/tests/lang/common/test_lang_contract_validation.py
- desloppify/desloppify/desloppify/tests/lang/csharp/test_csharp_deps.py
- desloppify/desloppify/desloppify/tests/lang/csharp/test_csharp_scan.py
- desloppify/desloppify/desloppify/tests/lang/dart/test_dart_deps.py
- desloppify/desloppify/desloppify/tests/review/test_review_coverage.py
- desloppify/desloppify/desloppify/tests/review/test_review_dimensions_direct.py
- desloppify/desloppify/desloppify/tests/review/test_work_queue.py
- desloppify/desloppify/desloppify/tests/scan/test_flat_dirs.py
- desloppify/desloppify/desloppify/tests/scan/test_scan_reporting_direct.py
- desloppify/desloppify/desloppify/tests/scan/test_scan_workflow_wontfix_direct.py
- desloppify/desloppify/desloppify/tests/scoring/test_scorecard.py
- desloppify/desloppify/desloppify/tests/scoring/test_scorecard_draw_direct.py
- desloppify/desloppify/desloppify/tests/snapshots/cli_smoke/state-python.json
- desloppify/desloppify/desloppify/tests/state/test_state.py
- desloppify/desloppify/desloppify/tests/state/test_state_internal_direct.py
- devour_data/docs/docker_compose_-_ask_me_about_docker_1.md
- devour_data/docs/docker_compose_-_browse_common_faqs_10.md
- devour_data/docs/docker_compose_-_docker_compose_2.md
- devour_data/docs/docker_compose_-_explore_the_compose_file_referenc_8.md
- devour_data/docs/docker_compose_-_how_compose_works_4.md
- devour_data/docs/docker_compose_-_install_compose_5.md
- devour_data/docs/docker_compose_-_use_compose_bridge_9.md
- internal/scraper/web.go
- pkg/godocs/parser.go
Task requirements:
1. Read the immutable packet and follow `system_prompt` constraints exactly.
2. Evaluate ONLY listed files and ONLY listed dimensions for this batch.
3. Return 0-10 high-quality findings for this batch (empty array allowed).
4. Score/finding consistency is required: broader or more severe findings MUST lower dimension scores.
5. Every finding must include `related_files` with at least 2 files when possible.
6. Every finding must include `impact_scope` and `fix_scope`.
7. Every scored dimension MUST include dimension_notes with concrete evidence.
8. If a dimension score is >85, include `unreported_risk` in dimension_notes.
9. Use exactly one decimal place for every assessment and abstraction sub-axis score.
10. Do not edit repository files.
11. Return ONLY valid JSON, no markdown fences.
Scope enums:
- impact_scope: "local" | "module" | "subsystem" | "codebase"
- fix_scope: "single_edit" | "multi_file_refactor" | "architectural_change"
Output schema:
{
"batch": "Design Coherence — Mechanical Concern Signals",
"batch_index": 4,
"assessments": {"<dimension>": <0-100 with one decimal place>},
"dimension_notes": {
"<dimension>": {
"evidence": ["specific code observations"],
"impact_scope": "local|module|subsystem|codebase",
"fix_scope": "single_edit|multi_file_refactor|architectural_change",
"confidence": "high|medium|low",
"unreported_risk": "required when score >85",
"sub_axes": {"abstraction_leverage": 0-100 with one decimal place, "indirection_cost": 0-100 with one decimal place, "interface_honesty": 0-100 with one decimal place} // required for abstraction_fitness when evidence supports it
}
},
"findings": []
}
@@ -0,0 +1,125 @@
You are a focused subagent reviewer for a single holistic investigation batch.
Repository root: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour
Immutable packet: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour/.desloppify/review_packets/holistic_packet_20260224_101201.json
Batch index: 5
Batch name: Cross-cutting Sweep
Batch dimensions: error_consistency
Batch rationale: selected dimensions had no direct batch mapping; review representative cross-cutting files
Files assigned:
- internal/quality/enhanced_types.go
- internal/quality/scoring_test.go
- internal/quality/types.go
- pkg/rustdocs/parser_test.go
- internal/config/config.go
- cmd/scrape.go
- internal/quality/plugins/go/analyzers/detectors.go
- internal/quality/plugins/go/analyzers/advanced.go
- internal/scraper/web.go
- internal/quality/plugins/go/plugin.go
- internal/scheduler/scheduler.go
- cmd/push.go
- internal/scraper/localsearch_test.go
- cmd/ask.go
- internal/ai/openai.go
- internal/server/server.go
- cmd/get.go
- internal/quality/analyzers/controlflow.go
- internal/vector/store.go
- examples/demo_scrapers.go
- internal/indexer/indexer.go
- internal/scraper/openapi.go
- pkg/pythondocs/parser.go
- cmd/get_test.go
- internal/quality/scanner_test.go
- internal/scraper/localsearch.go
- internal/scraper/external/nuxtdocs.go
- internal/quality/plugins/go/analyzers/test_coverage.go
- internal/quality/scanner.go
- internal/search/engine.go
- cmd/serve.go
- internal/scraper/github.go
- internal/scraper/external/astrodocs.go
- internal/scraper/external/cloudflaredocs.go
- internal/scraper/external/dockerdocs.go
- README.md
- desloppify/desloppify/desloppify/app/commands/_show_terminal.py
- desloppify/desloppify/desloppify/app/commands/fix/apply_flow.py
- desloppify/desloppify/desloppify/app/commands/issues_cmd.py
- desloppify/desloppify/desloppify/app/commands/next.py
- desloppify/desloppify/desloppify/app/commands/resolve/selection.py
- desloppify/desloppify/desloppify/app/commands/scan/scan_reporting_llm.py
- desloppify/desloppify/desloppify/app/commands/status_parts/render.py
- desloppify/desloppify/desloppify/app/output/scorecard_parts/projection.py
- desloppify/desloppify/desloppify/engine/detectors/security/rules.py
- desloppify/desloppify/desloppify/engine/scoring_internal/subjective/core.py
- desloppify/desloppify/desloppify/engine/state_internal/resolution.py
- desloppify/desloppify/desloppify/intelligence/review/__init__.py
- desloppify/desloppify/desloppify/intelligence/review/context_internal/structure.py
- desloppify/desloppify/desloppify/intelligence/review/dimensions/data.py
- desloppify/desloppify/desloppify/intelligence/review/importing/holistic.py
- desloppify/desloppify/desloppify/languages/_shared/phases_common.py
- desloppify/desloppify/desloppify/languages/_shared/review_data/dimensions.json
- desloppify/desloppify/desloppify/languages/_shared/scaffold_detect_commands.py
- desloppify/desloppify/desloppify/languages/csharp/_parse_helpers.py
- desloppify/desloppify/desloppify/languages/csharp/commands.py
- desloppify/desloppify/desloppify/languages/csharp/deps/cli.py
- desloppify/desloppify/desloppify/languages/csharp/deps/fallback.py
- desloppify/desloppify/desloppify/languages/csharp/detectors/deps.py
- desloppify/desloppify/desloppify/languages/csharp/phases.py
- desloppify/desloppify/desloppify/languages/csharp/test_coverage.py
- desloppify/desloppify/desloppify/languages/dart/__init__.py
- desloppify/desloppify/desloppify/languages/dart/commands.py
- desloppify/desloppify/desloppify/languages/dart/detectors/deps.py
- desloppify/desloppify/desloppify/languages/dart/extractors.py
- desloppify/desloppify/desloppify/languages/dart/move.py
- desloppify/desloppify/desloppify/languages/framework/commands_base.py
- desloppify/desloppify/desloppify/languages/gdscript/__init__.py
- desloppify/desloppify/desloppify/languages/gdscript/detectors/deps.py
- desloppify/desloppify/desloppify/languages/python/__init__.py
- desloppify/desloppify/desloppify/languages/python/commands.py
- desloppify/desloppify/desloppify/languages/python/detectors/security.py
- desloppify/desloppify/desloppify/languages/python/detectors/smells.py
- desloppify/desloppify/desloppify/languages/python/move.py
- desloppify/desloppify/desloppify/languages/python/phases.py
- desloppify/desloppify/desloppify/languages/python/test_coverage.py
- desloppify/desloppify/desloppify/languages/python/tests/test_py_facade.py
- desloppify/desloppify/desloppify/languages/typescript/detectors/_smell_detectors.py
- desloppify/desloppify/desloppify/languages/typescript/detectors/_smell_effects.py
- desloppify/desloppify/desloppify/languages/typescript/detectors/deps.py
Task requirements:
1. Read the immutable packet and follow `system_prompt` constraints exactly.
2. Evaluate ONLY listed files and ONLY listed dimensions for this batch.
3. Return 0-10 high-quality findings for this batch (empty array allowed).
4. Score/finding consistency is required: broader or more severe findings MUST lower dimension scores.
5. Every finding must include `related_files` with at least 2 files when possible.
6. Every finding must include `impact_scope` and `fix_scope`.
7. Every scored dimension MUST include dimension_notes with concrete evidence.
8. If a dimension score is >85, include `unreported_risk` in dimension_notes.
9. Use exactly one decimal place for every assessment and abstraction sub-axis score.
10. Do not edit repository files.
11. Return ONLY valid JSON, no markdown fences.
Scope enums:
- impact_scope: "local" | "module" | "subsystem" | "codebase"
- fix_scope: "single_edit" | "multi_file_refactor" | "architectural_change"
Output schema:
{
"batch": "Cross-cutting Sweep",
"batch_index": 5,
"assessments": {"<dimension>": <0-100 with one decimal place>},
"dimension_notes": {
"<dimension>": {
"evidence": ["specific code observations"],
"impact_scope": "local|module|subsystem|codebase",
"fix_scope": "single_edit|multi_file_refactor|architectural_change",
"confidence": "high|medium|low",
"unreported_risk": "required when score >85",
"sub_axes": {"abstraction_leverage": 0-100 with one decimal place, "indirection_cost": 0-100 with one decimal place, "interface_honesty": 0-100 with one decimal place} // required for abstraction_fitness when evidence supports it
}
},
"findings": []
}
@@ -0,0 +1,161 @@
You are a focused subagent reviewer for a single holistic investigation batch.
Repository root: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour
Immutable packet: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour/.desloppify/review_packets/holistic_packet_20260224_101201.json
Batch index: 6
Batch name: Full Codebase Sweep
Batch dimensions: cross_module_architecture, error_consistency, abstraction_fitness, test_strategy, design_coherence
Batch rationale: thorough default: evaluate cross-cutting quality across all production files
Files assigned:
- cleanup_unused.go
- cmd/ask.go
- cmd/auto.go
- cmd/demo.go
- cmd/desloppify_proxy.go
- cmd/devour/main.go
- cmd/generate_scorecards/main.go
- cmd/get.go
- cmd/init.go
- cmd/languages.go
- cmd/push.go
- cmd/quality.go
- cmd/query.go
- cmd/realtest/main.go
- cmd/review.go
- cmd/root.go
- cmd/runtime_helpers.go
- cmd/scorecard.go
- cmd/scrape.go
- cmd/serve.go
- cmd/status.go
- cmd/sync.go
- cmd/verify.go
- examples/demo_scrapers.go
- internal/ai/ai.go
- internal/ai/openai.go
- internal/config/config.go
- internal/indexer/indexer.go
- internal/markdown/formatter.go
- internal/projectstate/state.go
- internal/quality/analyzers/controlflow.go
- internal/quality/analyzers/dataflow.go
- internal/quality/analyzers/practices.go
- internal/quality/detector.go
- internal/quality/detectors/complexity.go
- internal/quality/detectors/duplication.go
- internal/quality/detectors/naming.go
- internal/quality/docs_evidence.go
- internal/quality/enhanced_types.go
- internal/quality/languages.go
- internal/quality/plugins/go/analyzers/advanced.go
- internal/quality/plugins/go/analyzers/deadcode.go
- internal/quality/plugins/go/analyzers/detectors.go
- internal/quality/plugins/go/analyzers/security.go
- internal/quality/plugins/go/analyzers/test_coverage.go
- internal/quality/plugins/go/fixers/advanced_fixers.go
- internal/quality/plugins/go/fixers/fixers.go
- internal/quality/plugins/go/plugin.go
- internal/quality/plugins/plugin.go
- internal/quality/plugins/registry.go
- internal/quality/scanner.go
- internal/quality/scoring.go
- internal/quality/state.go
- internal/quality/types.go
- internal/scheduler/scheduler.go
- internal/scraper/external/astrodocs.go
- internal/scraper/external/cloudflaredocs.go
- internal/scraper/external/dockerdocs.go
- internal/scraper/external/godocs.go
- internal/scraper/external/javadocs.go
- internal/scraper/external/mcpdocs.go
- internal/scraper/external/nuxtdocs.go
- internal/scraper/external/pythondocs.go
- internal/scraper/external/reactdocs.go
- internal/scraper/external/register.go
- internal/scraper/external/rustdocs.go
- internal/scraper/external/springdocs.go
- internal/scraper/external/tsdocs.go
- internal/scraper/external/types.go
- internal/scraper/external/vuedocs.go
- internal/scraper/github.go
- internal/scraper/local.go
- internal/scraper/localsearch.go
- internal/scraper/normalize.go
- internal/scraper/openapi.go
- internal/scraper/register_core.go
- internal/scraper/registry_simple.go
- internal/scraper/scraper.go
- internal/scraper/web.go
- internal/scraper/wrapper.go
- internal/search/engine.go
- internal/server/server.go
- internal/storage/writer.go
- internal/ui/banner.go
- internal/ui/character.go
- internal/vector/store.go
- main.go
- pkg/astrodocs/parser.go
- pkg/astrodocs/types.go
- pkg/client/client.go
- pkg/cloudflaredocs/parser.go
- pkg/cloudflaredocs/types.go
- pkg/dockerdocs/parser.go
- pkg/dockerdocs/types.go
- pkg/godocs/parser.go
- pkg/godocs/types.go
- pkg/javadocs/parser.go
- pkg/javadocs/types.go
- pkg/mcpdocs/parser.go
- pkg/mcpdocs/types.go
- pkg/nuxtdocs/parser.go
- pkg/nuxtdocs/types.go
- pkg/parserutil/url.go
- pkg/pythondocs/parser.go
- pkg/pythondocs/types.go
- pkg/reactdocs/parser.go
- pkg/reactdocs/types.go
- pkg/rustdocs/parser.go
- pkg/rustdocs/types.go
- pkg/springdocs/parser.go
- pkg/springdocs/types.go
- pkg/tsdocs/parser.go
- pkg/tsdocs/types.go
- pkg/types/types.go
- pkg/vuedocs/parser.go
- pkg/vuedocs/types.go
Task requirements:
1. Read the immutable packet and follow `system_prompt` constraints exactly.
2. Evaluate ONLY listed files and ONLY listed dimensions for this batch.
3. Return 0-10 high-quality findings for this batch (empty array allowed).
4. Score/finding consistency is required: broader or more severe findings MUST lower dimension scores.
5. Every finding must include `related_files` with at least 2 files when possible.
6. Every finding must include `impact_scope` and `fix_scope`.
7. Every scored dimension MUST include dimension_notes with concrete evidence.
8. If a dimension score is >85, include `unreported_risk` in dimension_notes.
9. Use exactly one decimal place for every assessment and abstraction sub-axis score.
10. Do not edit repository files.
11. Return ONLY valid JSON, no markdown fences.
Scope enums:
- impact_scope: "local" | "module" | "subsystem" | "codebase"
- fix_scope: "single_edit" | "multi_file_refactor" | "architectural_change"
Output schema:
{
"batch": "Full Codebase Sweep",
"batch_index": 6,
"assessments": {"<dimension>": <0-100 with one decimal place>},
"dimension_notes": {
"<dimension>": {
"evidence": ["specific code observations"],
"impact_scope": "local|module|subsystem|codebase",
"fix_scope": "single_edit|multi_file_refactor|architectural_change",
"confidence": "high|medium|low",
"unreported_risk": "required when score >85",
"sub_axes": {"abstraction_leverage": 0-100 with one decimal place, "indirection_cost": 0-100 with one decimal place, "interface_honesty": 0-100 with one decimal place} // required for abstraction_fitness when evidence supports it
}
},
"findings": []
}
@@ -0,0 +1,40 @@
{
"batch": "Architecture & Coupling",
"batch_index": 1,
"assessments": {
"cross_module_architecture": 82.0
},
"dimension_notes": {
"cross_module_architecture": {
"evidence": [
"`internal/quality/enhanced_types.go` centralizes many unrelated boundary contracts (scoring metrics, detector transparency, narrative strategy/tools, debt tracking, config) in one package-level type hub.",
"`internal/quality/types.go` also acts as a second broad contract hub (findings, scan result, scorecard, language/config, extraction structs), overlapping the same ownership area as `enhanced_types.go` instead of separating by subdomain.",
"`internal/quality/scoring_test.go` consumes these shared package-level contracts directly, which reinforces coupling to a wide internal surface rather than narrower scorer-specific interfaces/types."
],
"impact_scope": "module",
"fix_scope": "multi_file_refactor",
"confidence": "high"
}
},
"findings": [
{
"dimension": "cross_module_architecture",
"identifier": "quality_package_contract_hub_coupling",
"summary": "Quality contracts are concentrated in broad type hubs, creating a coupling hotspot.",
"related_files": [
"internal/quality/enhanced_types.go",
"internal/quality/types.go",
"internal/quality/scoring_test.go"
],
"evidence": [
"`enhanced_types.go` and `types.go` both define large cross-cutting models for multiple concerns (analysis narrative, scoring, detector stats, config, extraction metadata) under one package boundary.",
"The two files blur subdomain ownership, so edits to one concern can force unrelated consumers in the same package to track evolving shared structs.",
"Scoring tests rely on shared package contracts rather than a tighter scorer-local contract, indicating broad internal API exposure."
],
"suggestion": "Split `internal/quality` contracts by bounded concern (for example: `qualitycore` findings/scan types, `qualityscore` scorecard/metrics, `qualitynarrative` narrative/report DTOs) and keep scorer-facing types in a narrow subpackage/API. Migrate tests to import only scorer-relevant types to reduce transitive coupling.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
}
]
}
@@ -0,0 +1,83 @@
{
"batch": "Abstractions & Dependencies",
"batch_index": 2,
"assessments": {
"abstraction_fitness": 72.0
},
"dimension_notes": {
"abstraction_fitness": {
"evidence": [
"Four external scrapers (Astro, Docker, Cloudflare, Nuxt) each reimplement the same transport/change-detection skeleton (`fetchPage`, `DetectChanges`, `generateHash`, per-page `Document` assembly) with only parser/model differences.",
"`cmd/serve.go` RPC path for `devour_scrape` mutates CLI globals (`scrapeFormat`, `scrapeOutput`, `scrapeAllowEmpty`) to call `scrapeOne` from `cmd/scrape.go`, showing a leaky abstraction where server flow depends on CLI stateful wiring.",
"`vector.Store` advertises interchangeable backends, but `NewStore` can return `ChromemStore` whose methods are all unimplemented runtime errors, so the abstraction surface overstates usable implementations."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high",
"sub_axes": {
"abstraction_leverage": 68.0,
"indirection_cost": 73.0,
"interface_honesty": 70.0
}
}
},
"findings": [
{
"dimension": "abstraction_fitness",
"identifier": "duplicated_external_scraper_skeleton",
"summary": "External docs scrapers duplicate the same orchestration instead of sharing one adapter base",
"related_files": [
"internal/scraper/external/astrodocs.go",
"internal/scraper/external/cloudflaredocs.go",
"internal/scraper/external/dockerdocs.go",
"internal/scraper/external/nuxtdocs.go"
],
"evidence": [
"Each scraper defines near-identical `fetchPage`, `DetectChanges`, and `generateHash` logic.",
"Each `Scrape` method repeats the same flow: validate URL -> fetch HTML -> parser call -> append main doc + sub-doc loop(s).",
"Differences are mostly parser/model-specific mapping, but transport/error/hash logic is copy-pasted."
],
"suggestion": "Introduce a shared docs-scraper base (or helper pipeline) for HTTP fetch + hashing + standard error handling, and keep only parser-specific mapping in per-provider adapters.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "abstraction_fitness",
"identifier": "scrape_api_leaks_cli_state",
"summary": "Server scrape RPC depends on mutable CLI globals to reuse scrape pipeline",
"related_files": [
"cmd/serve.go",
"cmd/scrape.go",
"cmd/get.go"
],
"evidence": [
"`handleServeMethod` temporarily rewrites `scrapeFormat`, `scrapeOutput`, and `scrapeAllowEmpty` before calling `scrapeOne`, then restores them.",
"`scrapeOne` is not a pure service API; it is coupled to CLI-level shared state and output behavior.",
"`cmd/get.go` also routes through `runScrape`, reinforcing that scraping orchestration is command-centric rather than a reusable application service."
],
"suggestion": "Extract a stateless scrape service function (explicit request struct + options) used by both CLI commands and RPC handlers; keep CLI flags as translation-only at command boundary.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
},
{
"dimension": "abstraction_fitness",
"identifier": "vector_store_interface_overpromises",
"summary": "Vector store abstraction exposes backends that are selectable but not actually implemented",
"related_files": [
"internal/vector/store.go",
"internal/indexer/indexer.go"
],
"evidence": [
"`NewStore` can return `ChromemStore` when config type is `chromem`.",
"All `ChromemStore` interface methods currently return `not implemented` errors.",
"`Indexer` depends only on `vector.Store`, so backend failure appears at runtime after abstraction selection instead of at wiring/validation time."
],
"suggestion": "Make backend capabilities explicit: either remove/guard `chromem` selection until complete, or return an initialization error type from store construction and enforce backend readiness before indexing starts.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
}
]
}
@@ -0,0 +1,87 @@
{
"batch": "Governance & Contracts",
"batch_index": 3,
"assessments": {
"cross_module_architecture": 81.0,
"test_strategy": 69.0
},
"dimension_notes": {
"cross_module_architecture": {
"evidence": [
"README.md defines the quality model publicly as 5 mechanical + 7 subjective dimensions, implying a stable external contract for score interpretation.",
"internal/quality/enhanced_types.go defines additional dimensions (e.g., DimensionElegance, DimensionContracts) that are not reflected in README governance docs.",
"internal/quality/types.go exposes status/state enums (including StatusIgnored) that are absent from README user-facing resolution policy examples, creating docs-vs-runtime boundary drift."
],
"impact_scope": "codebase",
"fix_scope": "architectural_change",
"confidence": "medium"
},
"test_strategy": {
"evidence": [
"internal/quality/scoring_test.go is heavily unit-focused and validates scorer internals, but does not validate public contract stability between quality types and README-documented behavior.",
"README.md publishes CLI and JSON-RPC command contracts, but assigned tests do not exercise those published interfaces as compatibility gates.",
"pkg/rustdocs/parser_test.go and internal/quality/scoring_test.go include brittle assertions (fixed positional result indexing and strict output text matching) that couple tests to implementation/presentation details."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high"
}
},
"findings": [
{
"dimension": "cross_module_architecture",
"identifier": "docs_runtime_quality_contract_drift",
"summary": "Quality dimensions/statuses drift between public docs and runtime model contracts.",
"related_files": [
"README.md",
"internal/quality/enhanced_types.go",
"internal/quality/types.go"
],
"evidence": [
"README.md frames score interpretation around a specific dimension set and user workflow.",
"internal/quality/enhanced_types.go includes extra dimension constants not represented in README contract text.",
"internal/quality/types.go includes StatusIgnored while README resolution examples only expose fixed/wontfix/false_positive paths."
],
"suggestion": "Define a single versioned public quality contract (dimensions + statuses) as source-of-truth, generate README contract tables from it, and add a CI check that fails when exported enums and published docs diverge.",
"confidence": "medium",
"impact_scope": "codebase",
"fix_scope": "architectural_change"
},
{
"dimension": "test_strategy",
"identifier": "missing_public_contract_compat_tests",
"summary": "Published CLI/RPC governance contracts are not protected by compatibility tests.",
"related_files": [
"README.md",
"internal/quality/scoring_test.go",
"internal/quality/types.go"
],
"evidence": [
"README.md documents stable command and RPC method surfaces for users.",
"internal/quality/scoring_test.go covers scoring behavior but not end-to-end contract assertions tied to documented surfaces.",
"No assigned test verifies that externally documented score/status semantics remain compatible with exported model types."
],
"suggestion": "Add contract tests that parse/validate documented command and RPC surface claims against runtime registration/types, and add schema-level golden tests for serialized quality/status payloads.",
"confidence": "high",
"impact_scope": "codebase",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "test_strategy",
"identifier": "brittle_assertions_on_order_and_formatting",
"summary": "Tests are fragile due to strict ordering and presentation-string coupling.",
"related_files": [
"internal/quality/scoring_test.go",
"pkg/rustdocs/parser_test.go"
],
"evidence": [
"pkg/rustdocs/parser_test.go asserts semantic expectations through fixed indexes (results[0], results[1], results[2]) rather than identity-based matching.",
"internal/quality/scoring_test.go asserts many exact output substrings in FormatScorecard, coupling tests to formatting text that can change without behavioral regressions."
],
"suggestion": "Refactor tests to assert semantic invariants (presence by key/kind, normalized structured fields) and reserve strict golden-output checks for explicitly versioned presentation contracts.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
}
]
}
@@ -0,0 +1,76 @@
{
"batch": "Design Coherence \u2014 Mechanical Concern Signals",
"batch_index": 4,
"assessments": {
"design_coherence": 74.0
},
"dimension_notes": {
"design_coherence": {
"evidence": [
"Command passthrough wrappers add non-functional indirection in language command modules: `languages/dart/commands.py` defines `_cmd_*_impl` plus thin `cmd_*` forwarders for each command (`cmd_large`, `cmd_complexity`, `cmd_deps`, `cmd_cycles`, `cmd_orphaned`, `cmd_dupes`).",
"C# deps module mixes parsing, graph construction, CLI arg resolution, and terminal rendering in one file (`languages/csharp/detectors/deps.py` contains core graph builders and also `cmd_deps`/`cmd_cycles` UI handlers).",
"Status rendering duplicates score-bar construction logic in two loops in `app/commands/status_parts/render.py` (same threshold/color/filled computation blocks around lines ~191-199 and ~223-231), increasing drift risk."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high"
}
},
"findings": [
{
"dimension": "design_coherence",
"identifier": "passthrough_command_wrappers_add_low_leverage_layers",
"summary": "Language command modules include thin forwarding wrappers with no behavioral value.",
"related_files": [
"desloppify/desloppify/desloppify/languages/dart/commands.py",
"desloppify/desloppify/desloppify/languages/csharp/commands.py",
"desloppify/desloppify/desloppify/languages/framework/commands_base.py"
],
"evidence": [
"`languages/dart/commands.py` creates `_cmd_*_impl` callables and then defines six `cmd_*` functions that only call the corresponding impl.",
"`languages/csharp/commands.py` also keeps thin wrappers (`cmd_large`, `cmd_complexity`, `cmd_deps`, `cmd_cycles`) that forward directly to other callables.",
"The framework already provides factory-returned callables; wrapper layers increase symbol count and call depth without adding policy."
],
"suggestion": "Assign factory outputs directly to exported command names (or build the registry directly from generated callables) and keep wrappers only where they add language-specific behavior, validation, or formatting.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "design_coherence",
"identifier": "csharp_deps_module_blends_core_and_cli_responsibilities",
"summary": "C# dependency detector file combines analysis engine logic with CLI presentation.",
"related_files": [
"desloppify/desloppify/desloppify/languages/csharp/detectors/deps.py",
"desloppify/desloppify/desloppify/languages/csharp/commands.py",
"desloppify/desloppify/desloppify/languages/_shared/scaffold_detect_commands.py"
],
"evidence": [
"`languages/csharp/detectors/deps.py` includes internal graph/parsing functions (`_parse_project_assets_references`, `_build_dep_graph_roslyn`, `build_dep_graph`) and command handlers (`cmd_deps`, `cmd_cycles`) in the same module.",
"`languages/csharp/commands.py` forwards to `cmd_deps_direct`/`cmd_cycles_deps`, meaning command routing depends on detector-module UI functions instead of a clean detector API.",
"Other language paths use shared command scaffolding (`languages/_shared/scaffold_detect_commands.py`) that separates command shell concerns from graph/detection logic."
],
"suggestion": "Split C# deps into `detectors/deps_core.py` (graph/parsing only) and command-facing adapters in `commands.py` (or scaffold factories), so detectors expose data and command modules own JSON/table rendering.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
},
{
"dimension": "design_coherence",
"identifier": "status_bar_render_logic_duplicated_in_two_paths",
"summary": "Status renderer repeats identical score-bar construction for mechanical and subjective rows.",
"related_files": [
"desloppify/desloppify/desloppify/app/commands/status_parts/render.py",
"desloppify/desloppify/desloppify/app/output/scorecard_parts/projection.py"
],
"evidence": [
"`show_dimension_table` in `status_parts/render.py` computes `filled`, score thresholds, and colored bar in one loop for mechanical dimensions and repeats the same logic in a second loop for subjective entries.",
"Duplicated threshold constants (`>=98`, `>=93`) and color composition appear in both blocks, creating a two-site maintenance point for one rendering policy."
],
"suggestion": "Extract a shared `render_score_bar(score_val, bar_len)` helper (or move the row formatting policy into scorecard projection/output helpers) and reuse it for both loops to keep display semantics consistent.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "single_edit"
}
]
}
@@ -0,0 +1,113 @@
{
"batch": "Cross-cutting Sweep",
"batch_index": 5,
"assessments": {
"error_consistency": 68.0
},
"dimension_notes": {
"error_consistency": {
"evidence": [
"RPC paths mix strict and lax decode behavior: `cmd/serve.go` returns decode errors for `devour_scrape`/`devour_ask` but ignores decode errors for `devour_query` and `devour_sync` (`_ = json.Unmarshal(...)`).",
"`cmd/serve.go` `devour_status` ignores errors from `LoadSourceState` and `EnsureIndexed` then dereferences `idxStats.Documents`, which can panic on nil when indexing fails.",
"`internal/server/server.go` exposes raw internal errors to clients (`Message: err.Error()`), while parse/invalid-request errors are normalized strings; HTTP transport always emits 400 for any RPC error, collapsing error classes.",
"`internal/ai/openai.go` does not check HTTP status before JSON decode, unlike scraper fetchers in `internal/scraper/external/*.go` and `internal/scraper/openapi.go` that explicitly gate on status codes.",
"`internal/search/engine.go` and `internal/scraper/openapi.go` mix wrapped and passthrough errors in adjacent paths, producing uneven context for operators."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high"
}
},
"findings": [
{
"dimension": "error_consistency",
"identifier": "rpc_decode_and_response_contract_drift",
"summary": "RPC methods use inconsistent decode and error-response contracts",
"related_files": [
"cmd/serve.go",
"internal/server/server.go"
],
"evidence": [
"`cmd/serve.go` ignores JSON decode errors in `devour_query`/`devour_sync` but returns decode errors in `devour_scrape`/`devour_ask`.",
"`internal/server/server.go` returns raw `err.Error()` in RPC payloads and maps all RPC errors to HTTP 400 in `writeRPC`."
],
"suggestion": "Define one RPC error policy: always validate/decode params the same way, map known classes to stable RPC codes/messages, and map transport HTTP statuses by error class (client input vs internal failure).",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "serve_status_swallows_errors_then_dereferences_nil",
"summary": "Status handler ignores errors and can panic from nil stats",
"related_files": [
"cmd/serve.go",
"internal/search/engine.go"
],
"evidence": [
"`cmd/serve.go` uses `state, _ := projectstate.LoadSourceState(...)` and `idxStats, _ := engine.EnsureIndexed(ctx)` in `devour_status`.",
"The same block unconditionally reads `idxStats.Documents` and `idxStats.LastIndexedAt`, which is unsafe if `EnsureIndexed` returned an error."
],
"suggestion": "Handle both errors explicitly in `devour_status`; return structured partial-status with an error field or fail the method uniformly, but do not ignore and dereference.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "single_edit"
},
{
"dimension": "error_consistency",
"identifier": "openai_http_error_path_not_normalized",
"summary": "OpenAI client lacks explicit HTTP status error handling",
"related_files": [
"internal/ai/openai.go",
"internal/scraper/openapi.go",
"internal/scraper/external/astrodocs.go"
],
"evidence": [
"`internal/ai/openai.go` decodes response bodies directly and only checks `embeddingResp.Error`/`chatResp.Error`; non-2xx responses without expected JSON become generic decode errors.",
"`internal/scraper/openapi.go` and external scrapers explicitly validate HTTP status before body parsing and return explicit HTTP status errors."
],
"suggestion": "Add explicit non-2xx handling in both OpenAI request paths: include status code, bounded response body excerpt, and endpoint context before JSON decode.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "mixed_wrapping_vs_passthrough_in_core_flows",
"summary": "Adjacent paths alternate between wrapped and raw error returns",
"related_files": [
"internal/search/engine.go",
"internal/scraper/openapi.go",
"internal/config/config.go"
],
"evidence": [
"`internal/search/engine.go` frequently returns raw errors (`return nil, err`) from filesystem/index operations.",
"`internal/scraper/openapi.go` similarly passes through request/file errors raw in `readSpec`, while other branches wrap with operation context.",
"`internal/config/config.go` demonstrates contextual wrapping style (`read config`, `parse config`), creating drift against less-informative paths."
],
"suggestion": "Adopt a package-level rule: wrap external boundary failures with operation context (read/write/parse/network) and preserve `%w`; apply consistently across search/openapi paths.",
"confidence": "medium",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "scanner_pipeline_fail_open_is_inconsistent",
"summary": "Quality scan path mixes fail-open and fail-fast behaviors",
"related_files": [
"internal/quality/scanner.go",
"internal/quality/plugins/go/analyzers/test_coverage.go",
"internal/quality/plugins/go/analyzers/detectors.go"
],
"evidence": [
"`internal/quality/scanner.go` logs detector failures and continues, silently reducing coverage of reported findings.",
"`test_coverage.go` returns `(nil, nil)` for missing `go` tool or missing generated coverage file, while other detector failures return explicit errors.",
"`detectors.go` has parse/count paths that silently `continue` on per-file errors, further mixing behavior."
],
"suggestion": "Standardize detector error semantics with typed outcomes (hard error, soft-skip with reason, per-file skip count) and surface these in scan results so degraded scans are explicit.",
"confidence": "medium",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
}
]
}
@@ -0,0 +1,161 @@
{
"batch": "Full Codebase Sweep",
"batch_index": 6,
"assessments": {
"cross_module_architecture": 72.0,
"error_consistency": 70.0,
"abstraction_fitness": 68.0,
"test_strategy": 64.0,
"design_coherence": 69.0
},
"dimension_notes": {
"cross_module_architecture": {
"evidence": [
"cmd/serve.go calls command handlers directly (runSync, scrapeOne) and mutates command-level globals (scrapeFormat/scrapeOutput/scrapeAllowEmpty and syncForce/syncRebuild/syncSource) before invoking them, coupling RPC transport to CLI flag state.",
"Source-type detection logic exists in both internal/scraper/scraper.go:92 (DetectSourceType) and cmd/scrape.go:314 (detectSourceType), creating boundary drift for core classification behavior."
],
"impact_scope": "subsystem",
"fix_scope": "architectural_change",
"confidence": "high"
},
"error_consistency": {
"evidence": [
"cmd/serve.go suppresses parse and subsystem errors via `_ = json.Unmarshal(...)` and `state, _ := projectstate.LoadSourceState(...)` / `idxStats, _ := engine.EnsureIndexed(...)`, while sibling paths return wrapped errors.",
"Startup failure behavior differs across modules: cmd/root.go exits process with os.Exit(1), while internal/quality/plugins/go/plugin.go init() panics on registration failure."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high"
},
"abstraction_fitness": {
"evidence": [
"Most external scraper implementations repeat near-identical transport/change-detection scaffolding (`fetchPage`, `DetectChanges`, `generateHash`) across files such as internal/scraper/external/reactdocs.go, tsdocs.go, godocs.go, rustdocs.go, and cloudflaredocs.go.",
"This repeated wrapper structure adds maintenance indirection without policy variance (same HTTP GET + user-agent + status check + body read flow)."
],
"impact_scope": "subsystem",
"fix_scope": "architectural_change",
"confidence": "high",
"sub_axes": {
"abstraction_leverage": 61.0,
"indirection_cost": 43.0,
"interface_honesty": 74.0
}
},
"test_strategy": {
"evidence": [
"High-impact command flows lack direct tests: cmd/serve.go, cmd/sync.go, cmd/query.go, cmd/push.go, cmd/verify.go have no file-level tests while they orchestrate indexing/scraping/status behavior.",
"Most external scrapers are untested at integration unit level (e.g., internal/scraper/external/{godocs,rustdocs,reactdocs,cloudflaredocs,nuxtdocs,...}.go) even though they include custom parsing-to-document mapping and network error handling."
],
"impact_scope": "codebase",
"fix_scope": "multi_file_refactor",
"confidence": "high"
},
"design_coherence": {
"evidence": [
"cmd/serve.go:71-210 centralizes multiple unrelated workflows (query/status/scrape/ask/sync) in one switch, with request parsing, domain orchestration, and response formatting mixed in a single function.",
"The same function also performs cross-command mutable state choreography (temporarily overriding scrape/sync globals), combining transport logic with command execution mechanics."
],
"impact_scope": "module",
"fix_scope": "multi_file_refactor",
"confidence": "high"
}
},
"findings": [
{
"dimension": "cross_module_architecture",
"identifier": "rpc_cli_global_state_coupling",
"summary": "RPC handlers mutate CLI global flags to execute workflows across command boundaries.",
"related_files": [
"cmd/serve.go",
"cmd/scrape.go",
"cmd/sync.go"
],
"evidence": [
"cmd/serve.go sets scrapeFormat/scrapeOutput/scrapeAllowEmpty before calling scrapeOne and restores afterward.",
"cmd/serve.go sets syncForce/syncRebuild/syncSource before calling runSync and restores afterward."
],
"suggestion": "Extract scrape/sync/query use-cases into stateless service functions (input structs + return structs), call them from both Cobra commands and RPC handlers, and remove shared mutable command globals from runtime execution paths.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
},
{
"dimension": "error_consistency",
"identifier": "silent_error_drops_in_rpc_paths",
"summary": "Several RPC paths ignore parse/index/state errors, producing inconsistent failure contracts.",
"related_files": [
"cmd/serve.go",
"internal/search/engine.go",
"internal/projectstate/state.go"
],
"evidence": [
"cmd/serve.go ignores JSON unmarshal errors for devour_query/devour_sync (`_ = json.Unmarshal(...)`).",
"cmd/serve.go ignores errors from LoadSourceState and EnsureIndexed, yet reads fields from returned values.",
"Other command paths generally propagate wrapped errors with `%w`."
],
"suggestion": "Treat request decode, source-state load, and index-check failures uniformly: return explicit wrapped errors from each branch and avoid underscore-discarded errors in RPC handlers.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "single_edit"
},
{
"dimension": "abstraction_fitness",
"identifier": "external_scraper_transport_duplication",
"summary": "External scrapers duplicate the same HTTP fetch/hash/change-detection boilerplate.",
"related_files": [
"internal/scraper/external/reactdocs.go",
"internal/scraper/external/tsdocs.go",
"internal/scraper/external/godocs.go",
"internal/scraper/external/rustdocs.go",
"internal/scraper/external/cloudflaredocs.go"
],
"evidence": [
"Each scraper reimplements equivalent `fetchPage` with identical request/header/response-body flow.",
"Each scraper reimplements nearly identical `DetectChanges` and `generateHash` logic."
],
"suggestion": "Introduce a shared base helper (e.g., `fetchPage`, `contentHash`, `detectChanges`) in internal/scraper/external/types.go or a dedicated internal transport module, then keep per-scraper files focused on parser-to-document mapping.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "test_strategy",
"identifier": "missing_tests_on_orchestration_and_adapters",
"summary": "Critical orchestration commands and most external adapters have no direct tests.",
"related_files": [
"cmd/serve.go",
"cmd/sync.go",
"cmd/query.go",
"internal/scraper/external/godocs.go",
"internal/scraper/external/reactdocs.go"
],
"evidence": [
"No *_test.go peers for cmd/serve.go, cmd/sync.go, cmd/query.go despite high-risk orchestration behavior.",
"Only one external scraper has direct tests while many adapter implementations contain custom mapping and error-path logic."
],
"suggestion": "Add table-driven tests for RPC method branches and error propagation in cmd/serve.go, plus httptest-based adapter contract tests covering fetch failures, parser errors, and document mapping for each external scraper family.",
"confidence": "high",
"impact_scope": "codebase",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "design_coherence",
"identifier": "serve_handler_multi_responsibility_switch",
"summary": "Single handler function mixes transport, parsing, orchestration, and mutable state management.",
"related_files": [
"cmd/serve.go",
"cmd/scrape.go",
"cmd/sync.go",
"cmd/query.go"
],
"evidence": [
"handleServeMethod contains method routing plus per-method request schemas, domain calls, and response shaping in one body.",
"The function coordinates temporary mutation of command globals to reuse CLI code paths."
],
"suggestion": "Split each RPC method into dedicated handler functions (e.g., handleQueryRPC, handleScrapeRPC, handleSyncRPC) that depend on explicit service interfaces instead of command globals.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
}
]
}
@@ -0,0 +1,85 @@
{
"assessments": {
"error_consistency": 56.9
},
"dimension_notes": {
"error_consistency": {
"evidence": [
"Quality analyzers mix hard failures and silent skips: `TestCoverageDetector.Detect` returns `nil, nil` when `go` is missing and when coverage file is still absent (`internal/quality/plugins/go/analyzers/test_coverage.go:38-41,50-52`), while other paths return wrapped errors.",
"Multiple detector paths suppress parse/read failures instead of propagating context (`internal/quality/plugins/go/analyzers/detectors.go:44-47,111-114`).",
"Scraper modules differ on partial failure handling: `LocalScraper.Scrape` drops document conversion errors (`internal/scraper/local.go:90-93`), while `WebScraper`/`LocalSearchScraper` accumulate and return aggregated scrape errors (`internal/scraper/web.go:257-261`, `internal/scraper/localsearch.go:141-145`).",
"HTTP error context is uneven: external scrapers return bare `HTTP <code>` (`internal/scraper/external/astrodocs.go:86-88`) while local search includes status plus response body excerpt (`internal/scraper/localsearch.go:212-217`).",
"Plugin startup uses process-terminating panic on registration failure (`internal/quality/plugins/go/plugin.go:360-363`) instead of the recoverable error style used elsewhere in scanner execution (`internal/quality/scanner.go:109-116`)."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high",
"unreported_risk": ""
}
},
"findings": [
{
"dimension": "error_consistency",
"identifier": "quality_detectors_silent_failure_paths",
"summary": "Quality detectors silently skip failures in some paths but fail loudly in others",
"related_files": [
"internal/quality/plugins/go/analyzers/test_coverage.go",
"internal/quality/plugins/go/analyzers/detectors.go"
],
"evidence": [
"`TestCoverageDetector.Detect` returns `nil, nil` when `go` is unavailable and when `coverage.out` is missing after test run, making tooling/environment failures indistinguishable from a clean run.",
"`LargeFileDetector.Detect` continues on `countLines` error and `GodStructDetector.analyzeFile` returns nil on parse error, suppressing scanner visibility into unreadable/unparseable files."
],
"suggestion": "Standardize detector contracts: return typed non-fatal diagnostics (or wrapped errors) for tool-missing/parse/read failures, and let scanner decide whether to downgrade to warnings instead of silently returning no findings.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "scraper_partial_failure_contract_drift",
"summary": "Scraper implementations disagree on whether per-item errors are surfaced or dropped",
"related_files": [
"internal/scraper/local.go",
"internal/scraper/web.go",
"internal/scraper/localsearch.go"
],
"evidence": [
"`LocalScraper.Scrape` ignores `fileToDocument` errors by returning nil in the walk callback and continues without recording failures.",
"`WebScraper` and `LocalSearchScraper` collect per-URL failures and return an explicit error when no documents are produced."
],
"suggestion": "Adopt one partial-failure policy across scrapers (for example: collect bounded per-item errors and return them when output is empty, optionally include warnings when output is partial). Implement this via a shared helper used by local/web/localsearch scrapers.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "http_error_context_inconsistent",
"summary": "HTTP failure messages vary from rich context to opaque status-only strings",
"related_files": [
"internal/scraper/external/astrodocs.go",
"internal/scraper/external/cloudflaredocs.go",
"internal/scraper/external/dockerdocs.go",
"internal/scraper/external/nuxtdocs.go",
"internal/scraper/localsearch.go"
],
"evidence": [
"External docs scrapers return generic `HTTP %d` without endpoint or response snippet, reducing traceability during failures.",
"Local search includes status code and trimmed response body in error messages, providing significantly better debugging context."
],
"suggestion": "Create a shared HTTP error formatter for scraper clients that includes URL host/path, status code, and a bounded response-body excerpt; update all external scraper fetchers to use it.",
"confidence": "medium",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
}
],
"review_quality": {
"batch_count": 1,
"dimension_coverage": 1.0,
"evidence_density": 1.667,
"high_score_without_risk": 0,
"finding_pressure": 6.604,
"dimensions_with_findings": 1
}
}
@@ -0,0 +1,125 @@
You are a focused subagent reviewer for a single holistic investigation batch.
Repository root: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour
Immutable packet: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour/.desloppify/review_packets/holistic_packet_20260224_101740.json
Batch index: 1
Batch name: Cross-cutting Sweep
Batch dimensions: error_consistency
Batch rationale: selected dimensions had no direct batch mapping; review representative cross-cutting files
Files assigned:
- internal/quality/enhanced_types.go
- internal/quality/scoring_test.go
- internal/quality/types.go
- pkg/rustdocs/parser_test.go
- internal/config/config.go
- cmd/scrape.go
- internal/quality/plugins/go/analyzers/detectors.go
- internal/quality/plugins/go/analyzers/advanced.go
- internal/scraper/web.go
- internal/quality/plugins/go/plugin.go
- internal/scheduler/scheduler.go
- cmd/push.go
- internal/scraper/localsearch_test.go
- cmd/ask.go
- internal/ai/openai.go
- internal/server/server.go
- cmd/get.go
- internal/quality/analyzers/controlflow.go
- internal/vector/store.go
- examples/demo_scrapers.go
- internal/indexer/indexer.go
- internal/scraper/openapi.go
- pkg/pythondocs/parser.go
- cmd/get_test.go
- internal/quality/scanner_test.go
- internal/scraper/localsearch.go
- cmd/serve.go
- internal/scraper/external/nuxtdocs.go
- internal/quality/plugins/go/analyzers/test_coverage.go
- internal/quality/scanner.go
- internal/search/engine.go
- internal/scraper/github.go
- internal/scraper/external/astrodocs.go
- internal/scraper/external/cloudflaredocs.go
- internal/scraper/external/dockerdocs.go
- internal/quality/plugins/go/fixers/advanced_fixers.go
- cleanup_unused.go
- main.go
- cmd/ask_test.go
- cmd/auto.go
- internal/scraper/local.go
- internal/scraper/local_test.go
- README.md
- .desloppify/config.json
- .desloppify/query.json
- .desloppify/subagents/runs/20260223_100953/prompts/batch-1.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-2.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-3.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-4.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-5.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-6.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-1.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-2.md
- .github/workflows/ci.yml
- AGENTS.md
- cmd/devour_enhanced.py
- cmd/devour_enhanced_fixed.py
- cmd/devour_enhanced_v2.py
- desloppify/desloppify/desloppify/app/commands/_show_terminal.py
- desloppify/desloppify/desloppify/app/commands/fix/apply_flow.py
- desloppify/desloppify/desloppify/app/commands/issues_cmd.py
- desloppify/desloppify/desloppify/app/commands/next.py
- desloppify/desloppify/desloppify/app/commands/resolve/selection.py
- desloppify/desloppify/desloppify/app/commands/scan/scan_reporting_llm.py
- desloppify/desloppify/desloppify/app/commands/status_parts/render.py
- desloppify/desloppify/desloppify/app/output/scorecard_parts/projection.py
- desloppify/desloppify/desloppify/engine/detectors/security/rules.py
- desloppify/desloppify/desloppify/engine/scoring_internal/subjective/core.py
- desloppify/desloppify/desloppify/engine/state_internal/resolution.py
- desloppify/desloppify/desloppify/intelligence/review/__init__.py
- desloppify/desloppify/desloppify/intelligence/review/context_internal/structure.py
- desloppify/desloppify/desloppify/intelligence/review/dimensions/data.py
- desloppify/desloppify/desloppify/intelligence/review/importing/holistic.py
- desloppify/desloppify/desloppify/languages/_shared/phases_common.py
- desloppify/desloppify/desloppify/languages/_shared/review_data/dimensions.json
- desloppify/desloppify/desloppify/languages/_shared/scaffold_detect_commands.py
- desloppify/desloppify/desloppify/languages/csharp/_parse_helpers.py
- desloppify/desloppify/desloppify/languages/csharp/commands.py
- desloppify/desloppify/desloppify/languages/csharp/deps/cli.py
- desloppify/desloppify/desloppify/languages/csharp/deps/fallback.py
Task requirements:
1. Read the immutable packet and follow `system_prompt` constraints exactly.
2. Evaluate ONLY listed files and ONLY listed dimensions for this batch.
3. Return 0-10 high-quality findings for this batch (empty array allowed).
4. Score/finding consistency is required: broader or more severe findings MUST lower dimension scores.
5. Every finding must include `related_files` with at least 2 files when possible.
6. Every finding must include `impact_scope` and `fix_scope`.
7. Every scored dimension MUST include dimension_notes with concrete evidence.
8. If a dimension score is >85, include `unreported_risk` in dimension_notes.
9. Use exactly one decimal place for every assessment and abstraction sub-axis score.
10. Do not edit repository files.
11. Return ONLY valid JSON, no markdown fences.
Scope enums:
- impact_scope: "local" | "module" | "subsystem" | "codebase"
- fix_scope: "single_edit" | "multi_file_refactor" | "architectural_change"
Output schema:
{
"batch": "Cross-cutting Sweep",
"batch_index": 1,
"assessments": {"<dimension>": <0-100 with one decimal place>},
"dimension_notes": {
"<dimension>": {
"evidence": ["specific code observations"],
"impact_scope": "local|module|subsystem|codebase",
"fix_scope": "single_edit|multi_file_refactor|architectural_change",
"confidence": "high|medium|low",
"unreported_risk": "required when score >85",
"sub_axes": {"abstraction_leverage": 0-100 with one decimal place, "indirection_cost": 0-100 with one decimal place, "interface_honesty": 0-100 with one decimal place} // required for abstraction_fitness when evidence supports it
}
},
"findings": []
}
@@ -0,0 +1,78 @@
{
"batch": "Cross-cutting Sweep",
"batch_index": 1,
"assessments": {
"error_consistency": 73.0
},
"dimension_notes": {
"error_consistency": {
"evidence": [
"Quality analyzers mix hard failures and silent skips: `TestCoverageDetector.Detect` returns `nil, nil` when `go` is missing and when coverage file is still absent (`internal/quality/plugins/go/analyzers/test_coverage.go:38-41,50-52`), while other paths return wrapped errors.",
"Multiple detector paths suppress parse/read failures instead of propagating context (`internal/quality/plugins/go/analyzers/detectors.go:44-47,111-114`).",
"Scraper modules differ on partial failure handling: `LocalScraper.Scrape` drops document conversion errors (`internal/scraper/local.go:90-93`), while `WebScraper`/`LocalSearchScraper` accumulate and return aggregated scrape errors (`internal/scraper/web.go:257-261`, `internal/scraper/localsearch.go:141-145`).",
"HTTP error context is uneven: external scrapers return bare `HTTP <code>` (`internal/scraper/external/astrodocs.go:86-88`) while local search includes status plus response body excerpt (`internal/scraper/localsearch.go:212-217`).",
"Plugin startup uses process-terminating panic on registration failure (`internal/quality/plugins/go/plugin.go:360-363`) instead of the recoverable error style used elsewhere in scanner execution (`internal/quality/scanner.go:109-116`)."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high"
}
},
"findings": [
{
"dimension": "error_consistency",
"identifier": "quality_detectors_silent_failure_paths",
"summary": "Quality detectors silently skip failures in some paths but fail loudly in others",
"related_files": [
"internal/quality/plugins/go/analyzers/test_coverage.go",
"internal/quality/plugins/go/analyzers/detectors.go"
],
"evidence": [
"`TestCoverageDetector.Detect` returns `nil, nil` when `go` is unavailable and when `coverage.out` is missing after test run, making tooling/environment failures indistinguishable from a clean run.",
"`LargeFileDetector.Detect` continues on `countLines` error and `GodStructDetector.analyzeFile` returns nil on parse error, suppressing scanner visibility into unreadable/unparseable files."
],
"suggestion": "Standardize detector contracts: return typed non-fatal diagnostics (or wrapped errors) for tool-missing/parse/read failures, and let scanner decide whether to downgrade to warnings instead of silently returning no findings.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "scraper_partial_failure_contract_drift",
"summary": "Scraper implementations disagree on whether per-item errors are surfaced or dropped",
"related_files": [
"internal/scraper/local.go",
"internal/scraper/web.go",
"internal/scraper/localsearch.go"
],
"evidence": [
"`LocalScraper.Scrape` ignores `fileToDocument` errors by returning nil in the walk callback and continues without recording failures.",
"`WebScraper` and `LocalSearchScraper` collect per-URL failures and return an explicit error when no documents are produced."
],
"suggestion": "Adopt one partial-failure policy across scrapers (for example: collect bounded per-item errors and return them when output is empty, optionally include warnings when output is partial). Implement this via a shared helper used by local/web/localsearch scrapers.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "http_error_context_inconsistent",
"summary": "HTTP failure messages vary from rich context to opaque status-only strings",
"related_files": [
"internal/scraper/external/astrodocs.go",
"internal/scraper/external/cloudflaredocs.go",
"internal/scraper/external/dockerdocs.go",
"internal/scraper/external/nuxtdocs.go",
"internal/scraper/localsearch.go"
],
"evidence": [
"External docs scrapers return generic `HTTP %d` without endpoint or response snippet, reducing traceability during failures.",
"Local search includes status code and trimmed response body in error messages, providing significantly better debugging context."
],
"suggestion": "Create a shared HTTP error formatter for scraper clients that includes URL host/path, status code, and a bounded response-body excerpt; update all external scraper fetchers to use it.",
"confidence": "medium",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
}
]
}
@@ -0,0 +1,117 @@
{
"assessments": {
"error_consistency": 50.0
},
"dimension_notes": {
"error_consistency": {
"evidence": [
"cmd/scrape.go uses `wrapErr` for YAML fallback parsing but returns `%w` with outer `err` (`return fmt.Errorf(\"parse sources file: %w\", err)`), which can misreport or drop the actual parse failure.",
"cmd/ask.go drops `localErr` (`localRanked = nil`) and later reports only live fetch errors, so a failed local retrieval path is silently omitted from the returned error contract.",
"internal/search/engine.go frequently returns raw errors from filesystem/JSON boundaries (`return nil, err`) while nearby modules like internal/indexer/indexer.go and cmd/serve.go consistently wrap with operation context.",
"internal/quality/plugins/go/analyzers/test_coverage.go returns `nil, nil` for missing `go` tool and missing `coverage.out`, but returns hard errors for other failures, creating mixed silent-skip vs fail-fast behavior in one detector family.",
"internal/quality/plugins/go/plugin.go panics in package init on registration failure, while command entrypoints use returned errors + controlled process exit (cmd/root.go), producing inconsistent failure modes across startup paths."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high",
"unreported_risk": ""
}
},
"findings": [
{
"dimension": "error_consistency",
"identifier": "wrong_error_wrapped_in_sources_fallback_parse",
"summary": "Fallback YAML parse wraps the wrong error variable, losing true parse context.",
"related_files": [
"cmd/scrape.go",
"internal/config/config.go"
],
"evidence": [
"cmd/scrape.go:153-155 checks `wrapErr` but returns `%w` with `err` from a different scope.",
"internal/config/config.go consistently wraps the current failing error with `%w` at parse/read boundaries, showing intended local convention."
],
"suggestion": "In cmd/scrape.go, change the return to `fmt.Errorf(\"parse sources file: %w\", wrapErr)` so the emitted error always preserves the actual fallback parse failure.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "single_edit"
},
{
"dimension": "error_consistency",
"identifier": "local_ask_retrieval_error_is_silently_dropped",
"summary": "Local retrieval failures are discarded, so final errors hide one failing path.",
"related_files": [
"cmd/ask.go",
"cmd/serve.go"
],
"evidence": [
"cmd/ask.go:122-125 sets `localRanked = nil` on `localErr` and does not append/report that error.",
"cmd/ask.go:145-147 returns only `fetchErrors` for failure messaging, excluding local retrieval failure cause.",
"cmd/serve.go wraps and propagates lower-level errors with operation context (`run devour_ask search: %w`), showing a stricter error propagation pattern at adjacent boundary code."
],
"suggestion": "Capture `localErr` into the same error aggregation path (or return immediately with context) so both local and live retrieval failures are visible to callers.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "search_engine_drops_operation_context_on_io_errors",
"summary": "Search engine returns raw IO/JSON errors without operation context at key boundaries.",
"related_files": [
"internal/search/engine.go",
"internal/indexer/indexer.go"
],
"evidence": [
"internal/search/engine.go returns bare errors at several boundaries (e.g., MkdirAll/listDocFiles/os.ReadFile/json.Unmarshal paths).",
"internal/indexer/indexer.go wraps errors with explicit operation strings (`failed to generate embeddings`, `failed to add to vector store`), improving traceability."
],
"suggestion": "Wrap engine boundary errors with operation-specific context (`fmt.Errorf(\"ensure index metadata: %w\", err)`, etc.) consistently across Rebuild/EnsureIndexed/Search.",
"confidence": "medium",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "test_coverage_detector_mixes_silent_skip_and_hard_failure",
"summary": "Coverage detector silently skips some environment failures but fails hard on others.",
"related_files": [
"internal/quality/plugins/go/analyzers/test_coverage.go",
"internal/quality/scanner.go"
],
"evidence": [
"internal/quality/plugins/go/analyzers/test_coverage.go:39-41 returns `nil, nil` if `go` is unavailable; lines 50-51 also return `nil, nil` when profile is missing.",
"The same detector returns hard errors for command execution failures (`failed to run test coverage`) and parse failures, yielding mixed failure contracts.",
"internal/quality/scanner.go aggregates detector errors, so silent `nil,nil` prevents scanner-level visibility for some failure modes."
],
"suggestion": "Standardize detector contract: either return explicit typed 'skipped' metadata/finding for non-actionable environment gaps, or return wrapped errors consistently so scanner can report them predictably.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
},
{
"dimension": "error_consistency",
"identifier": "plugin_registration_uses_panic_while_cli_paths_return_errors",
"summary": "Plugin init panics on registration failure instead of using returned error flow.",
"related_files": [
"internal/quality/plugins/go/plugin.go",
"cmd/root.go"
],
"evidence": [
"internal/quality/plugins/go/plugin.go:360-363 panics in `init()` when registration fails.",
"cmd/root.go:41-45 follows controlled error propagation (`Execute` prints error and exits), indicating a different process-level failure strategy."
],
"suggestion": "Replace panic-based registration with explicit initialization returning errors (or a centralized startup validation step) so failures follow the same surfaced error path as other startup errors.",
"confidence": "medium",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
}
],
"review_quality": {
"batch_count": 1,
"dimension_coverage": 1.0,
"evidence_density": 1.0,
"high_score_without_risk": 0,
"finding_pressure": 12.12,
"dimensions_with_findings": 1
}
}
@@ -0,0 +1,125 @@
You are a focused subagent reviewer for a single holistic investigation batch.
Repository root: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour
Immutable packet: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour/.desloppify/review_packets/holistic_packet_20260224_102123.json
Batch index: 1
Batch name: Cross-cutting Sweep
Batch dimensions: error_consistency
Batch rationale: selected dimensions had no direct batch mapping; review representative cross-cutting files
Files assigned:
- internal/quality/enhanced_types.go
- internal/quality/scoring_test.go
- internal/quality/types.go
- pkg/rustdocs/parser_test.go
- internal/config/config.go
- cmd/scrape.go
- internal/quality/plugins/go/analyzers/detectors.go
- internal/quality/plugins/go/analyzers/advanced.go
- internal/scraper/web.go
- internal/quality/plugins/go/plugin.go
- internal/scheduler/scheduler.go
- cmd/push.go
- internal/scraper/localsearch_test.go
- cmd/ask.go
- internal/ai/openai.go
- internal/server/server.go
- cmd/get.go
- internal/quality/analyzers/controlflow.go
- internal/vector/store.go
- examples/demo_scrapers.go
- internal/indexer/indexer.go
- internal/scraper/openapi.go
- pkg/pythondocs/parser.go
- cmd/get_test.go
- internal/quality/scanner_test.go
- internal/scraper/localsearch.go
- cmd/serve.go
- internal/scraper/external/nuxtdocs.go
- internal/quality/plugins/go/analyzers/test_coverage.go
- internal/quality/scanner.go
- internal/search/engine.go
- internal/scraper/github.go
- internal/scraper/external/astrodocs.go
- internal/scraper/external/cloudflaredocs.go
- internal/scraper/external/dockerdocs.go
- internal/quality/plugins/go/fixers/advanced_fixers.go
- cleanup_unused.go
- main.go
- cmd/ask_test.go
- cmd/auto.go
- internal/scraper/local.go
- internal/scraper/local_test.go
- README.md
- .desloppify/config.json
- .desloppify/query.json
- .desloppify/subagents/runs/20260223_100953/prompts/batch-1.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-2.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-3.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-4.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-5.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-6.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-1.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-2.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-5.md
- .github/workflows/ci.yml
- AGENTS.md
- cmd/devour_enhanced.py
- cmd/devour_enhanced_fixed.py
- cmd/devour_enhanced_v2.py
- desloppify/desloppify/desloppify/app/commands/_show_terminal.py
- desloppify/desloppify/desloppify/app/commands/fix/apply_flow.py
- desloppify/desloppify/desloppify/app/commands/issues_cmd.py
- desloppify/desloppify/desloppify/app/commands/next.py
- desloppify/desloppify/desloppify/app/commands/resolve/selection.py
- desloppify/desloppify/desloppify/app/commands/scan/scan_reporting_llm.py
- desloppify/desloppify/desloppify/app/commands/status_parts/render.py
- desloppify/desloppify/desloppify/app/output/scorecard_parts/projection.py
- desloppify/desloppify/desloppify/engine/detectors/security/rules.py
- desloppify/desloppify/desloppify/engine/scoring_internal/subjective/core.py
- desloppify/desloppify/desloppify/engine/state_internal/resolution.py
- desloppify/desloppify/desloppify/intelligence/review/__init__.py
- desloppify/desloppify/desloppify/intelligence/review/context_internal/structure.py
- desloppify/desloppify/desloppify/intelligence/review/dimensions/data.py
- desloppify/desloppify/desloppify/intelligence/review/importing/holistic.py
- desloppify/desloppify/desloppify/languages/_shared/phases_common.py
- desloppify/desloppify/desloppify/languages/_shared/review_data/dimensions.json
- desloppify/desloppify/desloppify/languages/_shared/scaffold_detect_commands.py
- desloppify/desloppify/desloppify/languages/csharp/_parse_helpers.py
- desloppify/desloppify/desloppify/languages/csharp/commands.py
- desloppify/desloppify/desloppify/languages/csharp/deps/cli.py
Task requirements:
1. Read the immutable packet and follow `system_prompt` constraints exactly.
2. Evaluate ONLY listed files and ONLY listed dimensions for this batch.
3. Return 0-10 high-quality findings for this batch (empty array allowed).
4. Score/finding consistency is required: broader or more severe findings MUST lower dimension scores.
5. Every finding must include `related_files` with at least 2 files when possible.
6. Every finding must include `impact_scope` and `fix_scope`.
7. Every scored dimension MUST include dimension_notes with concrete evidence.
8. If a dimension score is >85, include `unreported_risk` in dimension_notes.
9. Use exactly one decimal place for every assessment and abstraction sub-axis score.
10. Do not edit repository files.
11. Return ONLY valid JSON, no markdown fences.
Scope enums:
- impact_scope: "local" | "module" | "subsystem" | "codebase"
- fix_scope: "single_edit" | "multi_file_refactor" | "architectural_change"
Output schema:
{
"batch": "Cross-cutting Sweep",
"batch_index": 1,
"assessments": {"<dimension>": <0-100 with one decimal place>},
"dimension_notes": {
"<dimension>": {
"evidence": ["specific code observations"],
"impact_scope": "local|module|subsystem|codebase",
"fix_scope": "single_edit|multi_file_refactor|architectural_change",
"confidence": "high|medium|low",
"unreported_risk": "required when score >85",
"sub_axes": {"abstraction_leverage": 0-100 with one decimal place, "indirection_cost": 0-100 with one decimal place, "interface_honesty": 0-100 with one decimal place} // required for abstraction_fitness when evidence supports it
}
},
"findings": []
}
@@ -0,0 +1,110 @@
{
"batch": "Cross-cutting Sweep",
"batch_index": 1,
"assessments": {
"error_consistency": 74.0
},
"dimension_notes": {
"error_consistency": {
"evidence": [
"cmd/scrape.go uses `wrapErr` for YAML fallback parsing but returns `%w` with outer `err` (`return fmt.Errorf(\"parse sources file: %w\", err)`), which can misreport or drop the actual parse failure.",
"cmd/ask.go drops `localErr` (`localRanked = nil`) and later reports only live fetch errors, so a failed local retrieval path is silently omitted from the returned error contract.",
"internal/search/engine.go frequently returns raw errors from filesystem/JSON boundaries (`return nil, err`) while nearby modules like internal/indexer/indexer.go and cmd/serve.go consistently wrap with operation context.",
"internal/quality/plugins/go/analyzers/test_coverage.go returns `nil, nil` for missing `go` tool and missing `coverage.out`, but returns hard errors for other failures, creating mixed silent-skip vs fail-fast behavior in one detector family.",
"internal/quality/plugins/go/plugin.go panics in package init on registration failure, while command entrypoints use returned errors + controlled process exit (cmd/root.go), producing inconsistent failure modes across startup paths."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high"
}
},
"findings": [
{
"dimension": "error_consistency",
"identifier": "wrong_error_wrapped_in_sources_fallback_parse",
"summary": "Fallback YAML parse wraps the wrong error variable, losing true parse context.",
"related_files": [
"cmd/scrape.go",
"internal/config/config.go"
],
"evidence": [
"cmd/scrape.go:153-155 checks `wrapErr` but returns `%w` with `err` from a different scope.",
"internal/config/config.go consistently wraps the current failing error with `%w` at parse/read boundaries, showing intended local convention."
],
"suggestion": "In cmd/scrape.go, change the return to `fmt.Errorf(\"parse sources file: %w\", wrapErr)` so the emitted error always preserves the actual fallback parse failure.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "single_edit"
},
{
"dimension": "error_consistency",
"identifier": "local_ask_retrieval_error_is_silently_dropped",
"summary": "Local retrieval failures are discarded, so final errors hide one failing path.",
"related_files": [
"cmd/ask.go",
"cmd/serve.go"
],
"evidence": [
"cmd/ask.go:122-125 sets `localRanked = nil` on `localErr` and does not append/report that error.",
"cmd/ask.go:145-147 returns only `fetchErrors` for failure messaging, excluding local retrieval failure cause.",
"cmd/serve.go wraps and propagates lower-level errors with operation context (`run devour_ask search: %w`), showing a stricter error propagation pattern at adjacent boundary code."
],
"suggestion": "Capture `localErr` into the same error aggregation path (or return immediately with context) so both local and live retrieval failures are visible to callers.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "search_engine_drops_operation_context_on_io_errors",
"summary": "Search engine returns raw IO/JSON errors without operation context at key boundaries.",
"related_files": [
"internal/search/engine.go",
"internal/indexer/indexer.go"
],
"evidence": [
"internal/search/engine.go returns bare errors at several boundaries (e.g., MkdirAll/listDocFiles/os.ReadFile/json.Unmarshal paths).",
"internal/indexer/indexer.go wraps errors with explicit operation strings (`failed to generate embeddings`, `failed to add to vector store`), improving traceability."
],
"suggestion": "Wrap engine boundary errors with operation-specific context (`fmt.Errorf(\"ensure index metadata: %w\", err)`, etc.) consistently across Rebuild/EnsureIndexed/Search.",
"confidence": "medium",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "test_coverage_detector_mixes_silent_skip_and_hard_failure",
"summary": "Coverage detector silently skips some environment failures but fails hard on others.",
"related_files": [
"internal/quality/plugins/go/analyzers/test_coverage.go",
"internal/quality/scanner.go"
],
"evidence": [
"internal/quality/plugins/go/analyzers/test_coverage.go:39-41 returns `nil, nil` if `go` is unavailable; lines 50-51 also return `nil, nil` when profile is missing.",
"The same detector returns hard errors for command execution failures (`failed to run test coverage`) and parse failures, yielding mixed failure contracts.",
"internal/quality/scanner.go aggregates detector errors, so silent `nil,nil` prevents scanner-level visibility for some failure modes."
],
"suggestion": "Standardize detector contract: either return explicit typed 'skipped' metadata/finding for non-actionable environment gaps, or return wrapped errors consistently so scanner can report them predictably.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
},
{
"dimension": "error_consistency",
"identifier": "plugin_registration_uses_panic_while_cli_paths_return_errors",
"summary": "Plugin init panics on registration failure instead of using returned error flow.",
"related_files": [
"internal/quality/plugins/go/plugin.go",
"cmd/root.go"
],
"evidence": [
"internal/quality/plugins/go/plugin.go:360-363 panics in `init()` when registration fails.",
"cmd/root.go:41-45 follows controlled error propagation (`Execute` prints error and exits), indicating a different process-level failure strategy."
],
"suggestion": "Replace panic-based registration with explicit initialization returning errors (or a centralized startup validation step) so failures follow the same surfaced error path as other startup errors.",
"confidence": "medium",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
}
]
}
@@ -0,0 +1,106 @@
{
"assessments": {
"error_consistency": 47.2
},
"dimension_notes": {
"error_consistency": {
"evidence": [
"Multiple analyzers silently suppress parse/read failures instead of returning contextual errors (internal/quality/plugins/go/analyzers/detectors.go:112-114, 240-245; internal/quality/plugins/go/analyzers/test_coverage.go:211-214).",
"Scraper entrypoints mix contextual wrapping and raw passthrough in similar boundary operations (internal/scraper/openapi.go:43-50 returns raw err; internal/scraper/github.go:29-37 returns raw err; internal/scraper/external/nuxtdocs.go:39-45 wraps with %w).",
"Several paths collapse structured errors into joined strings, reducing traceability and unwrapping (internal/scraper/web.go:259; internal/scraper/localsearch.go:143; cmd/ask.go:320,340).",
"Some runtime errors are dropped and processing continues without durable signal (internal/server/server.go:213 ignores encode error; internal/scraper/local.go:90-93 drops file parse errors; internal/search/engine.go:134-137 skips parse failures)."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high",
"unreported_risk": ""
}
},
"findings": [
{
"dimension": "error_consistency",
"identifier": "silent_parse_failures_in_analyzers",
"summary": "Analyzer parse/read errors are silently converted to no findings.",
"related_files": [
"internal/quality/plugins/go/analyzers/detectors.go",
"internal/quality/plugins/go/analyzers/test_coverage.go",
"internal/quality/scanner.go"
],
"evidence": [
"GodStructDetector.analyzeFile returns nil on parser.ParseFile error (detectors.go:112-114).",
"DebugLogDetector.analyzeFile also returns nil on parse failure (detectors.go:240-241).",
"UntestedFuncDetector returns (nil, nil) when coverage.out read fails (test_coverage.go:211-214), making failure indistinguishable from success.",
"Scanner loop logs detector failure and continues (scanner.go:77-79), so detector internals that swallow errors become invisible."
],
"suggestion": "Standardize detector contract: return wrapped errors for parse/read failures (`fmt.Errorf(\"parse %s: %w\", path, err)`) and let scanner classify them as detector execution errors instead of empty findings.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "mixed_error_wrapping_at_scraper_boundaries",
"summary": "Scrapers mix raw error passthrough with contextual wrapping.",
"related_files": [
"internal/scraper/openapi.go",
"internal/scraper/github.go",
"internal/scraper/external/nuxtdocs.go"
],
"evidence": [
"OpenAPI scraper returns raw `err` from readSpec/parseOpenAPISpec (openapi.go:43-50).",
"GitHub scraper returns raw resolveRepo/MkdirTemp errors (github.go:29-37).",
"Nuxt external scraper wraps fetch/parse failures with operation context and `%w` (nuxtdocs.go:39-45)."
],
"suggestion": "Adopt one boundary rule for scraper packages: every external I/O or parse failure should be wrapped with operation context and `%w` before returning.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "string_aggregated_errors_lose_causal_chain",
"summary": "Aggregated scrape failures are flattened to strings, losing unwrap support.",
"related_files": [
"internal/scraper/web.go",
"internal/scraper/localsearch.go",
"cmd/ask.go"
],
"evidence": [
"Web scraper returns `fmt.Errorf(\"web scrape failed: %s\", strings.Join(scrapeErrors, \"; \"))` (web.go:259).",
"Local search scraper similarly returns joined string error text (localsearch.go:143).",
"ask command accumulates URL/term errors as formatted strings (ask.go:320,340), not typed/wrapped errors."
],
"suggestion": "Store and return structured error sets (e.g., `[]error` wrapped via a multi-error type) and only stringify at CLI presentation boundaries.",
"confidence": "medium",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
},
{
"dimension": "error_consistency",
"identifier": "error_drop_and_continue_without_signal",
"summary": "Some failures are intentionally ignored with no traceable signal.",
"related_files": [
"internal/server/server.go",
"internal/scraper/local.go",
"internal/search/engine.go"
],
"evidence": [
"Server ignores encode error when writing parse-error RPC response (`_ = out.Encode(...)`) (server.go:213).",
"Local scraper drops file conversion errors by returning nil in walk callback (local.go:90-93).",
"Search engine skips parseDocFile errors with `continue` and no reporting (engine.go:134-137)."
],
"suggestion": "When continuing after non-fatal errors, emit a consistent warning/collector path (counter + sampled log + optional returned diagnostics) so operators can detect degraded runs.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
}
],
"review_quality": {
"batch_count": 1,
"dimension_coverage": 1.0,
"evidence_density": 1.0,
"high_score_without_risk": 0,
"finding_pressure": 9.74,
"dimensions_with_findings": 1
}
}
@@ -0,0 +1,125 @@
You are a focused subagent reviewer for a single holistic investigation batch.
Repository root: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour
Immutable packet: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour/.desloppify/review_packets/holistic_packet_20260224_103046.json
Batch index: 1
Batch name: Cross-cutting Sweep
Batch dimensions: error_consistency
Batch rationale: selected dimensions had no direct batch mapping; review representative cross-cutting files
Files assigned:
- internal/quality/enhanced_types.go
- internal/quality/scoring_test.go
- internal/quality/types.go
- pkg/rustdocs/parser_test.go
- internal/config/config.go
- cmd/scrape.go
- internal/quality/plugins/go/analyzers/detectors.go
- internal/quality/plugins/go/analyzers/advanced.go
- internal/scraper/web.go
- internal/quality/plugins/go/plugin.go
- internal/scheduler/scheduler.go
- cmd/push.go
- internal/scraper/localsearch_test.go
- cmd/ask.go
- internal/ai/openai.go
- internal/server/server.go
- cmd/get.go
- internal/quality/analyzers/controlflow.go
- internal/vector/store.go
- examples/demo_scrapers.go
- internal/indexer/indexer.go
- internal/scraper/openapi.go
- pkg/pythondocs/parser.go
- cmd/get_test.go
- internal/quality/scanner_test.go
- internal/scraper/localsearch.go
- cmd/serve.go
- internal/scraper/external/nuxtdocs.go
- internal/quality/plugins/go/analyzers/test_coverage.go
- internal/quality/scanner.go
- internal/search/engine.go
- internal/scraper/github.go
- internal/scraper/external/astrodocs.go
- internal/scraper/external/cloudflaredocs.go
- internal/scraper/external/dockerdocs.go
- internal/quality/plugins/go/fixers/advanced_fixers.go
- cleanup_unused.go
- main.go
- cmd/ask_test.go
- cmd/auto.go
- internal/scraper/local.go
- internal/scraper/local_test.go
- README.md
- .desloppify/config.json
- .desloppify/query.json
- .desloppify/subagents/runs/20260223_100953/prompts/batch-1.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-2.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-3.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-4.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-5.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-6.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-1.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-2.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-5.md
- .desloppify/subagents/runs/20260224_101740/prompts/batch-1.md
- .github/workflows/ci.yml
- AGENTS.md
- cmd/devour_enhanced.py
- cmd/devour_enhanced_fixed.py
- cmd/devour_enhanced_v2.py
- desloppify/desloppify/desloppify/app/commands/_show_terminal.py
- desloppify/desloppify/desloppify/app/commands/fix/apply_flow.py
- desloppify/desloppify/desloppify/app/commands/issues_cmd.py
- desloppify/desloppify/desloppify/app/commands/next.py
- desloppify/desloppify/desloppify/app/commands/resolve/selection.py
- desloppify/desloppify/desloppify/app/commands/scan/scan_reporting_llm.py
- desloppify/desloppify/desloppify/app/commands/status_parts/render.py
- desloppify/desloppify/desloppify/app/output/scorecard_parts/projection.py
- desloppify/desloppify/desloppify/engine/detectors/security/rules.py
- desloppify/desloppify/desloppify/engine/scoring_internal/subjective/core.py
- desloppify/desloppify/desloppify/engine/state_internal/resolution.py
- desloppify/desloppify/desloppify/intelligence/review/__init__.py
- desloppify/desloppify/desloppify/intelligence/review/context_internal/structure.py
- desloppify/desloppify/desloppify/intelligence/review/dimensions/data.py
- desloppify/desloppify/desloppify/intelligence/review/importing/holistic.py
- desloppify/desloppify/desloppify/languages/_shared/phases_common.py
- desloppify/desloppify/desloppify/languages/_shared/review_data/dimensions.json
- desloppify/desloppify/desloppify/languages/_shared/scaffold_detect_commands.py
- desloppify/desloppify/desloppify/languages/csharp/_parse_helpers.py
- desloppify/desloppify/desloppify/languages/csharp/commands.py
Task requirements:
1. Read the immutable packet and follow `system_prompt` constraints exactly.
2. Evaluate ONLY listed files and ONLY listed dimensions for this batch.
3. Return 0-10 high-quality findings for this batch (empty array allowed).
4. Score/finding consistency is required: broader or more severe findings MUST lower dimension scores.
5. Every finding must include `related_files` with at least 2 files when possible.
6. Every finding must include `impact_scope` and `fix_scope`.
7. Every scored dimension MUST include dimension_notes with concrete evidence.
8. If a dimension score is >85, include `unreported_risk` in dimension_notes.
9. Use exactly one decimal place for every assessment and abstraction sub-axis score.
10. Do not edit repository files.
11. Return ONLY valid JSON, no markdown fences.
Scope enums:
- impact_scope: "local" | "module" | "subsystem" | "codebase"
- fix_scope: "single_edit" | "multi_file_refactor" | "architectural_change"
Output schema:
{
"batch": "Cross-cutting Sweep",
"batch_index": 1,
"assessments": {"<dimension>": <0-100 with one decimal place>},
"dimension_notes": {
"<dimension>": {
"evidence": ["specific code observations"],
"impact_scope": "local|module|subsystem|codebase",
"fix_scope": "single_edit|multi_file_refactor|architectural_change",
"confidence": "high|medium|low",
"unreported_risk": "required when score >85",
"sub_axes": {"abstraction_leverage": 0-100 with one decimal place, "indirection_cost": 0-100 with one decimal place, "interface_honesty": 0-100 with one decimal place} // required for abstraction_fitness when evidence supports it
}
},
"findings": []
}
@@ -0,0 +1,99 @@
{
"batch": "Cross-cutting Sweep",
"batch_index": 1,
"assessments": {
"error_consistency": 71.0
},
"dimension_notes": {
"error_consistency": {
"evidence": [
"Multiple analyzers silently suppress parse/read failures instead of returning contextual errors (internal/quality/plugins/go/analyzers/detectors.go:112-114, 240-245; internal/quality/plugins/go/analyzers/test_coverage.go:211-214).",
"Scraper entrypoints mix contextual wrapping and raw passthrough in similar boundary operations (internal/scraper/openapi.go:43-50 returns raw err; internal/scraper/github.go:29-37 returns raw err; internal/scraper/external/nuxtdocs.go:39-45 wraps with %w).",
"Several paths collapse structured errors into joined strings, reducing traceability and unwrapping (internal/scraper/web.go:259; internal/scraper/localsearch.go:143; cmd/ask.go:320,340).",
"Some runtime errors are dropped and processing continues without durable signal (internal/server/server.go:213 ignores encode error; internal/scraper/local.go:90-93 drops file parse errors; internal/search/engine.go:134-137 skips parse failures)."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high"
}
},
"findings": [
{
"dimension": "error_consistency",
"identifier": "silent_parse_failures_in_analyzers",
"summary": "Analyzer parse/read errors are silently converted to no findings.",
"related_files": [
"internal/quality/plugins/go/analyzers/detectors.go",
"internal/quality/plugins/go/analyzers/test_coverage.go",
"internal/quality/scanner.go"
],
"evidence": [
"GodStructDetector.analyzeFile returns nil on parser.ParseFile error (detectors.go:112-114).",
"DebugLogDetector.analyzeFile also returns nil on parse failure (detectors.go:240-241).",
"UntestedFuncDetector returns (nil, nil) when coverage.out read fails (test_coverage.go:211-214), making failure indistinguishable from success.",
"Scanner loop logs detector failure and continues (scanner.go:77-79), so detector internals that swallow errors become invisible."
],
"suggestion": "Standardize detector contract: return wrapped errors for parse/read failures (`fmt.Errorf(\"parse %s: %w\", path, err)`) and let scanner classify them as detector execution errors instead of empty findings.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "mixed_error_wrapping_at_scraper_boundaries",
"summary": "Scrapers mix raw error passthrough with contextual wrapping.",
"related_files": [
"internal/scraper/openapi.go",
"internal/scraper/github.go",
"internal/scraper/external/nuxtdocs.go"
],
"evidence": [
"OpenAPI scraper returns raw `err` from readSpec/parseOpenAPISpec (openapi.go:43-50).",
"GitHub scraper returns raw resolveRepo/MkdirTemp errors (github.go:29-37).",
"Nuxt external scraper wraps fetch/parse failures with operation context and `%w` (nuxtdocs.go:39-45)."
],
"suggestion": "Adopt one boundary rule for scraper packages: every external I/O or parse failure should be wrapped with operation context and `%w` before returning.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "string_aggregated_errors_lose_causal_chain",
"summary": "Aggregated scrape failures are flattened to strings, losing unwrap support.",
"related_files": [
"internal/scraper/web.go",
"internal/scraper/localsearch.go",
"cmd/ask.go"
],
"evidence": [
"Web scraper returns `fmt.Errorf(\"web scrape failed: %s\", strings.Join(scrapeErrors, \"; \"))` (web.go:259).",
"Local search scraper similarly returns joined string error text (localsearch.go:143).",
"ask command accumulates URL/term errors as formatted strings (ask.go:320,340), not typed/wrapped errors."
],
"suggestion": "Store and return structured error sets (e.g., `[]error` wrapped via a multi-error type) and only stringify at CLI presentation boundaries.",
"confidence": "medium",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
},
{
"dimension": "error_consistency",
"identifier": "error_drop_and_continue_without_signal",
"summary": "Some failures are intentionally ignored with no traceable signal.",
"related_files": [
"internal/server/server.go",
"internal/scraper/local.go",
"internal/search/engine.go"
],
"evidence": [
"Server ignores encode error when writing parse-error RPC response (`_ = out.Encode(...)`) (server.go:213).",
"Local scraper drops file conversion errors by returning nil in walk callback (local.go:90-93).",
"Search engine skips parseDocFile errors with `continue` and no reporting (engine.go:134-137)."
],
"suggestion": "When continuing after non-fatal errors, emit a consistent warning/collector path (counter + sampled log + optional returned diagnostics) so operators can detect degraded runs.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
}
]
}
@@ -0,0 +1,103 @@
{
"assessments": {
"error_consistency": 52.1
},
"dimension_notes": {
"error_consistency": {
"evidence": [
"HTTP RPC path collapses response write failures to generic `http.Error(..., \"rpc write error\", ...)`, while stdio path returns wrapped encode errors (`encode parse-error response: %w`) in the same server module.",
"Batch scrape aggregates per-source failures but returns only `one or more sources failed`, while other command paths return joined/wrapped causes (`errors.Join(...)` in ask flow).",
"Quality scan loop logs detector failures and continues, while detector implementations mix per-file silent skips (`continue` on file read/count errors) and fail-fast returns for parse errors, yielding inconsistent failure visibility.",
"Ask fallback persistence/reindex errors are explicitly ignored (`_, _ = storage.SaveDocuments`, `_, _ = engine.Rebuild`) whereas push/scrape treat analogous save/reindex failures as returned errors."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high",
"unreported_risk": ""
}
},
"findings": [
{
"dimension": "error_consistency",
"identifier": "rpc_transport_error_detail_divergence",
"summary": "HTTP and stdio RPC transports expose write/encode failures with different fidelity.",
"related_files": [
"internal/server/server.go",
"cmd/serve.go"
],
"evidence": [
"In `internal/server/server.go`, HTTP `/rpc` writes generic `rpc write error` on writeRPC failure (lines ~132-140), dropping underlying encode context.",
"In the same file, stdio transport returns wrapped encode errors (line ~218) and raw encode failures (line ~224), preserving actionable context.",
"`cmd/serve.go` uses one server abstraction for both modes, so callers see transport-dependent error behavior for equivalent failure classes."
],
"suggestion": "Unify transport error contract: introduce a shared helper that classifies encode/write failures and records structured context (operation, transport, cause). Keep client-facing RPC payload stable, but log/return wrapped internal causes consistently for both HTTP and stdio.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "batch_scrape_error_cause_loss",
"summary": "Batch scrape returns a generic terminal error after printing detailed source failures.",
"related_files": [
"cmd/scrape.go",
"cmd/ask.go"
],
"evidence": [
"`cmd/scrape.go` records per-source failures, prints each error, then returns `one or more sources failed` (lines ~187-207), losing machine-readable causes.",
"`cmd/ask.go` returns joined upstream causes when retrieval fails (`errors.Join(fetchErrors...)`, lines ~149-154), preserving aggregate failure detail.",
"This creates inconsistent caller behavior across commands: some flows expose causal chains, others require scraping stdout logs."
],
"suggestion": "Accumulate per-source errors in `scrapeFromConfig` and return `fmt.Errorf(\"...: %w\", errors.Join(errs...))` while still printing the summary; this keeps CLI UX and provides consistent programmatic error chains.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "single_edit"
},
{
"dimension": "error_consistency",
"identifier": "quality_scan_detector_failure_semantics_mixed",
"summary": "Quality scanning mixes swallowed detector failures with fail-fast detector behavior.",
"related_files": [
"internal/quality/scanner.go",
"internal/quality/plugins/go/analyzers/detectors.go",
"internal/quality/plugins/go/analyzers/advanced.go"
],
"evidence": [
"`internal/quality/scanner.go` logs detector errors and continues (lines ~76-79), so scan succeeds even when detectors fail.",
"`detectors.go` has inconsistent per-file policy: `LargeFileDetector` silently `continue`s on count errors (lines ~44-47), while `GodStructDetector` returns wrapped errors and aborts detector run (lines ~102-105).",
"This produces non-uniform failure propagation: some analysis gaps are silent, some are surfaced, and scanner-level behavior masks both as successful scans."
],
"suggestion": "Define a uniform detector error policy (e.g., collect per-file errors + thresholded hard-fail) and enforce it via shared helper APIs so scanner output includes a structured `partial_failures` section instead of ad-hoc continue/abort choices.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
},
{
"dimension": "error_consistency",
"identifier": "ask_fallback_ignores_persistence_errors",
"summary": "Ask fallback suppresses save/reindex failures that analogous command flows propagate.",
"related_files": [
"cmd/ask.go",
"cmd/push.go",
"cmd/scrape.go"
],
"evidence": [
"`cmd/ask.go` ignores persistence/index maintenance errors with blank identifier assignments (lines ~369-377).",
"`cmd/push.go` and `cmd/scrape.go` both return save/reindex failures (`save docs failed`, `reindex failed`, `reindex after scrape...`) instead of suppressing them.",
"Equivalent storage/index boundary failures therefore produce different observability depending on command path."
],
"suggestion": "Handle ask fallback persistence/reindex errors explicitly: either return them when they invalidate guarantees, or attach them to response metadata/logged warnings with a consistent typed error category used across commands.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
}
],
"review_quality": {
"batch_count": 1,
"dimension_coverage": 1.0,
"evidence_density": 1.0,
"high_score_without_risk": 0,
"finding_pressure": 8.88,
"dimensions_with_findings": 1
}
}
@@ -0,0 +1,125 @@
You are a focused subagent reviewer for a single holistic investigation batch.
Repository root: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour
Immutable packet: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour/.desloppify/review_packets/holistic_packet_20260224_104658.json
Batch index: 1
Batch name: Cross-cutting Sweep
Batch dimensions: error_consistency
Batch rationale: selected dimensions had no direct batch mapping; review representative cross-cutting files
Files assigned:
- internal/quality/enhanced_types.go
- internal/quality/scoring_test.go
- internal/quality/types.go
- pkg/rustdocs/parser_test.go
- internal/config/config.go
- cmd/scrape.go
- internal/quality/plugins/go/analyzers/detectors.go
- internal/quality/plugins/go/analyzers/advanced.go
- internal/scraper/web.go
- internal/quality/plugins/go/plugin.go
- internal/scheduler/scheduler.go
- cmd/push.go
- internal/scraper/localsearch_test.go
- cmd/ask.go
- internal/ai/openai.go
- internal/server/server.go
- cmd/get.go
- internal/quality/analyzers/controlflow.go
- internal/vector/store.go
- examples/demo_scrapers.go
- internal/indexer/indexer.go
- internal/scraper/openapi.go
- pkg/pythondocs/parser.go
- cmd/get_test.go
- internal/quality/scanner_test.go
- internal/scraper/localsearch.go
- cmd/serve.go
- internal/scraper/external/nuxtdocs.go
- internal/quality/plugins/go/analyzers/test_coverage.go
- internal/quality/scanner.go
- internal/search/engine.go
- internal/scraper/github.go
- internal/scraper/external/astrodocs.go
- internal/scraper/external/cloudflaredocs.go
- internal/scraper/external/dockerdocs.go
- internal/quality/plugins/go/fixers/advanced_fixers.go
- cleanup_unused.go
- main.go
- cmd/ask_test.go
- cmd/auto.go
- internal/scraper/local.go
- internal/scraper/local_test.go
- README.md
- .desloppify/config.json
- .desloppify/query.json
- .desloppify/subagents/runs/20260223_100953/prompts/batch-1.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-2.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-3.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-4.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-5.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-6.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-1.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-2.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-5.md
- .desloppify/subagents/runs/20260224_101740/prompts/batch-1.md
- .github/workflows/ci.yml
- AGENTS.md
- cmd/devour_enhanced.py
- cmd/devour_enhanced_fixed.py
- cmd/devour_enhanced_v2.py
- desloppify/desloppify/desloppify/app/commands/_show_terminal.py
- desloppify/desloppify/desloppify/app/commands/fix/apply_flow.py
- desloppify/desloppify/desloppify/app/commands/issues_cmd.py
- desloppify/desloppify/desloppify/app/commands/next.py
- desloppify/desloppify/desloppify/app/commands/resolve/selection.py
- desloppify/desloppify/desloppify/app/commands/scan/scan_reporting_llm.py
- desloppify/desloppify/desloppify/app/commands/status_parts/render.py
- desloppify/desloppify/desloppify/app/output/scorecard_parts/projection.py
- desloppify/desloppify/desloppify/engine/detectors/security/rules.py
- desloppify/desloppify/desloppify/engine/scoring_internal/subjective/core.py
- desloppify/desloppify/desloppify/engine/state_internal/resolution.py
- desloppify/desloppify/desloppify/intelligence/review/__init__.py
- desloppify/desloppify/desloppify/intelligence/review/context_internal/structure.py
- desloppify/desloppify/desloppify/intelligence/review/dimensions/data.py
- desloppify/desloppify/desloppify/intelligence/review/importing/holistic.py
- desloppify/desloppify/desloppify/languages/_shared/phases_common.py
- desloppify/desloppify/desloppify/languages/_shared/review_data/dimensions.json
- desloppify/desloppify/desloppify/languages/_shared/scaffold_detect_commands.py
- desloppify/desloppify/desloppify/languages/csharp/_parse_helpers.py
- desloppify/desloppify/desloppify/languages/csharp/commands.py
Task requirements:
1. Read the immutable packet and follow `system_prompt` constraints exactly.
2. Evaluate ONLY listed files and ONLY listed dimensions for this batch.
3. Return 0-10 high-quality findings for this batch (empty array allowed).
4. Score/finding consistency is required: broader or more severe findings MUST lower dimension scores.
5. Every finding must include `related_files` with at least 2 files when possible.
6. Every finding must include `impact_scope` and `fix_scope`.
7. Every scored dimension MUST include dimension_notes with concrete evidence.
8. If a dimension score is >85, include `unreported_risk` in dimension_notes.
9. Use exactly one decimal place for every assessment and abstraction sub-axis score.
10. Do not edit repository files.
11. Return ONLY valid JSON, no markdown fences.
Scope enums:
- impact_scope: "local" | "module" | "subsystem" | "codebase"
- fix_scope: "single_edit" | "multi_file_refactor" | "architectural_change"
Output schema:
{
"batch": "Cross-cutting Sweep",
"batch_index": 1,
"assessments": {"<dimension>": <0-100 with one decimal place>},
"dimension_notes": {
"<dimension>": {
"evidence": ["specific code observations"],
"impact_scope": "local|module|subsystem|codebase",
"fix_scope": "single_edit|multi_file_refactor|architectural_change",
"confidence": "high|medium|low",
"unreported_risk": "required when score >85",
"sub_axes": {"abstraction_leverage": 0-100 with one decimal place, "indirection_cost": 0-100 with one decimal place, "interface_honesty": 0-100 with one decimal place} // required for abstraction_fitness when evidence supports it
}
},
"findings": []
}
@@ -0,0 +1,96 @@
{
"batch": "Cross-cutting Sweep",
"batch_index": 1,
"assessments": {
"error_consistency": 74.0
},
"dimension_notes": {
"error_consistency": {
"evidence": [
"HTTP RPC path collapses response write failures to generic `http.Error(..., \"rpc write error\", ...)`, while stdio path returns wrapped encode errors (`encode parse-error response: %w`) in the same server module.",
"Batch scrape aggregates per-source failures but returns only `one or more sources failed`, while other command paths return joined/wrapped causes (`errors.Join(...)` in ask flow).",
"Quality scan loop logs detector failures and continues, while detector implementations mix per-file silent skips (`continue` on file read/count errors) and fail-fast returns for parse errors, yielding inconsistent failure visibility.",
"Ask fallback persistence/reindex errors are explicitly ignored (`_, _ = storage.SaveDocuments`, `_, _ = engine.Rebuild`) whereas push/scrape treat analogous save/reindex failures as returned errors."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high"
}
},
"findings": [
{
"dimension": "error_consistency",
"identifier": "rpc_transport_error_detail_divergence",
"summary": "HTTP and stdio RPC transports expose write/encode failures with different fidelity.",
"related_files": [
"internal/server/server.go",
"cmd/serve.go"
],
"evidence": [
"In `internal/server/server.go`, HTTP `/rpc` writes generic `rpc write error` on writeRPC failure (lines ~132-140), dropping underlying encode context.",
"In the same file, stdio transport returns wrapped encode errors (line ~218) and raw encode failures (line ~224), preserving actionable context.",
"`cmd/serve.go` uses one server abstraction for both modes, so callers see transport-dependent error behavior for equivalent failure classes."
],
"suggestion": "Unify transport error contract: introduce a shared helper that classifies encode/write failures and records structured context (operation, transport, cause). Keep client-facing RPC payload stable, but log/return wrapped internal causes consistently for both HTTP and stdio.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "batch_scrape_error_cause_loss",
"summary": "Batch scrape returns a generic terminal error after printing detailed source failures.",
"related_files": [
"cmd/scrape.go",
"cmd/ask.go"
],
"evidence": [
"`cmd/scrape.go` records per-source failures, prints each error, then returns `one or more sources failed` (lines ~187-207), losing machine-readable causes.",
"`cmd/ask.go` returns joined upstream causes when retrieval fails (`errors.Join(fetchErrors...)`, lines ~149-154), preserving aggregate failure detail.",
"This creates inconsistent caller behavior across commands: some flows expose causal chains, others require scraping stdout logs."
],
"suggestion": "Accumulate per-source errors in `scrapeFromConfig` and return `fmt.Errorf(\"...: %w\", errors.Join(errs...))` while still printing the summary; this keeps CLI UX and provides consistent programmatic error chains.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "single_edit"
},
{
"dimension": "error_consistency",
"identifier": "quality_scan_detector_failure_semantics_mixed",
"summary": "Quality scanning mixes swallowed detector failures with fail-fast detector behavior.",
"related_files": [
"internal/quality/scanner.go",
"internal/quality/plugins/go/analyzers/detectors.go",
"internal/quality/plugins/go/analyzers/advanced.go"
],
"evidence": [
"`internal/quality/scanner.go` logs detector errors and continues (lines ~76-79), so scan succeeds even when detectors fail.",
"`detectors.go` has inconsistent per-file policy: `LargeFileDetector` silently `continue`s on count errors (lines ~44-47), while `GodStructDetector` returns wrapped errors and aborts detector run (lines ~102-105).",
"This produces non-uniform failure propagation: some analysis gaps are silent, some are surfaced, and scanner-level behavior masks both as successful scans."
],
"suggestion": "Define a uniform detector error policy (e.g., collect per-file errors + thresholded hard-fail) and enforce it via shared helper APIs so scanner output includes a structured `partial_failures` section instead of ad-hoc continue/abort choices.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
},
{
"dimension": "error_consistency",
"identifier": "ask_fallback_ignores_persistence_errors",
"summary": "Ask fallback suppresses save/reindex failures that analogous command flows propagate.",
"related_files": [
"cmd/ask.go",
"cmd/push.go",
"cmd/scrape.go"
],
"evidence": [
"`cmd/ask.go` ignores persistence/index maintenance errors with blank identifier assignments (lines ~369-377).",
"`cmd/push.go` and `cmd/scrape.go` both return save/reindex failures (`save docs failed`, `reindex failed`, `reindex after scrape...`) instead of suppressing them.",
"Equivalent storage/index boundary failures therefore produce different observability depending on command path."
],
"suggestion": "Handle ask fallback persistence/reindex errors explicitly: either return them when they invalidate guarantees, or attach them to response metadata/logged warnings with a consistent typed error category used across commands.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
}
]
}
@@ -0,0 +1,87 @@
{
"assessments": {
"error_consistency": 56.5
},
"dimension_notes": {
"error_consistency": {
"evidence": [
"Error wrapping is inconsistent across neighboring call paths: `cmd/serve.go` wraps many failures with operation context (e.g. `run devour_query search`), while `runServe` and `handleServeMethod` still return raw errors directly from `loadAppConfig` (`cmd/serve.go:45`, `cmd/serve.go:75`).",
"Several lower-level functions return bare underlying errors without context (`internal/scraper/local.go:49`, `internal/scraper/local.go:57`, `internal/config/config.go:207`, `internal/config/config.go:364`) while sibling code in the same files uses contextual wrapping.",
"Error classification is string-based in `cmd/ask.go` (`strings.Contains(fetchErr.Error(), \"persistence warning:\")` at line 191), making behavior dependent on message text rather than typed/sentinel error contracts.",
"At least one path suppresses error details entirely (`countLOC` returns `0` on read failure in `internal/quality/plugins/go/plugin.go:353-356`), while other analyzer code returns explicit detector errors."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high",
"unreported_risk": ""
}
},
"findings": [
{
"dimension": "error_consistency",
"identifier": "mixed_wrapped_and_bare_error_propagation",
"summary": "Adjacent layers mix contextual wrapping and bare error returns",
"related_files": [
"cmd/serve.go",
"internal/config/config.go",
"internal/scraper/local.go",
"cmd/scrape.go"
],
"evidence": [
"`cmd/serve.go:87-109` wraps errors with operation labels, but `cmd/serve.go:45` and `cmd/serve.go:75` return raw `loadAppConfig` errors.",
"`internal/config/config.go:207` and `:364` return bare errors, while nearby code uses contextual `fmt.Errorf(...: %w)`.",
"`internal/scraper/local.go:49`, `:57`, `:104`, `:154`, `:164` return raw errors despite other branches adding context and joins."
],
"suggestion": "Adopt a single propagation rule for non-boundary layers: always wrap external/I-O failures with operation context (e.g., `fmt.Errorf(\"load config path: %w\", err)`). Apply this consistently in `cmd/serve.go`, `internal/config/config.go`, and `internal/scraper/local.go`.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "string_based_error_classification_in_ask_flow",
"summary": "Ask flow branches on error message text instead of typed categories",
"related_files": [
"cmd/ask.go",
"cmd/scrape.go",
"internal/server/server.go"
],
"evidence": [
"`cmd/ask.go:379` and `:384` encode category in message prefix (`\"persistence warning:\"`).",
"`cmd/ask.go:191` uses `strings.Contains(fetchErr.Error(), \"persistence warning:\")` for control flow, coupling behavior to mutable message strings.",
"Other modules treat errors opaquely and pass through generic text (`internal/server/server.go:256` returns `err.Error()` to RPC clients), amplifying inconsistency in downstream handling."
],
"suggestion": "Introduce typed/sentinel errors for recoverable warning classes (e.g., `var ErrPersistenceWarning`) and use `errors.Is`/`errors.As` in `cmd/ask.go` instead of string matching. Keep human-readable text separate from machine classification.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "analyzer_error_handling_policy_is_incoherent",
"summary": "Analyzer/plugin paths alternate between explicit, wrapped, and silent failures",
"related_files": [
"internal/quality/plugins/go/analyzers/detectors.go",
"internal/quality/plugins/go/analyzers/advanced.go",
"internal/quality/plugins/go/plugin.go"
],
"evidence": [
"`internal/quality/plugins/go/analyzers/detectors.go:46-60` converts read failures into synthetic findings (`detector_error`) rather than returning an error.",
"`internal/quality/plugins/go/analyzers/advanced.go:242` returns raw parse errors from helper methods without context.",
"`internal/quality/plugins/go/plugin.go:353-356` swallows file read failures in `countLOC` by returning `0`, losing failure provenance entirely."
],
"suggestion": "Define a consistent analyzer failure contract: either (a) report detector-execution issues as standardized `detector_error` findings with metadata, or (b) return wrapped errors; do not silently coerce errors to default values. Refactor `countLOC` to return `(int, error)` and propagate classification consistently.",
"confidence": "medium",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
}
],
"review_quality": {
"batch_count": 1,
"dimension_coverage": 1.0,
"evidence_density": 1.333,
"high_score_without_risk": 0,
"finding_pressure": 7.244,
"dimensions_with_findings": 1
}
}
@@ -0,0 +1,125 @@
You are a focused subagent reviewer for a single holistic investigation batch.
Repository root: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour
Immutable packet: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour/.desloppify/review_packets/holistic_packet_20260224_105325.json
Batch index: 1
Batch name: Cross-cutting Sweep
Batch dimensions: error_consistency
Batch rationale: selected dimensions had no direct batch mapping; review representative cross-cutting files
Files assigned:
- internal/quality/enhanced_types.go
- internal/quality/scoring_test.go
- internal/quality/types.go
- pkg/rustdocs/parser_test.go
- internal/config/config.go
- cmd/scrape.go
- internal/quality/plugins/go/analyzers/detectors.go
- internal/quality/plugins/go/analyzers/advanced.go
- internal/scraper/web.go
- internal/quality/plugins/go/plugin.go
- internal/scheduler/scheduler.go
- cmd/push.go
- internal/scraper/localsearch_test.go
- cmd/ask.go
- internal/ai/openai.go
- internal/server/server.go
- cmd/get.go
- internal/quality/analyzers/controlflow.go
- internal/vector/store.go
- examples/demo_scrapers.go
- internal/indexer/indexer.go
- internal/scraper/openapi.go
- pkg/pythondocs/parser.go
- cmd/get_test.go
- internal/quality/scanner_test.go
- internal/scraper/localsearch.go
- cmd/serve.go
- internal/scraper/external/nuxtdocs.go
- internal/quality/scanner.go
- internal/quality/plugins/go/analyzers/test_coverage.go
- internal/search/engine.go
- internal/scraper/github.go
- internal/scraper/external/astrodocs.go
- internal/scraper/external/cloudflaredocs.go
- internal/scraper/external/dockerdocs.go
- internal/quality/plugins/go/fixers/advanced_fixers.go
- cleanup_unused.go
- main.go
- cmd/ask_test.go
- cmd/auto.go
- internal/scraper/local.go
- internal/scraper/local_test.go
- README.md
- .desloppify/config.json
- .desloppify/query.json
- .desloppify/subagents/runs/20260223_100953/prompts/batch-1.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-2.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-3.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-4.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-5.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-6.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-1.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-2.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-5.md
- .desloppify/subagents/runs/20260224_101740/prompts/batch-1.md
- .github/workflows/ci.yml
- AGENTS.md
- cmd/devour_enhanced.py
- cmd/devour_enhanced_fixed.py
- cmd/devour_enhanced_v2.py
- desloppify/desloppify/desloppify/app/commands/_show_terminal.py
- desloppify/desloppify/desloppify/app/commands/fix/apply_flow.py
- desloppify/desloppify/desloppify/app/commands/issues_cmd.py
- desloppify/desloppify/desloppify/app/commands/next.py
- desloppify/desloppify/desloppify/app/commands/resolve/selection.py
- desloppify/desloppify/desloppify/app/commands/scan/scan_reporting_llm.py
- desloppify/desloppify/desloppify/app/commands/status_parts/render.py
- desloppify/desloppify/desloppify/app/output/scorecard_parts/projection.py
- desloppify/desloppify/desloppify/engine/detectors/security/rules.py
- desloppify/desloppify/desloppify/engine/scoring_internal/subjective/core.py
- desloppify/desloppify/desloppify/engine/state_internal/resolution.py
- desloppify/desloppify/desloppify/intelligence/review/__init__.py
- desloppify/desloppify/desloppify/intelligence/review/context_internal/structure.py
- desloppify/desloppify/desloppify/intelligence/review/dimensions/data.py
- desloppify/desloppify/desloppify/intelligence/review/importing/holistic.py
- desloppify/desloppify/desloppify/languages/_shared/phases_common.py
- desloppify/desloppify/desloppify/languages/_shared/review_data/dimensions.json
- desloppify/desloppify/desloppify/languages/_shared/scaffold_detect_commands.py
- desloppify/desloppify/desloppify/languages/csharp/_parse_helpers.py
- desloppify/desloppify/desloppify/languages/csharp/commands.py
Task requirements:
1. Read the immutable packet and follow `system_prompt` constraints exactly.
2. Evaluate ONLY listed files and ONLY listed dimensions for this batch.
3. Return 0-10 high-quality findings for this batch (empty array allowed).
4. Score/finding consistency is required: broader or more severe findings MUST lower dimension scores.
5. Every finding must include `related_files` with at least 2 files when possible.
6. Every finding must include `impact_scope` and `fix_scope`.
7. Every scored dimension MUST include dimension_notes with concrete evidence.
8. If a dimension score is >85, include `unreported_risk` in dimension_notes.
9. Use exactly one decimal place for every assessment and abstraction sub-axis score.
10. Do not edit repository files.
11. Return ONLY valid JSON, no markdown fences.
Scope enums:
- impact_scope: "local" | "module" | "subsystem" | "codebase"
- fix_scope: "single_edit" | "multi_file_refactor" | "architectural_change"
Output schema:
{
"batch": "Cross-cutting Sweep",
"batch_index": 1,
"assessments": {"<dimension>": <0-100 with one decimal place>},
"dimension_notes": {
"<dimension>": {
"evidence": ["specific code observations"],
"impact_scope": "local|module|subsystem|codebase",
"fix_scope": "single_edit|multi_file_refactor|architectural_change",
"confidence": "high|medium|low",
"unreported_risk": "required when score >85",
"sub_axes": {"abstraction_leverage": 0-100 with one decimal place, "indirection_cost": 0-100 with one decimal place, "interface_honesty": 0-100 with one decimal place} // required for abstraction_fitness when evidence supports it
}
},
"findings": []
}
@@ -0,0 +1,80 @@
{
"batch": "Cross-cutting Sweep",
"batch_index": 1,
"assessments": {
"error_consistency": 74.0
},
"dimension_notes": {
"error_consistency": {
"evidence": [
"Error wrapping is inconsistent across neighboring call paths: `cmd/serve.go` wraps many failures with operation context (e.g. `run devour_query search`), while `runServe` and `handleServeMethod` still return raw errors directly from `loadAppConfig` (`cmd/serve.go:45`, `cmd/serve.go:75`).",
"Several lower-level functions return bare underlying errors without context (`internal/scraper/local.go:49`, `internal/scraper/local.go:57`, `internal/config/config.go:207`, `internal/config/config.go:364`) while sibling code in the same files uses contextual wrapping.",
"Error classification is string-based in `cmd/ask.go` (`strings.Contains(fetchErr.Error(), \"persistence warning:\")` at line 191), making behavior dependent on message text rather than typed/sentinel error contracts.",
"At least one path suppresses error details entirely (`countLOC` returns `0` on read failure in `internal/quality/plugins/go/plugin.go:353-356`), while other analyzer code returns explicit detector errors."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high"
}
},
"findings": [
{
"dimension": "error_consistency",
"identifier": "mixed_wrapped_and_bare_error_propagation",
"summary": "Adjacent layers mix contextual wrapping and bare error returns",
"related_files": [
"cmd/serve.go",
"internal/config/config.go",
"internal/scraper/local.go",
"cmd/scrape.go"
],
"evidence": [
"`cmd/serve.go:87-109` wraps errors with operation labels, but `cmd/serve.go:45` and `cmd/serve.go:75` return raw `loadAppConfig` errors.",
"`internal/config/config.go:207` and `:364` return bare errors, while nearby code uses contextual `fmt.Errorf(...: %w)`.",
"`internal/scraper/local.go:49`, `:57`, `:104`, `:154`, `:164` return raw errors despite other branches adding context and joins."
],
"suggestion": "Adopt a single propagation rule for non-boundary layers: always wrap external/I-O failures with operation context (e.g., `fmt.Errorf(\"load config path: %w\", err)`). Apply this consistently in `cmd/serve.go`, `internal/config/config.go`, and `internal/scraper/local.go`.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "string_based_error_classification_in_ask_flow",
"summary": "Ask flow branches on error message text instead of typed categories",
"related_files": [
"cmd/ask.go",
"cmd/scrape.go",
"internal/server/server.go"
],
"evidence": [
"`cmd/ask.go:379` and `:384` encode category in message prefix (`\"persistence warning:\"`).",
"`cmd/ask.go:191` uses `strings.Contains(fetchErr.Error(), \"persistence warning:\")` for control flow, coupling behavior to mutable message strings.",
"Other modules treat errors opaquely and pass through generic text (`internal/server/server.go:256` returns `err.Error()` to RPC clients), amplifying inconsistency in downstream handling."
],
"suggestion": "Introduce typed/sentinel errors for recoverable warning classes (e.g., `var ErrPersistenceWarning`) and use `errors.Is`/`errors.As` in `cmd/ask.go` instead of string matching. Keep human-readable text separate from machine classification.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "analyzer_error_handling_policy_is_incoherent",
"summary": "Analyzer/plugin paths alternate between explicit, wrapped, and silent failures",
"related_files": [
"internal/quality/plugins/go/analyzers/detectors.go",
"internal/quality/plugins/go/analyzers/advanced.go",
"internal/quality/plugins/go/plugin.go"
],
"evidence": [
"`internal/quality/plugins/go/analyzers/detectors.go:46-60` converts read failures into synthetic findings (`detector_error`) rather than returning an error.",
"`internal/quality/plugins/go/analyzers/advanced.go:242` returns raw parse errors from helper methods without context.",
"`internal/quality/plugins/go/plugin.go:353-356` swallows file read failures in `countLOC` by returning `0`, losing failure provenance entirely."
],
"suggestion": "Define a consistent analyzer failure contract: either (a) report detector-execution issues as standardized `detector_error` findings with metadata, or (b) return wrapped errors; do not silently coerce errors to default values. Refactor `countLOC` to return `(int, error)` and propagate classification consistently.",
"confidence": "medium",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
}
]
}
@@ -0,0 +1,125 @@
You are a focused subagent reviewer for a single holistic investigation batch.
Repository root: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour
Immutable packet: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour/.desloppify/review_packets/holistic_packet_20260224_105807.json
Batch index: 1
Batch name: Cross-cutting Sweep
Batch dimensions: error_consistency
Batch rationale: selected dimensions had no direct batch mapping; review representative cross-cutting files
Files assigned:
- internal/quality/enhanced_types.go
- internal/quality/scoring_test.go
- internal/quality/types.go
- pkg/rustdocs/parser_test.go
- internal/config/config.go
- cmd/scrape.go
- internal/quality/plugins/go/analyzers/detectors.go
- internal/quality/plugins/go/analyzers/advanced.go
- internal/scraper/web.go
- internal/quality/plugins/go/plugin.go
- internal/scheduler/scheduler.go
- cmd/push.go
- internal/scraper/localsearch_test.go
- cmd/ask.go
- internal/ai/openai.go
- internal/server/server.go
- cmd/get.go
- internal/quality/analyzers/controlflow.go
- internal/vector/store.go
- examples/demo_scrapers.go
- internal/indexer/indexer.go
- internal/scraper/openapi.go
- pkg/pythondocs/parser.go
- cmd/get_test.go
- internal/quality/scanner_test.go
- internal/scraper/localsearch.go
- cmd/serve.go
- internal/scraper/external/nuxtdocs.go
- internal/quality/scanner.go
- internal/quality/plugins/go/analyzers/test_coverage.go
- internal/search/engine.go
- internal/scraper/github.go
- internal/scraper/external/astrodocs.go
- internal/scraper/external/cloudflaredocs.go
- internal/scraper/external/dockerdocs.go
- internal/quality/plugins/go/fixers/advanced_fixers.go
- cleanup_unused.go
- main.go
- cmd/ask_test.go
- cmd/auto.go
- internal/scraper/local.go
- internal/scraper/local_test.go
- README.md
- .desloppify/config.json
- .desloppify/query.json
- .desloppify/subagents/runs/20260223_100953/prompts/batch-1.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-2.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-3.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-4.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-5.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-6.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-1.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-2.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-5.md
- .desloppify/subagents/runs/20260224_101740/prompts/batch-1.md
- .desloppify/subagents/runs/20260224_104658/prompts/batch-1.md
- .github/workflows/ci.yml
- AGENTS.md
- cmd/devour_enhanced.py
- cmd/devour_enhanced_fixed.py
- cmd/devour_enhanced_v2.py
- desloppify/desloppify/desloppify/app/commands/_show_terminal.py
- desloppify/desloppify/desloppify/app/commands/fix/apply_flow.py
- desloppify/desloppify/desloppify/app/commands/issues_cmd.py
- desloppify/desloppify/desloppify/app/commands/next.py
- desloppify/desloppify/desloppify/app/commands/resolve/selection.py
- desloppify/desloppify/desloppify/app/commands/scan/scan_reporting_llm.py
- desloppify/desloppify/desloppify/app/commands/status_parts/render.py
- desloppify/desloppify/desloppify/app/output/scorecard_parts/projection.py
- desloppify/desloppify/desloppify/engine/detectors/security/rules.py
- desloppify/desloppify/desloppify/engine/scoring_internal/subjective/core.py
- desloppify/desloppify/desloppify/engine/state_internal/resolution.py
- desloppify/desloppify/desloppify/intelligence/review/__init__.py
- desloppify/desloppify/desloppify/intelligence/review/context_internal/structure.py
- desloppify/desloppify/desloppify/intelligence/review/dimensions/data.py
- desloppify/desloppify/desloppify/intelligence/review/importing/holistic.py
- desloppify/desloppify/desloppify/languages/_shared/phases_common.py
- desloppify/desloppify/desloppify/languages/_shared/review_data/dimensions.json
- desloppify/desloppify/desloppify/languages/_shared/scaffold_detect_commands.py
- desloppify/desloppify/desloppify/languages/csharp/_parse_helpers.py
Task requirements:
1. Read the immutable packet and follow `system_prompt` constraints exactly.
2. Evaluate ONLY listed files and ONLY listed dimensions for this batch.
3. Return 0-10 high-quality findings for this batch (empty array allowed).
4. Score/finding consistency is required: broader or more severe findings MUST lower dimension scores.
5. Every finding must include `related_files` with at least 2 files when possible.
6. Every finding must include `impact_scope` and `fix_scope`.
7. Every scored dimension MUST include dimension_notes with concrete evidence.
8. If a dimension score is >85, include `unreported_risk` in dimension_notes.
9. Use exactly one decimal place for every assessment and abstraction sub-axis score.
10. Do not edit repository files.
11. Return ONLY valid JSON, no markdown fences.
Scope enums:
- impact_scope: "local" | "module" | "subsystem" | "codebase"
- fix_scope: "single_edit" | "multi_file_refactor" | "architectural_change"
Output schema:
{
"batch": "Cross-cutting Sweep",
"batch_index": 1,
"assessments": {"<dimension>": <0-100 with one decimal place>},
"dimension_notes": {
"<dimension>": {
"evidence": ["specific code observations"],
"impact_scope": "local|module|subsystem|codebase",
"fix_scope": "single_edit|multi_file_refactor|architectural_change",
"confidence": "high|medium|low",
"unreported_risk": "required when score >85",
"sub_axes": {"abstraction_leverage": 0-100 with one decimal place, "indirection_cost": 0-100 with one decimal place, "interface_honesty": 0-100 with one decimal place} // required for abstraction_fitness when evidence supports it
}
},
"findings": []
}
@@ -0,0 +1,86 @@
{
"assessments": {
"error_consistency": 56.5
},
"dimension_notes": {
"error_consistency": {
"evidence": [
"CLI command handlers frequently return upstream errors without operation context (e.g., cmd/get.go:47, cmd/scrape.go:91/126, cmd/push.go:52, cmd/ask.go:130), while nearby code in the same command set does wrap with contextual messages (e.g., cmd/push.go:81/91/97, cmd/scrape.go:132/146/204/210).",
"Server transport paths mix wrapped and raw propagation: write/encode paths use wrapTransportError and contextual fmt.Errorf (internal/server/server.go:223/229/267/273), but Start returns raw listener/scanner errors directly (internal/server/server.go:171/234).",
"Python CLI surfaces process-fatal errors inconsistently across sibling commands: direct sys.exit after print (cmd/devour_enhanced.py:600/607/616, cmd/devour_enhanced_v2.py:601/608/617), raise SystemExit in another command flow (desloppify/.../app/commands/next.py:137), and validation-time sys.exit in resolve flow (desloppify/.../app/commands/resolve/selection.py:84/87)."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high",
"unreported_risk": ""
}
},
"findings": [
{
"dimension": "error_consistency",
"identifier": "cli_raw_error_passthrough_inconsistent_context",
"summary": "Sibling CLI commands mix raw error passthrough and contextual wrapping.",
"related_files": [
"cmd/get.go",
"cmd/scrape.go",
"cmd/push.go",
"cmd/ask.go"
],
"evidence": [
"runGet returns constructDocURL error directly (cmd/get.go:47).",
"runScrape returns load/scrape errors directly (cmd/scrape.go:91, cmd/scrape.go:126).",
"runPush returns loadAppConfig error directly (cmd/push.go:52), but wraps later failures with operation labels (cmd/push.go:81, cmd/push.go:91, cmd/push.go:97).",
"runAsk similarly passes config load error directly (cmd/ask.go:130)."
],
"suggestion": "Standardize command-boundary wrapping: every external call failure should be wrapped with command+operation context (for example, \"load app config for get command: %w\") before returning.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "transport_error_contract_mixed_wrapped_and_raw",
"summary": "Server transport code alternates between wrapped errors and raw returns.",
"related_files": [
"internal/server/server.go",
"cmd/serve.go"
],
"evidence": [
"RPC setup/handler paths in cmd/serve consistently wrap with operation context (e.g., cmd/serve.go:69, cmd/serve.go:96, cmd/serve.go:107).",
"internal server code has contextual wrappers for encode paths (internal/server/server.go:223, 229, 267, 273), but returns raw runtime errors in Start (internal/server/server.go:171, 234)."
],
"suggestion": "Define a transport-level error policy (wrapped with transport+operation context vs typed sentinel mapping) and apply it to listener/scanner failure returns in Start for parity with existing wrapped encode paths.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "python_cli_exit_strategy_fragmented",
"summary": "Python command flows use inconsistent fatal-error signaling patterns.",
"related_files": [
"cmd/devour_enhanced.py",
"cmd/devour_enhanced_v2.py",
"desloppify/desloppify/desloppify/app/commands/next.py",
"desloppify/desloppify/desloppify/app/commands/resolve/selection.py"
],
"evidence": [
"Enhanced scripts print then call sys.exit(1) in multiple branches (cmd/devour_enhanced.py:600/607/616; cmd/devour_enhanced_v2.py:601/608/617).",
"Another command raises SystemExit directly on output failure (desloppify/.../next.py:137).",
"Resolve validation path also exits process directly via sys.exit(1) (desloppify/.../resolve/selection.py:84/87)."
],
"suggestion": "Adopt one CLI failure contract for Python commands (for example, raise a command exception and let one top-level runner map to exit code and formatted output) instead of mixing local sys.exit, print+exit, and raise SystemExit.",
"confidence": "medium",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
}
],
"review_quality": {
"batch_count": 1,
"dimension_coverage": 1.0,
"evidence_density": 1.0,
"high_score_without_risk": 0,
"finding_pressure": 7.244,
"dimensions_with_findings": 1
}
}
@@ -0,0 +1,125 @@
You are a focused subagent reviewer for a single holistic investigation batch.
Repository root: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour
Immutable packet: /home/tdvorak/Desktop/PROG_projekty/GOLANG/Devour/.desloppify/review_packets/holistic_packet_20260224_110336.json
Batch index: 1
Batch name: Cross-cutting Sweep
Batch dimensions: error_consistency
Batch rationale: selected dimensions had no direct batch mapping; review representative cross-cutting files
Files assigned:
- internal/quality/enhanced_types.go
- internal/quality/scoring_test.go
- internal/quality/types.go
- pkg/rustdocs/parser_test.go
- internal/config/config.go
- cmd/scrape.go
- internal/quality/plugins/go/analyzers/detectors.go
- internal/quality/plugins/go/analyzers/advanced.go
- internal/scraper/web.go
- internal/quality/plugins/go/plugin.go
- internal/scheduler/scheduler.go
- cmd/push.go
- internal/scraper/localsearch_test.go
- cmd/ask.go
- internal/ai/openai.go
- internal/server/server.go
- cmd/get.go
- internal/quality/analyzers/controlflow.go
- internal/vector/store.go
- examples/demo_scrapers.go
- internal/indexer/indexer.go
- internal/scraper/openapi.go
- pkg/pythondocs/parser.go
- cmd/get_test.go
- internal/quality/scanner_test.go
- internal/scraper/localsearch.go
- cmd/serve.go
- internal/scraper/external/nuxtdocs.go
- internal/quality/scanner.go
- internal/quality/plugins/go/analyzers/test_coverage.go
- internal/search/engine.go
- internal/scraper/github.go
- internal/scraper/external/astrodocs.go
- internal/scraper/external/cloudflaredocs.go
- internal/scraper/external/dockerdocs.go
- internal/quality/plugins/go/fixers/advanced_fixers.go
- cleanup_unused.go
- main.go
- cmd/ask_test.go
- cmd/auto.go
- internal/scraper/local.go
- internal/scraper/local_test.go
- README.md
- .desloppify/config.json
- .desloppify/query.json
- .desloppify/subagents/runs/20260223_100953/prompts/batch-1.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-2.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-3.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-4.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-5.md
- .desloppify/subagents/runs/20260223_100953/prompts/batch-6.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-1.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-2.md
- .desloppify/subagents/runs/20260224_101201/prompts/batch-5.md
- .desloppify/subagents/runs/20260224_101740/prompts/batch-1.md
- .desloppify/subagents/runs/20260224_104658/prompts/batch-1.md
- .github/workflows/ci.yml
- AGENTS.md
- cmd/devour_enhanced.py
- cmd/devour_enhanced_fixed.py
- cmd/devour_enhanced_v2.py
- desloppify/desloppify/desloppify/app/commands/_show_terminal.py
- desloppify/desloppify/desloppify/app/commands/fix/apply_flow.py
- desloppify/desloppify/desloppify/app/commands/issues_cmd.py
- desloppify/desloppify/desloppify/app/commands/next.py
- desloppify/desloppify/desloppify/app/commands/resolve/selection.py
- desloppify/desloppify/desloppify/app/commands/scan/scan_reporting_llm.py
- desloppify/desloppify/desloppify/app/commands/status_parts/render.py
- desloppify/desloppify/desloppify/app/output/scorecard_parts/projection.py
- desloppify/desloppify/desloppify/engine/detectors/security/rules.py
- desloppify/desloppify/desloppify/engine/scoring_internal/subjective/core.py
- desloppify/desloppify/desloppify/engine/state_internal/resolution.py
- desloppify/desloppify/desloppify/intelligence/review/__init__.py
- desloppify/desloppify/desloppify/intelligence/review/context_internal/structure.py
- desloppify/desloppify/desloppify/intelligence/review/dimensions/data.py
- desloppify/desloppify/desloppify/intelligence/review/importing/holistic.py
- desloppify/desloppify/desloppify/languages/_shared/phases_common.py
- desloppify/desloppify/desloppify/languages/_shared/review_data/dimensions.json
- desloppify/desloppify/desloppify/languages/_shared/scaffold_detect_commands.py
- desloppify/desloppify/desloppify/languages/csharp/_parse_helpers.py
Task requirements:
1. Read the immutable packet and follow `system_prompt` constraints exactly.
2. Evaluate ONLY listed files and ONLY listed dimensions for this batch.
3. Return 0-10 high-quality findings for this batch (empty array allowed).
4. Score/finding consistency is required: broader or more severe findings MUST lower dimension scores.
5. Every finding must include `related_files` with at least 2 files when possible.
6. Every finding must include `impact_scope` and `fix_scope`.
7. Every scored dimension MUST include dimension_notes with concrete evidence.
8. If a dimension score is >85, include `unreported_risk` in dimension_notes.
9. Use exactly one decimal place for every assessment and abstraction sub-axis score.
10. Do not edit repository files.
11. Return ONLY valid JSON, no markdown fences.
Scope enums:
- impact_scope: "local" | "module" | "subsystem" | "codebase"
- fix_scope: "single_edit" | "multi_file_refactor" | "architectural_change"
Output schema:
{
"batch": "Cross-cutting Sweep",
"batch_index": 1,
"assessments": {"<dimension>": <0-100 with one decimal place>},
"dimension_notes": {
"<dimension>": {
"evidence": ["specific code observations"],
"impact_scope": "local|module|subsystem|codebase",
"fix_scope": "single_edit|multi_file_refactor|architectural_change",
"confidence": "high|medium|low",
"unreported_risk": "required when score >85",
"sub_axes": {"abstraction_leverage": 0-100 with one decimal place, "indirection_cost": 0-100 with one decimal place, "interface_honesty": 0-100 with one decimal place} // required for abstraction_fitness when evidence supports it
}
},
"findings": []
}
@@ -0,0 +1,79 @@
{
"batch": "Cross-cutting Sweep",
"batch_index": 1,
"assessments": {
"error_consistency": 74.0
},
"dimension_notes": {
"error_consistency": {
"evidence": [
"CLI command handlers frequently return upstream errors without operation context (e.g., cmd/get.go:47, cmd/scrape.go:91/126, cmd/push.go:52, cmd/ask.go:130), while nearby code in the same command set does wrap with contextual messages (e.g., cmd/push.go:81/91/97, cmd/scrape.go:132/146/204/210).",
"Server transport paths mix wrapped and raw propagation: write/encode paths use wrapTransportError and contextual fmt.Errorf (internal/server/server.go:223/229/267/273), but Start returns raw listener/scanner errors directly (internal/server/server.go:171/234).",
"Python CLI surfaces process-fatal errors inconsistently across sibling commands: direct sys.exit after print (cmd/devour_enhanced.py:600/607/616, cmd/devour_enhanced_v2.py:601/608/617), raise SystemExit in another command flow (desloppify/.../app/commands/next.py:137), and validation-time sys.exit in resolve flow (desloppify/.../app/commands/resolve/selection.py:84/87)."
],
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor",
"confidence": "high"
}
},
"findings": [
{
"dimension": "error_consistency",
"identifier": "cli_raw_error_passthrough_inconsistent_context",
"summary": "Sibling CLI commands mix raw error passthrough and contextual wrapping.",
"related_files": [
"cmd/get.go",
"cmd/scrape.go",
"cmd/push.go",
"cmd/ask.go"
],
"evidence": [
"runGet returns constructDocURL error directly (cmd/get.go:47).",
"runScrape returns load/scrape errors directly (cmd/scrape.go:91, cmd/scrape.go:126).",
"runPush returns loadAppConfig error directly (cmd/push.go:52), but wraps later failures with operation labels (cmd/push.go:81, cmd/push.go:91, cmd/push.go:97).",
"runAsk similarly passes config load error directly (cmd/ask.go:130)."
],
"suggestion": "Standardize command-boundary wrapping: every external call failure should be wrapped with command+operation context (for example, \"load app config for get command: %w\") before returning.",
"confidence": "high",
"impact_scope": "subsystem",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "transport_error_contract_mixed_wrapped_and_raw",
"summary": "Server transport code alternates between wrapped errors and raw returns.",
"related_files": [
"internal/server/server.go",
"cmd/serve.go"
],
"evidence": [
"RPC setup/handler paths in cmd/serve consistently wrap with operation context (e.g., cmd/serve.go:69, cmd/serve.go:96, cmd/serve.go:107).",
"internal server code has contextual wrappers for encode paths (internal/server/server.go:223, 229, 267, 273), but returns raw runtime errors in Start (internal/server/server.go:171, 234)."
],
"suggestion": "Define a transport-level error policy (wrapped with transport+operation context vs typed sentinel mapping) and apply it to listener/scanner failure returns in Start for parity with existing wrapped encode paths.",
"confidence": "high",
"impact_scope": "module",
"fix_scope": "multi_file_refactor"
},
{
"dimension": "error_consistency",
"identifier": "python_cli_exit_strategy_fragmented",
"summary": "Python command flows use inconsistent fatal-error signaling patterns.",
"related_files": [
"cmd/devour_enhanced.py",
"cmd/devour_enhanced_v2.py",
"desloppify/desloppify/desloppify/app/commands/next.py",
"desloppify/desloppify/desloppify/app/commands/resolve/selection.py"
],
"evidence": [
"Enhanced scripts print then call sys.exit(1) in multiple branches (cmd/devour_enhanced.py:600/607/616; cmd/devour_enhanced_v2.py:601/608/617).",
"Another command raises SystemExit directly on output failure (desloppify/.../next.py:137).",
"Resolve validation path also exits process directly via sys.exit(1) (desloppify/.../resolve/selection.py:84/87)."
],
"suggestion": "Adopt one CLI failure contract for Python commands (for example, raise a command exception and let one top-level runner map to exit code and formatted output) instead of mixing local sys.exit, print+exit, and raise SystemExit.",
"confidence": "medium",
"impact_scope": "subsystem",
"fix_scope": "architectural_change"
}
]
}
+69 -21
View File
@@ -8,11 +8,14 @@ description: >
duplicate functions, code smells, naming issues, import cycles, or coupling duplicate functions, code smells, naming issues, import cycles, or coupling
problems. Also use when asked for a health score, what to fix next, or to problems. Also use when asked for a health score, what to fix next, or to
create a cleanup plan. Supports 28 languages. create a cleanup plan. Supports 28 languages.
allowed-tools: Bash(desloppify *) allowed-tools: Bash(devour quality*, devour review*, devour scorecard*)
--- ---
# Desloppify # Desloppify
Devour delegates quality/review operations to `desloppify`.
Use Devour entrypoints (`devour quality ...`, `devour review ...`, `devour scorecard`) so users stay in one CLI surface.
## 1. Your Job ## 1. Your Job
**Improve code quality by fixing findings and maximizing strict score honestly.** **Improve code quality by fixing findings and maximizing strict score honestly.**
@@ -33,11 +36,11 @@ Never skip scores. The user tracks progress through them.
scan → follow the tool's strategy → fix or wontfix → rescan scan → follow the tool's strategy → fix or wontfix → rescan
``` ```
1. `desloppify scan --path .` — the scan output ends with **INSTRUCTIONS FOR AGENTS**. Follow them. Don't substitute your own analysis. 1. `devour quality scan --path .` — this delegates to `desloppify scan`; follow the output **INSTRUCTIONS FOR AGENTS** exactly.
2. Fix the issue the tool recommends. 2. Fix the issue the tool recommends.
3. `desloppify resolve fixed "<id>"` — or if it's intentional/acceptable: 3. `devour quality resolve fixed "<id>"` — or if it's intentional/acceptable:
`desloppify resolve wontfix "<id>" --note "reason why"` `devour quality resolve wontfix "<id>" --note "reason why"`
4. Rescan to verify. 4. Rescan to verify via `devour quality scan --path .`.
**Wontfix is not free.** It lowers the strict score. The gap between lenient and strict IS wontfix debt. Call it out when: **Wontfix is not free.** It lowers the strict score. The gap between lenient and strict IS wontfix debt. Call it out when:
- Wontfix count is growing — challenge whether past decisions still hold - Wontfix count is growing — challenge whether past decisions still hold
@@ -47,23 +50,24 @@ scan → follow the tool's strategy → fix or wontfix → rescan
## 3. Commands ## 3. Commands
```bash ```bash
desloppify scan --path src/ # full scan devour quality scan --path src/ # full scan
desloppify scan --path src/ --reset-subjective # reset subjective baseline to 0, then scan devour quality scan --path src/ --reset-subjective # reset subjective baseline to 0, then scan
desloppify next --count 5 # top priorities devour quality next --count 5 # top priorities
desloppify show <pattern> # filter by file/detector/ID devour quality show <pattern> # filter by file/detector/ID
desloppify plan # prioritized plan devour quality plan # prioritized plan
desloppify fix <fixer> --dry-run # auto-fix (dry-run first!) devour quality fix <fixer> --dry-run # auto-fix (dry-run first!)
desloppify move <src> <dst> --dry-run # move + update imports devour quality move <src> <dst> --dry-run # move + update imports
desloppify resolve fixed|wontfix|false_positive "<pat>" # classify finding outcome devour quality resolve fixed|wontfix|false_positive "<pat>" # classify finding outcome
desloppify review --prepare # generate subjective review data devour review --prepare # generate subjective review data
desloppify review --import file.json # import review results devour review --import file.json # import review results
devour review # default batch run (codex+parallel+scan-after-import)
``` ```
## 4. Subjective Reviews (biggest score lever) ## 4. Subjective Reviews (biggest score lever)
Score = 40% mechanical + 60% subjective. Subjective starts at 0% until reviewed. Score = 40% mechanical + 60% subjective. Subjective starts at 0% until reviewed.
1. `desloppify review --prepare` writes dimension definitions and codebase context 1. `devour review --prepare` — delegates to `desloppify review --prepare` and writes dimension definitions and codebase context
to `query.json`. to `query.json`.
2. **Review each dimension independently.** For best results, review dimensions in 2. **Review each dimension independently.** For best results, review dimensions in
@@ -78,7 +82,7 @@ Score = 40% mechanical + 60% subjective. Subjective starts at 0% until reviewed.
3. Merge assessments (average scores if multiple reviewers cover the same dimension) 3. Merge assessments (average scores if multiple reviewers cover the same dimension)
and findings, then import: and findings, then import:
```bash ```bash
desloppify review --import findings.json devour review --import findings.json
``` ```
Required output format per reviewer: Required output format per reviewer:
@@ -96,7 +100,7 @@ Score = 40% mechanical + 60% subjective. Subjective starts at 0% until reviewed.
} }
``` ```
Need a clean subjective rerun from zero? Run `desloppify scan --path src/ --reset-subjective` before preparing/importing fresh review data. Need a clean subjective rerun from zero? Run `devour quality scan --path src/ --reset-subjective` before preparing/importing fresh review data.
Even moderate scores (60-80) dramatically improve overall health. Even moderate scores (60-80) dramatically improve overall health.
@@ -127,7 +131,51 @@ When desloppify itself appears wrong or inconsistent:
## Prerequisite ## Prerequisite
`command -v desloppify >/dev/null 2>&1 && echo "desloppify: installed" || echo "NOT INSTALLED — run: pip install --upgrade git+https://github.com/peteromallet/desloppify.git"` `command -v desloppify >/dev/null 2>&1 && echo "desloppify: installed" || echo "NOT INSTALLED — run: pip install --upgrade \"desloppify[full]\""`
## Install-and-Run Prompt (Copy/Paste)
```text
Install or update desloppify, then scan this codebase for code quality issues
(dead code, duplication, complexity, smells). It tracks findings persistently
across sessions and reports a health score.
Requires Python 3.11+. Install from PyPI:
pip install --upgrade desloppify
# Recommended for deepest coverage (tree-sitter + Python security adapter):
pip install --upgrade "desloppify[full]"
desloppify update-skill codex # pick yours: claude, cursor, codex, copilot, windsurf, gemini
Use Devour wrappers (delegates to desloppify):
devour quality scan --path .
devour quality status
devour quality next
Direct equivalent commands:
desloppify scan --path .
desloppify status
desloppify next
--path is the directory to scan (use "." for the whole project, or "src/" etc).
Language is auto-detected. To override: desloppify --lang python scan --path .
(note: --lang goes BEFORE the subcommand)
Fix what it finds, then:
desloppify resolve fixed <id> --note "what changed" --attest "I have actually [DESCRIBE THE CONCRETE CHANGE YOU MADE] and I am not gaming the score by resolving without fixing."
For false positives:
desloppify resolve wontfix <id> --note "reason" --attest "I have actually verified this is intentional/false-positive and I am not gaming the score by resolving without fixing."
If subjective scores feel stale or inflated:
desloppify scan --path . --reset-subjective
Because state persists, run regularly (for example before each push).
If anything seems wrong/confusing in desloppify itself, capture a repro and ask
whether to log an upstream issue.
If you need to debug desloppify internals:
git clone https://github.com/peteromallet/desloppify.git /tmp/desloppify
```
<!-- desloppify-end --> <!-- desloppify-end -->
@@ -135,7 +183,7 @@ When desloppify itself appears wrong or inconsistent:
This is the canonical Codex overlay used by the README install command. This is the canonical Codex overlay used by the README install command.
1. Prefer first-class batch runs: `desloppify review --run-batches --runner codex --parallel`. 1. Prefer first-class batch runs: `devour review` (default) or `devour review --run-batches --runner codex --parallel`.
2. The command writes immutable packet snapshots under `.desloppify/review_packets/holistic_packet_*.json`; use those for reproducible retries. 2. The command writes immutable packet snapshots under `.desloppify/review_packets/holistic_packet_*.json`; use those for reproducible retries.
3. Keep reviewer input scoped to the immutable packet and the source files named in each batch. 3. Keep reviewer input scoped to the immutable packet and the source files named in each batch.
4. Do not use prior chat context, score history, narrative summaries, issue labels, or target-threshold anchoring while scoring. 4. Do not use prior chat context, score history, narrative summaries, issue labels, or target-threshold anchoring while scoring.
@@ -152,7 +200,7 @@ This is the canonical Codex overlay used by the README install command.
``` ```
7. Keep `findings` schema compatible with `query.system_prompt`. 7. Keep `findings` schema compatible with `query.system_prompt`.
8. If a batch fails, retry only that slice with `desloppify review --run-batches --packet <packet.json> --only-batches <idxs>`. 8. If a batch fails, retry only that slice with `devour review --run-batches --packet <packet.json> --only-batches <idxs>`.
<!-- desloppify-overlay: codex --> <!-- desloppify-overlay: codex -->
<!-- desloppify-end --> <!-- desloppify-end -->
+7 -1
View File
@@ -25,6 +25,12 @@ Optional sanity check:
./devour --help ./devour --help
``` ```
For quality/review commands, install the delegated upstream tool first:
```bash
pip install --upgrade "desloppify[full]"
```
## Branch and Commit Workflow ## Branch and Commit Workflow
1. Fork the repo. 1. Fork the repo.
@@ -50,7 +56,7 @@ At minimum, run:
go test ./... go test ./...
``` ```
If your changes affect CLI behavior, also run relevant commands directly (for example `devour get`, `devour ask`, `devour quality status`). If your changes affect CLI behavior, also run relevant commands directly (for example `devour get`, `devour ask`, `devour quality scan --path .`, `devour review --prepare`).
If you touch docs, verify commands and paths are real. If you touch docs, verify commands and paths are real.
+59 -1
View File
@@ -5,6 +5,11 @@
<h1 align="center">Devour</h1> <h1 align="center">Devour</h1>
<p align="center">Feed your AI real docs so it stops repeating the same mistakes.</p> <p align="center">Feed your AI real docs so it stops repeating the same mistakes.</p>
Code quality and review workflows are powered by [`peteromallet/desloppify`](https://github.com/peteromallet/desloppify), integrated behind Devour commands.
## Code Health Scorecard
<img src="scorecard.png" width="100%">
## Why Devour exists ## Why Devour exists
I built Devour because AI tools kept drifting away from official docs, then repeating wrong patterns in later prompts. I built Devour because AI tools kept drifting away from official docs, then repeating wrong patterns in later prompts.
@@ -81,7 +86,60 @@ go build -o devour ./cmd/devour
| `devour serve` | Start local stdio JSON-RPC server | | `devour serve` | Start local stdio JSON-RPC server |
| `devour auto "<intent>"` | Auto-route intent to command | | `devour auto "<intent>"` | Auto-route intent to command |
| `devour verify smoke` | Live smoke verification report | | `devour verify smoke` | Live smoke verification report |
| `devour quality ...` | Code quality scan/triage/fixes | | `devour quality ...` | Thin passthrough to `desloppify` |
| `devour review ...` | Holistic subjective review workflow via `desloppify review` |
| `devour scorecard` | Generate `desloppify` badge output via scan |
## Quality Prerequisite
Install `desloppify` before running `devour quality` / `devour review` / `devour scorecard`:
```bash
pip install --upgrade "desloppify[full]"
```
### AI Setup Prompt (Copy/Paste)
```text
Install or update desloppify, then scan this codebase for code quality issues
(dead code, duplication, complexity, smells). It tracks findings persistently
across sessions and reports a health score.
Requires Python 3.11+. Install from PyPI:
pip install --upgrade desloppify
# Recommended for deepest coverage (tree-sitter + Python security adapter):
pip install --upgrade "desloppify[full]"
desloppify update-skill codex # pick yours: claude, cursor, codex, copilot, windsurf, gemini
Use Devour wrappers (delegates to desloppify):
devour quality scan --path .
devour quality status
devour quality next
Direct equivalent commands:
desloppify scan --path .
desloppify status
desloppify next
--path is the directory to scan (use "." for the whole project, or "src/" etc).
Language is auto-detected. To override: desloppify --lang python scan --path .
(note: --lang goes BEFORE the subcommand)
Fix what it finds, then:
desloppify resolve fixed <id> --note "what changed" --attest "I have actually [DESCRIBE THE CONCRETE CHANGE YOU MADE] and I am not gaming the score by resolving without fixing."
For false positives:
desloppify resolve wontfix <id> --note "reason" --attest "I have actually verified this is intentional/false-positive and I am not gaming the score by resolving without fixing."
If subjective scores feel stale or inflated:
desloppify scan --path . --reset-subjective
Because state persists, run regularly (for example before each push).
If anything seems wrong/confusing in desloppify itself, capture a repro and ask
whether to log an upstream issue.
If you need to debug desloppify internals:
git clone https://github.com/peteromallet/desloppify.git /tmp/desloppify
```
## Supported `get` / `ask` languages and frameworks ## Supported `get` / `ask` languages and frameworks
- Go (`go`, `golang`) - Go (`go`, `golang`)
+2 -1
View File
@@ -33,7 +33,8 @@ Use this skill when a task is explicitly about Devour CLI operations or troubles
- `devour serve` (local stdio JSON-RPC) - `devour serve` (local stdio JSON-RPC)
- `devour auto` - `devour auto`
- `devour verify smoke` - `devour verify smoke`
- `devour quality ...` - `devour quality ...` (delegated to `desloppify`)
- `devour review ...` (delegated to `desloppify review`)
Remote server/push workflows are experimental. Remote server/push workflows are experimental.
+50 -14
View File
@@ -3,6 +3,7 @@ package cmd
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"path" "path"
@@ -79,6 +80,19 @@ type rankedDoc struct {
searchTerm string searchTerm string
} }
type askPersistenceWarning struct {
operation string
cause error
}
func (w *askPersistenceWarning) Error() string {
return fmt.Sprintf("persistence warning: %s: %v", w.operation, w.cause)
}
func (w *askPersistenceWarning) Unwrap() error {
return w.cause
}
func init() { func init() {
askCmd.Flags().StringVar(&askLanguage, "lang", "", "language/framework (required)") askCmd.Flags().StringVar(&askLanguage, "lang", "", "language/framework (required)")
askCmd.Flags().StringVarP(&askFormat, "format", "f", "json", "output format (json, text)") askCmd.Flags().StringVarP(&askFormat, "format", "f", "json", "output format (json, text)")
@@ -113,7 +127,7 @@ func runAsk(cmd *cobra.Command, args []string) error {
cfg, err := loadAppConfig() cfg, err := loadAppConfig()
if err != nil { if err != nil {
return err return fmt.Errorf("load app config for ask command: %w", err)
} }
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(askTimeoutSec)*time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(askTimeoutSec)*time.Second)
@@ -129,7 +143,10 @@ func runAsk(cmd *cobra.Command, args []string) error {
fallbackNeeded := shouldFallbackToLive(localRanked, terms) fallbackNeeded := shouldFallbackToLive(localRanked, terms)
fallbackCount := 0 fallbackCount := 0
fetchErrors := []string{} fetchErrors := []error{}
if localErr != nil {
fetchErrors = append(fetchErrors, fmt.Errorf("local retrieval failed: %w", localErr))
}
if fallbackNeeded { if fallbackNeeded {
fallbackDocs, fetched, errs := fetchAskDocsFromLive(ctx, cfg, language, question, terms) fallbackDocs, fetched, errs := fetchAskDocsFromLive(ctx, cfg, language, question, terms)
fallbackCount = fetched fallbackCount = fetched
@@ -143,7 +160,10 @@ func runAsk(cmd *cobra.Command, args []string) error {
} }
if len(ranked) == 0 { if len(ranked) == 0 {
return fmt.Errorf("no docs found for %q. errors: %s", language, strings.Join(fetchErrors, "; ")) if len(fetchErrors) == 0 {
return fmt.Errorf("no docs found for %q", language)
}
return fmt.Errorf("no docs found for %q: %w", language, errors.Join(fetchErrors...))
} }
sort.Slice(ranked, func(i, j int) bool { sort.Slice(ranked, func(i, j int) bool {
@@ -180,6 +200,12 @@ func runAsk(cmd *cobra.Command, args []string) error {
Confidence: computeConfidence(question, top), Confidence: computeConfidence(question, top),
FetchedAt: time.Now(), FetchedAt: time.Now(),
} }
for _, fetchErr := range fetchErrors {
var persistenceWarning *askPersistenceWarning
if errors.As(fetchErr, &persistenceWarning) {
response.Answer.Notes = append(response.Answer.Notes, persistenceWarning.Error())
}
}
switch strings.ToLower(askFormat) { switch strings.ToLower(askFormat) {
case "text": case "text":
@@ -290,20 +316,20 @@ func resultMatchesLanguage(result search.Result, language string) bool {
} }
} }
func fetchAskDocsFromLive(ctx context.Context, cfg *appconfig.Config, language, question string, terms []string) ([]rankedDoc, int, []string) { func fetchAskDocsFromLive(ctx context.Context, cfg *appconfig.Config, language, question string, terms []string) ([]rankedDoc, int, []error) {
sourceType := scraper.SourceType(mapLanguageToType(language)) sourceType := scraper.SourceType(mapLanguageToType(language))
if sourceType == "" { if sourceType == "" {
return nil, 0, []string{fmt.Sprintf("unsupported language: %s", language)} return nil, 0, []error{fmt.Errorf("unsupported language: %s", language)}
} }
sc := toScraperConfig(cfg, 2) sc := toScraperConfig(cfg, 2)
sc.MaxDepth = 1 sc.MaxDepth = 1
s := scraper.NewScraper(sourceType, sc) s := scraper.NewScraper(sourceType, sc)
if s == nil { if s == nil {
return nil, 0, []string{fmt.Sprintf("no scraper for %s (%s)", language, sourceType)} return nil, 0, []error{fmt.Errorf("no scraper for %s (%s)", language, sourceType)}
} }
var ranked []rankedDoc var ranked []rankedDoc
var fetchErrors []string var fetchErrors []error
seenURL := make(map[string]bool) seenURL := make(map[string]bool)
totalFetched := 0 totalFetched := 0
fetchedDocs := make([]*scraper.Document, 0) fetchedDocs := make([]*scraper.Document, 0)
@@ -311,11 +337,11 @@ func fetchAskDocsFromLive(ctx context.Context, cfg *appconfig.Config, language,
for _, term := range terms { for _, term := range terms {
docURLs, err := candidateDocURLs(language, term) docURLs, err := candidateDocURLs(language, term)
if err != nil { if err != nil {
fetchErrors = append(fetchErrors, fmt.Sprintf("%s: %v", term, err)) fetchErrors = append(fetchErrors, fmt.Errorf("%s: %w", term, err))
continue continue
} }
termFetched := false termFetched := false
termErrors := make([]string, 0, len(docURLs)) termErrors := make([]error, 0, len(docURLs))
for _, docURL := range docURLs { for _, docURL := range docURLs {
if seenURL[docURL] { if seenURL[docURL] {
continue continue
@@ -331,11 +357,11 @@ func fetchAskDocsFromLive(ctx context.Context, cfg *appconfig.Config, language,
docs, err := s.Scrape(ctx, source) docs, err := s.Scrape(ctx, source)
if err != nil { if err != nil {
termErrors = append(termErrors, fmt.Sprintf("%s: %v", docURL, err)) termErrors = append(termErrors, fmt.Errorf("%s: %w", docURL, err))
continue continue
} }
if len(docs) == 0 { if len(docs) == 0 {
termErrors = append(termErrors, fmt.Sprintf("%s: no documents extracted", docURL)) termErrors = append(termErrors, fmt.Errorf("%s: no documents extracted", docURL))
continue continue
} }
@@ -353,20 +379,30 @@ func fetchAskDocsFromLive(ctx context.Context, cfg *appconfig.Config, language,
break break
} }
if !termFetched && len(termErrors) > 0 { if !termFetched && len(termErrors) > 0 {
fetchErrors = append(fetchErrors, strings.Join(termErrors, " | ")) fetchErrors = append(fetchErrors, errors.Join(termErrors...))
} }
} }
// Persist fallback docs for future local-first queries. // Persist fallback docs for future local-first queries.
if len(fetchedDocs) > 0 { if len(fetchedDocs) > 0 {
_, _ = storage.SaveDocuments(fetchedDocs, storage.SaveOptions{ if _, err := storage.SaveDocuments(fetchedDocs, storage.SaveOptions{
Format: "json", Format: "json",
OutputDir: cfg.Storage.DocsDir, OutputDir: cfg.Storage.DocsDir,
AllowEmpty: true, AllowEmpty: true,
}); err != nil {
fetchErrors = append(fetchErrors, &askPersistenceWarning{
operation: "save fallback docs",
cause: err,
}) })
}
if cfg.Indexing.Enabled { if cfg.Indexing.Enabled {
engine := search.NewEngine(cfg) engine := search.NewEngine(cfg)
_, _ = engine.Rebuild(context.Background()) if _, err := engine.Rebuild(context.Background()); err != nil {
fetchErrors = append(fetchErrors, &askPersistenceWarning{
operation: "rebuild index after fallback",
cause: err,
})
}
} }
} }
+101
View File
@@ -0,0 +1,101 @@
package cmd
import (
"context"
"errors"
"fmt"
"io"
"os/exec"
"github.com/spf13/cobra"
)
const desloppifyAttribution = "Powered by peteromallet/desloppify"
const missingDesloppifyGuidance = `desloppify was not found in PATH.
Install or update desloppify, then scan this codebase for code quality issues
(dead code, duplication, complexity, smells). It tracks findings persistently
across sessions and reports a health score.
Requires Python 3.11+. Install from PyPI:
pip install --upgrade desloppify
pip install --upgrade "desloppify[full]"
desloppify update-skill codex
Use Devour wrappers (delegates to desloppify):
devour quality scan --path .
devour quality status
devour quality next
Direct equivalent commands:
desloppify scan --path .
desloppify status
desloppify next
--path is the directory to scan ("." for project root).
Language is auto-detected. To override:
desloppify --lang python scan --path .
Note: --lang goes BEFORE the subcommand.
Fix findings, then resolve and rescan:
desloppify resolve fixed <id> --note "what changed" --attest "I have actually [DESCRIBE THE CONCRETE CHANGE YOU MADE] and I am not gaming the score by resolving without fixing."
For false positives or intentional debt:
desloppify resolve wontfix <id> --note "reason" --attest "I have actually verified this is intentional/false-positive and I am not gaming the score by resolving without fixing."
If subjective scores feel stale or inflated:
desloppify scan --path . --reset-subjective
Run regularly (for example before each push). If desloppify itself looks wrong
or confusing, capture a repro and consider filing upstream:
git clone https://github.com/peteromallet/desloppify.git /tmp/desloppify`
func runDesloppifyFromCommand(cmd *cobra.Command, args []string, withAttribution bool) error {
return runDesloppifyWithIO(
cmd.Context(),
cmd.InOrStdin(),
cmd.OutOrStdout(),
cmd.ErrOrStderr(),
args,
withAttribution,
)
}
func runDesloppifyWithIO(
ctx context.Context,
stdin io.Reader,
stdout io.Writer,
stderr io.Writer,
args []string,
withAttribution bool,
) error {
if withAttribution {
fmt.Fprintf(stderr, "devour quality/review is %s\n", desloppifyAttribution)
}
binaryPath, err := exec.LookPath("desloppify")
if err != nil {
printDesloppifyInstallGuidance(stderr)
return fmt.Errorf("desloppify is required: %w", err)
}
run := exec.CommandContext(ctx, binaryPath, args...)
run.Stdin = stdin
run.Stdout = stdout
run.Stderr = stderr
if err := run.Run(); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return err
}
return fmt.Errorf("failed to run desloppify: %w", err)
}
return nil
}
func printDesloppifyInstallGuidance(stderr io.Writer) {
fmt.Fprintln(stderr, missingDesloppifyGuidance)
}
+136
View File
@@ -0,0 +1,136 @@
package cmd
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestQualityPassthroughScanArgs(t *testing.T) {
logFile := setupFakeDesloppify(t)
stdout, stderr, err := runRootCommand(t, "quality", "scan", "--path", ".")
if err != nil {
t.Fatalf("quality scan returned error: %v\nstderr:\n%s", err, stderr)
}
logged := readFileTrimmed(t, logFile)
if logged != "scan --path ." {
t.Fatalf("forwarded args = %q, want %q", logged, "scan --path .")
}
if !strings.Contains(stderr, desloppifyAttribution) {
t.Fatalf("stderr missing attribution, got:\n%s", stderr)
}
if !strings.Contains(stdout, "fake stdout") {
t.Fatalf("stdout missing fake tool output, got:\n%s", stdout)
}
}
func TestQualityNoArgsForwardsHelp(t *testing.T) {
logFile := setupFakeDesloppify(t)
_, stderr, err := runRootCommand(t, "quality")
if err != nil {
t.Fatalf("quality returned error: %v\nstderr:\n%s", err, stderr)
}
logged := readFileTrimmed(t, logFile)
if logged != "--help" {
t.Fatalf("forwarded args = %q, want %q", logged, "--help")
}
}
func TestReviewNoArgsRunsDefaultBatchFlow(t *testing.T) {
logFile := setupFakeDesloppify(t)
_, stderr, err := runRootCommand(t, "review")
if err != nil {
t.Fatalf("review returned error: %v\nstderr:\n%s", err, stderr)
}
logged := readFileTrimmed(t, logFile)
want := "review --run-batches --runner codex --parallel --scan-after-import"
if logged != want {
t.Fatalf("forwarded args = %q, want %q", logged, want)
}
}
func TestReviewForwardsArgs(t *testing.T) {
logFile := setupFakeDesloppify(t)
_, stderr, err := runRootCommand(t, "review", "--prepare")
if err != nil {
t.Fatalf("review --prepare returned error: %v\nstderr:\n%s", err, stderr)
}
logged := readFileTrimmed(t, logFile)
if logged != "review --prepare" {
t.Fatalf("forwarded args = %q, want %q", logged, "review --prepare")
}
}
func TestMissingDesloppifyShowsInstallGuidance(t *testing.T) {
t.Setenv("PATH", t.TempDir())
_, stderr, err := runRootCommand(t, "quality", "scan", "--path", ".")
if err == nil {
t.Fatal("expected error when desloppify is missing")
}
if !strings.Contains(stderr, "desloppify was not found in PATH") {
t.Fatalf("stderr missing missing-binary guidance, got:\n%s", stderr)
}
if !strings.Contains(stderr, "pip install --upgrade \"desloppify[full]\"") {
t.Fatalf("stderr missing install command, got:\n%s", stderr)
}
}
func setupFakeDesloppify(t *testing.T) string {
t.Helper()
tmpDir := t.TempDir()
logFile := filepath.Join(tmpDir, "args.log")
scriptPath := filepath.Join(tmpDir, "desloppify")
script := "#!/bin/sh\n" +
"printf '%s\\n' \"$*\" > \"$DEVOUR_TEST_LOG\"\n" +
"echo fake stdout\n" +
"echo fake stderr 1>&2\n" +
"exit 0\n"
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
t.Fatalf("failed to write fake desloppify script: %v", err)
}
oldPath := os.Getenv("PATH")
t.Setenv("PATH", tmpDir+string(os.PathListSeparator)+oldPath)
t.Setenv("DEVOUR_TEST_LOG", logFile)
return logFile
}
func runRootCommand(t *testing.T, args ...string) (string, string, error) {
t.Helper()
var outBuf bytes.Buffer
var errBuf bytes.Buffer
rootCmd.SetOut(&outBuf)
rootCmd.SetErr(&errBuf)
rootCmd.SetArgs(args)
_, err := rootCmd.ExecuteC()
return outBuf.String(), errBuf.String(), err
}
func readFileTrimmed(t *testing.T, path string) string {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("failed to read %s: %v", path, err)
}
return strings.TrimSpace(string(data))
}
-55
View File
@@ -1,55 +0,0 @@
#!/usr/bin/env python3
"""
Devour Scorecard CLI - Direct interface to generate scorecards from JSON data.
Usage: devour-scorecard <input.json> <output.png>
"""
import sys
import os
from pathlib import Path
# Add the cmd directory to Python path for imports
sys.path.insert(0, str(Path(__file__).parent))
try:
from devour_scorecard import load_devour_data, generate_scorecard
except ImportError as e:
print(f"Error importing scorecard module: {e}")
sys.exit(1)
def main():
if len(sys.argv) != 3:
print("Usage: devour-scorecard <input.json> <output.png>")
print("")
print("Examples:")
print(" devour-scorecard devour_data/quality/status.json scorecard.png")
print(" devour-scorecard scan_results.json health_badge.png")
sys.exit(1)
input_path = sys.argv[1]
output_path = sys.argv[2]
if not os.path.exists(input_path):
print(f"Error: Input file '{input_path}' not found")
sys.exit(1)
try:
# Load data and generate scorecard
data = load_devour_data(input_path)
result_path = generate_scorecard(data, output_path)
# Calculate file sizes for info
input_size = os.path.getsize(input_path)
output_size = os.path.getsize(result_path)
print(f"✅ Scorecard generated successfully!")
print(f"📁 Output: {result_path}")
print(f"📊 Input: {input_size:,} bytes → Output: {output_size:,} bytes")
print(f"📈 Dimensions: {len(data.dimensions)} categories analyzed")
except Exception as e:
print(f"❌ Error generating scorecard: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-518
View File
@@ -1,518 +0,0 @@
#!/usr/bin/env python3
"""
Devour Scorecard Generator - 1:1 recreation of desloppify scorecard style.
Generates visual health summary PNG with the exact same data structure and visual design.
"""
from __future__ import annotations
import json
import logging
import os
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Tuple, Any, Optional
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
print("Error: PIL/Pillow required. Install with: pip install Pillow")
sys.exit(1)
logger = logging.getLogger(__name__)
# Visual constants matching desloppify theme
SCALE = 2 # 2x for retina/high-DPI
BG = (248, 248, 246) # Light gray background
FRAME = (222, 222, 220) # Border frame
BORDER = (200, 200, 198) # Inner border
ACCENT = (88, 166, 255) # Blue accent
TEXT = (40, 44, 52) # Dark text
DIM = (140, 140, 140) # Dimmed text
BG_SCORE = (255, 255, 255) # Score background
BG_TABLE = (255, 255, 255) # Table background
BG_ROW_ALT = (250, 250, 248) # Alternating row background
@dataclass
class ScorecardData:
"""Data structure matching desloppify scorecard format."""
project_name: str
version: str
main_score: float
strict_score: float
dimensions: List[Tuple[str, Dict[str, Any]]]
def score_color(score: float, *, muted: bool = False) -> Tuple[int, int, int]:
"""Color-code a score: deep sage >= 90, mustard 70-90, dusty rose < 70.
muted=True returns a desaturated variant for secondary display (strict column).
"""
if score >= 90:
base = (68, 120, 68) # deep sage
elif score >= 70:
base = (120, 140, 72) # olive green
else:
base = (145, 155, 80) # yellow-green
if not muted:
return base
# Pastel orange shades for strict column
if score >= 90:
return (195, 160, 115) # light sandy peach
if score >= 70:
return (200, 148, 100) # warm apricot
return (195, 125, 95) # soft coral
def fmt_score(score: float) -> str:
"""Format score with one decimal place, dropping .0 for integers."""
if score == int(score):
return str(int(score))
return f"{score:.1f}"
def scale(value: int) -> int:
"""Scale value by retina factor."""
return value * SCALE
def load_font(size: int, *, serif: bool = False, bold: bool = False, mono: bool = False) -> ImageFont.ImageFont:
"""Load a font with cross-platform fallback."""
size = size * SCALE
candidates = []
if mono:
candidates = [
"/System/Library/Fonts/SFNSMono.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
"/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
"DejaVuSansMono.ttf",
]
elif serif and bold:
candidates = [
"/System/Library/Fonts/Supplemental/Georgia Bold.ttf",
"/System/Library/Fonts/NewYork.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf",
"DejaVuSerif-Bold.ttf",
]
elif serif:
candidates = [
"/System/Library/Fonts/Supplemental/Georgia.ttf",
"/System/Library/Fonts/NewYork.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf",
"DejaVuSerif.ttf",
]
elif bold:
candidates = [
"/System/Library/Fonts/SFCompact.ttf",
"/System/Library/Fonts/HelveticaNeue.ttc",
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
"DejaVuSans-Bold.ttf",
]
else:
candidates = [
"/System/Library/Fonts/SFCompact.ttf",
"/System/Library/Fonts/HelveticaNeue.ttc",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
"DejaVuSans.ttf",
]
for path in candidates:
try:
if os.path.exists(path):
return ImageFont.truetype(path, size)
except OSError:
continue
# Fallback to default font
try:
return ImageFont.load_default()
except:
return ImageFont.truetype("arial.ttf", size)
def draw_left_panel(
draw: ImageDraw.ImageDraw,
main_score: float,
strict_score: float,
project_name: str,
version: str,
*,
lp_left: int,
lp_right: int,
lp_top: int,
lp_bot: int,
) -> None:
"""Draw left panel with title, scores, and project info."""
# Fonts
font_title = load_font(16, bold=True)
font_big = load_font(48, bold=True)
font_version = load_font(11)
font_strict = load_font(12, bold=True)
# Title
title = "CODE HEALTH"
title_bbox = draw.textbbox((0, 0), title, font=font_title)
title_width = title_bbox[2] - title_bbox[0]
title_y = lp_top + scale(8)
center_x = (lp_left + lp_right) // 2
draw.text(
(center_x - title_width / 2, title_y - title_bbox[1]),
title,
fill=TEXT,
font=font_title,
)
# Main score
score_y = title_y + scale(35)
score_text = fmt_score(main_score)
score_bbox = draw.textbbox((0, 0), score_text, font=font_big)
score_width = score_bbox[2] - score_bbox[0]
# Score background
score_bg_y = score_y - scale(5)
score_bg_h = scale(55)
draw.rectangle(
(lp_left + scale(10), score_bg_y, lp_right - scale(10), score_bg_y + score_bg_h),
fill=BG_SCORE,
outline=BORDER,
width=1,
)
draw.text(
(center_x - score_width / 2, score_y - score_bbox[1]),
score_text,
fill=score_color(main_score),
font=font_big,
)
# Strict score
strict_y = score_bg_y + score_bg_h + scale(12)
strict_text = f"Strict: {fmt_score(strict_score)}"
strict_bbox = draw.textbbox((0, 0), strict_text, font=font_strict)
strict_width = strict_bbox[2] - strict_bbox[0]
draw.text(
(center_x - strict_width / 2, strict_y - strict_bbox[1]),
strict_text,
fill=score_color(strict_score, muted=True),
font=font_strict,
)
# Project info
info_y = strict_y + scale(25)
# Project name
name_text = project_name.upper()
name_bbox = draw.textbbox((0, 0), name_text, font=font_version)
name_width = name_bbox[2] - name_bbox[0]
draw.text(
(center_x - name_width / 2, info_y - name_bbox[1]),
name_text,
fill=DIM,
font=font_version,
)
# Version
version_y = info_y + scale(18)
version_text = f"v{version}" if version else "dev"
version_bbox = draw.textbbox((0, 0), version_text, font=font_version)
version_width = version_bbox[2] - version_bbox[0]
draw.text(
(center_x - version_width / 2, version_y - version_bbox[1]),
version_text,
fill=DIM,
font=font_version,
)
def draw_vert_rule_with_ornament(
draw: ImageDraw.ImageDraw,
x: int,
y1: int,
y2: int,
mid_y: int,
color: Tuple[int, int, int],
accent: Tuple[int, int, int],
) -> None:
"""Draw vertical divider with ornament at center."""
# Vertical lines
draw.line([(x, y1), (x, mid_y - scale(8))], fill=color, width=1)
draw.line([(x, mid_y + scale(8)), (x, y2)], fill=color, width=1)
# Ornament circle
ornament_size = scale(6)
ellipse1_coords = (
x - ornament_size // 2, mid_y - ornament_size // 2,
x + ornament_size // 2, mid_y + ornament_size // 2
)
draw.ellipse(ellipse1_coords, outline=accent, width=2)
ellipse2_coords = (
x - ornament_size // 2 + 2, mid_y - ornament_size // 2 + 2,
x + ornament_size // 2 - 2, mid_y + ornament_size // 2 - 2
)
draw.ellipse(ellipse2_coords, outline=color, width=1)
def draw_right_panel(
draw: ImageDraw.ImageDraw,
active_dims: List[Tuple[str, Dict[str, Any]]],
row_h: int,
*,
table_x1: int,
table_x2: int,
table_top: int,
table_bot: int,
) -> None:
"""Draw right panel: two separate dimension tables side by side."""
font_row = load_font(11, mono=True)
font_strict = load_font(9, mono=True)
row_count = len(active_dims)
cols = 2
rows_per_col = (row_count + cols - 1) // cols
table_width = table_x2 - table_x1
grid_gap = scale(8)
grid_width = (table_width - grid_gap) // cols
for col_index in range(cols):
grid_x1 = table_x1 + col_index * (grid_width + grid_gap)
grid_x2 = grid_x1 + grid_width
draw.rounded_rectangle(
(grid_x1, table_top, grid_x2, table_bot),
radius=scale(4),
fill=BG_TABLE,
outline=BORDER,
width=1,
)
name_col_width = scale(120)
value_col_gap = scale(4)
value_col_width = scale(34)
total_content_width = (
name_col_width
+ value_col_gap
+ value_col_width
+ value_col_gap
+ value_col_width
)
block_left = grid_x1 + (grid_width - total_content_width) // 2
name_col_x = block_left
health_col_x = name_col_x + name_col_width + value_col_gap
strict_col_x = health_col_x + value_col_width + value_col_gap + scale(4)
rows_this_col = min(rows_per_col, row_count - col_index * rows_per_col)
content_height = rows_this_col * row_h
content_top = (table_top + table_bot) // 2 - content_height // 2
sample_bbox = draw.textbbox((0, 0), "Xg", font=font_row)
row_text_height = sample_bbox[3] - sample_bbox[1]
row_text_offset = sample_bbox[1]
start_idx = col_index * rows_per_col
for row_index in range(rows_this_col):
dim_idx = start_idx + row_index
if dim_idx >= row_count:
break
name, data = active_dims[dim_idx]
band_top = content_top + row_index * row_h
band_bottom = band_top + row_h
if row_index % 2 == 1:
draw.rectangle(
(grid_x1 + 1, band_top, grid_x2 - 1, band_bottom), fill=BG_ROW_ALT
)
text_y = band_top + (row_h - row_text_height) // 2 - row_text_offset + scale(1)
score = data.get("score", 100)
strict = data.get("strict", score)
max_name_width = name_col_width - scale(2)
while (
name
and draw.textlength(name + "\u2026", font=font_row) > max_name_width
):
name = name[:-1]
if draw.textlength(name, font=font_row) > max_name_width:
name = name.rstrip() + "\u2026"
draw.text((name_col_x, text_y), name, fill=TEXT, font=font_row)
draw.text(
(health_col_x, text_y),
f"{fmt_score(score)}%",
fill=score_color(score),
font=font_row,
)
strict_text = f"{fmt_score(strict)}%"
strict_bbox = draw.textbbox((0, 0), strict_text, font=font_strict)
strict_text_height = strict_bbox[3] - strict_bbox[1]
strict_y = band_top + (row_h - strict_text_height) // 2 - strict_bbox[1]
draw.text(
(strict_col_x, strict_y),
strict_text,
fill=score_color(strict, muted=True),
font=font_strict,
)
def generate_scorecard(data: ScorecardData, output_path: str | Path) -> Path:
"""Render a landscape scorecard PNG from scorecard data. Returns output path."""
output_path = Path(output_path)
# Layout — landscape (wide), dimensions first
row_count = len(data.dimensions)
row_h = scale(20)
width = scale(780)
divider_x = scale(260)
frame_inset = scale(5)
cols = 2
rows_per_col = (row_count + cols - 1) // cols
table_content_h = scale(14) + scale(4) + scale(6) + rows_per_col * row_h
content_h = max(table_content_h + scale(28), scale(150))
height = scale(12) + content_h
# Create image
img = Image.new("RGB", (width, height), BG)
draw = ImageDraw.Draw(img)
# Double frame
draw.rectangle((0, 0, width - 1, height - 1), outline=FRAME, width=scale(2))
draw.rectangle(
(frame_inset, frame_inset, width - frame_inset - 1, height - frame_inset - 1),
outline=BORDER,
width=1,
)
content_top = frame_inset + scale(1)
content_bot = height - frame_inset - scale(1)
content_mid_y = (content_top + content_bot) // 2
# Left panel: title + score + project name
draw_left_panel(
draw,
data.main_score,
data.strict_score,
data.project_name,
data.version,
lp_left=frame_inset + scale(11),
lp_right=divider_x - scale(11),
lp_top=content_top + scale(4),
lp_bot=content_bot - scale(4),
)
# Vertical divider with ornament
draw_vert_rule_with_ornament(
draw,
divider_x,
content_top + scale(12),
content_bot - scale(12),
content_mid_y,
BORDER,
ACCENT,
)
# Right panel: dimension table
draw_right_panel(
draw,
data.dimensions,
row_h,
table_x1=divider_x + scale(11),
table_x2=width - frame_inset - scale(11),
table_top=content_top + scale(4),
table_bot=content_bot - scale(4),
)
# Save image
output_path.parent.mkdir(parents=True, exist_ok=True)
img.save(str(output_path), "PNG", optimize=True)
return output_path
def load_devour_data(json_path: str) -> ScorecardData:
"""Load Devour scan results and convert to scorecard format."""
with open(json_path, 'r') as f:
data = json.load(f)
# Extract findings
findings = data.get('findings', [])
# Calculate scores
total_score = sum(f.get('score', 0) * int(f.get('severity', 1)) for f in findings)
strict_score = total_score # Simplified - would use strict scoring logic
# Convert to percentage (inverted)
main_score = max(0, 100 - (total_score / 1000 * 100))
strict_score_pct = max(0, 100 - (strict_score / 1000 * 100))
# Group by type for dimensions
type_counts = {}
type_scores = {}
for finding in findings:
ftype = finding.get('type', 'unknown')
type_counts[ftype] = type_counts.get(ftype, 0) + 1
type_scores[ftype] = type_scores.get(ftype, 0) + finding.get('score', 0)
# Create dimensions list
dimensions = []
for ftype, count in type_counts.items():
avg_score = 100 - (type_scores[ftype] / max(1, count) / 10 * 100)
dimensions.append((
ftype.replace('_', ' ').title(),
{
'score': max(0, min(100, avg_score)),
'strict': max(0, min(100, avg_score * 0.8)), # Strict is lower
'count': count
}
))
# Sort by score (lowest first)
dimensions.sort(key=lambda x: x[1]['score'])
return ScorecardData(
project_name="Devour",
version="1.0.0",
main_score=main_score,
strict_score=strict_score_pct,
dimensions=dimensions[:8], # Limit to 8 dimensions
)
def main():
"""Main entry point."""
if len(sys.argv) != 3:
print("Usage: python devour_scorecard.py <devour_results.json> <output.png>")
sys.exit(1)
json_path = sys.argv[1]
output_path = sys.argv[2]
if not os.path.exists(json_path):
print(f"Error: Input file {json_path} not found")
sys.exit(1)
try:
# Load and convert data
data = load_devour_data(json_path)
# Generate scorecard
result_path = generate_scorecard(data, output_path)
print(f"Scorecard generated: {result_path}")
except Exception as e:
print(f"Error generating scorecard: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
+5 -2
View File
@@ -44,7 +44,7 @@ func runGet(cmd *cobra.Command, args []string) error {
url, err := constructDocURL(language, keyword) url, err := constructDocURL(language, keyword)
if err != nil { if err != nil {
return err return fmt.Errorf("construct documentation url for %s/%s: %w", language, keyword, err)
} }
sourceType := mapLanguageToType(language) sourceType := mapLanguageToType(language)
@@ -54,7 +54,10 @@ func runGet(cmd *cobra.Command, args []string) error {
fmt.Printf("URL: %s\n", url) fmt.Printf("URL: %s\n", url)
fmt.Printf("Type: %s\n\n", sourceType) fmt.Printf("Type: %s\n\n", sourceType)
return runScrape(cmd, []string{url}) if err := runScrape(cmd, []string{url}); err != nil {
return fmt.Errorf("run scrape for get command: %w", err)
}
return nil
} }
func constructDocURL(language, keyword string) (string, error) { func constructDocURL(language, keyword string) (string, error) {
+1 -1
View File
@@ -49,7 +49,7 @@ func runPush(cmd *cobra.Command, args []string) error {
cfg, err := loadAppConfig() cfg, err := loadAppConfig()
if err != nil { if err != nil {
return err return fmt.Errorf("load app config for push command: %w", err)
} }
server := strings.TrimSpace(pushServer) server := strings.TrimSpace(pushServer)
+11 -725
View File
@@ -1,734 +1,20 @@
package cmd package cmd
import ( import "github.com/spf13/cobra"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/spf13/cobra"
"github.com/yourorg/devour/internal/quality"
"github.com/yourorg/devour/internal/quality/detectors"
"github.com/yourorg/devour/internal/quality/plugins"
"github.com/yourorg/devour/internal/quality/plugins/go/fixers"
"github.com/yourorg/devour/internal/quality/review"
_ "github.com/yourorg/devour/internal/quality/plugins/go"
)
// qualityCmd represents the quality command
var qualityCmd = &cobra.Command{ var qualityCmd = &cobra.Command{
Use: "quality", Use: "quality [desloppify-args...]",
Short: "Code quality analysis and technical debt tracking", Short: "Code quality workflows powered by desloppify",
Long: `Analyze code quality issues like complexity, duplication, naming inconsistencies, DisableFlagParsing: true,
and more. Supports multiple languages including Go, Python, TypeScript, Java, and Rust. RunE: func(cmd *cobra.Command, args []string) error {
forward := args
Examples: if len(forward) == 0 {
devour quality scan ./src # Scan current directory forward = []string{"--help"}
devour quality scan --lang go ./src # Scan with explicit language }
devour quality status # Show current status return runDesloppifyFromCommand(cmd, forward, true)
devour quality next # Show next priority issue },
devour quality resolve fixed 123 --note "Refactored" # Mark issue as fixed`,
}
// scanCmd represents the scan subcommand
var scanCmd = &cobra.Command{
Use: "scan [path]",
Short: "Run code quality analysis",
Long: `Scan the given path for code quality issues. Automatically detects language
unless specified with --lang flag.
The scan will detect:
- Complexity issues (nested loops, excessive function calls)
- Code duplication and near-duplicates
- Naming inconsistencies across directories
- Large files and god classes
- Orphaned code and unused exports`,
RunE: runQualityScan,
}
// qualityStatusCmd represents the quality status subcommand
var qualityStatusCmd = &cobra.Command{
Use: "status",
Short: "Show code quality status and scorecard",
Long: `Display the current code quality status including:
- Overall health score and grade
- Findings broken down by type and severity
- Progress metrics and next steps`,
RunE: runQualityStatus,
}
// nextCmd represents the next subcommand
var nextCmd = &cobra.Command{
Use: "next",
Short: "Show the next highest priority issue to fix",
Long: `Display the next highest priority finding based on severity and score.
This helps you focus on the most impactful improvements first.`,
RunE: runQualityNext,
}
// resolveCmd represents the resolve subcommand
var resolveCmd = &cobra.Command{
Use: "resolve <status> <id>",
Short: "Mark a finding as resolved",
Long: `Mark a finding with a specific status:
- fixed: Issue has been resolved
- wontfix: Issue won't be fixed (valid reason required)
- false_positive: Finding is incorrect
- ignore: Temporarily ignore the finding
Examples:
devour quality resolve fixed abc123 --note "Refactored complex function"
devour quality resolve wontfix def456 --note "Legacy code, planned replacement"`,
RunE: runQualityResolve,
}
// Quality flags
var (
qualityPath string
qualityLanguage string
qualityExclude []string
qualityThreshold int
qualityMinLOC int
qualityTargetScore int
qualityFormat string
qualityResetSubjective bool
qualityNoBadge bool
qualityBadgePath string
)
var explain bool
var tier int
var resolveNote string
var attest string
var statusNarrative bool
var fixDryRun bool
var fixAll bool
var reviewPrepare bool
var reviewImport string
var fixCmd = &cobra.Command{
Use: "fix [id]",
Short: "Auto-fix code quality issues",
Long: `Automatically fix T1 (auto-fixable) issues.
Examples:
devour quality fix unused_import::file.go::fmt # Fix specific issue
devour quality fix --all --dry-run # Preview all fixes
devour quality fix --all # Fix all auto-fixable issues`,
RunE: runQualityFix,
}
var reviewCmd = &cobra.Command{
Use: "review",
Short: "Generate or import AI review packets",
Long: `Generate a review packet for AI analysis, or import responses.
Examples:
devour quality review --prepare # Generate review packet
devour quality review --import responses.json # Import AI responses`,
RunE: runQualityReview,
} }
func init() { func init() {
rootCmd.AddCommand(qualityCmd) rootCmd.AddCommand(qualityCmd)
qualityCmd.AddCommand(scanCmd, qualityStatusCmd, nextCmd, resolveCmd, fixCmd, reviewCmd)
// Scan flags
scanCmd.Flags().StringVar(&qualityPath, "path", ".", "Path to scan")
scanCmd.Flags().StringVar(&qualityLanguage, "lang", "", "Language (auto-detected if not specified)")
scanCmd.Flags().StringSliceVar(&qualityExclude, "exclude", []string{}, "Exclude patterns")
scanCmd.Flags().IntVar(&qualityThreshold, "threshold", 15, "Minimum score to flag an issue")
scanCmd.Flags().IntVar(&qualityMinLOC, "min-loc", 50, "Minimum lines of code to analyze")
scanCmd.Flags().IntVar(&qualityTargetScore, "target-score", 95, "Target health score")
scanCmd.Flags().StringVar(&qualityFormat, "format", "text", "Output format (text, json, strict)")
scanCmd.Flags().BoolVar(&qualityResetSubjective, "reset-subjective", false, "Reset subjective baseline")
scanCmd.Flags().BoolVar(&qualityNoBadge, "no-badge", false, "Skip badge generation")
scanCmd.Flags().StringVar(&qualityBadgePath, "badge-path", "scorecard.png", "Badge output path")
// Status flags
qualityStatusCmd.Flags().StringVar(&qualityFormat, "format", "text", "Output format (text, json, strict)")
qualityStatusCmd.Flags().BoolVar(&statusNarrative, "narrative", false, "Include narrative analysis")
// Next flags
nextCmd.Flags().BoolVar(&explain, "explain", false, "Show detailed explanation")
nextCmd.Flags().IntVar(&tier, "tier", 0, "Filter by severity tier (1-4)")
// Resolve flags
resolveCmd.Flags().StringVar(&resolveNote, "note", "", "Note explaining the resolution (required)")
resolveCmd.Flags().StringVar(&attest, "attest", "", "Attestation of improvement")
// Fix flags
fixCmd.Flags().BoolVar(&fixDryRun, "dry-run", false, "Show what would be fixed without making changes")
fixCmd.Flags().BoolVar(&fixAll, "all", false, "Fix all auto-fixable issues")
// Review flags
reviewCmd.Flags().BoolVar(&reviewPrepare, "prepare", false, "Generate review packet for AI analysis")
reviewCmd.Flags().StringVar(&reviewImport, "import", "", "Import review responses from file")
}
func runQualityScan(cmd *cobra.Command, args []string) error {
path := qualityPath
if len(args) > 0 {
path = args[0]
}
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("path does not exist: %s", path)
}
config := &quality.Config{
Path: path,
Language: qualityLanguage,
Exclude: qualityExclude,
Threshold: qualityThreshold,
MinLOC: qualityMinLOC,
TargetScore: qualityTargetScore,
ResetSubjective: qualityResetSubjective,
NoBadge: qualityNoBadge,
BadgePath: qualityBadgePath,
}
scanner := quality.NewScanner(config)
finder := quality.NewDefaultFileFinder()
scanner.SetFileFinder(finder)
lang := qualityLanguage
if lang == "" {
lang = quality.DetectLanguage(path)
fmt.Printf("Auto-detected language: %s\n", lang)
}
plugin, ok := plugins.Get(lang)
if ok {
fmt.Printf("Using %s plugin with AST analysis\n", lang)
for _, detector := range plugin.CreateDetectors(finder) {
scanner.RegisterDetector(detector)
}
} else {
scanner.RegisterDetector(detectors.NewComplexityDetector(finder))
scanner.RegisterDetector(detectors.NewDuplicationDetector(finder))
scanner.RegisterDetector(detectors.NewNamingDetector(finder))
}
ctx := context.Background()
result, err := scanner.Scan(ctx)
if err != nil {
return fmt.Errorf("scan failed: %w", err)
}
result.Findings = quality.AttachDocsEvidence(lang, result.Findings)
return outputScanResult(result, qualityFormat)
}
func runQualityStatus(cmd *cobra.Command, args []string) error {
// Load previous scan results
dataDir := filepath.Join(".", "devour_data", "quality")
statusFile := filepath.Join(dataDir, "status.json")
var findings []quality.Finding
var lastScan time.Time
if data, err := os.ReadFile(statusFile); err == nil {
var status struct {
Findings []quality.Finding `json:"findings"`
Timestamp time.Time `json:"timestamp"`
}
if err := json.Unmarshal(data, &status); err == nil {
findings = status.Findings
lastScan = status.Timestamp
}
}
if len(findings) == 0 {
fmt.Println("No previous scan results found. Run 'devour quality scan' first.")
return nil
}
// Generate scorecard
scorer := quality.NewScorer(qualityTargetScore)
scorecard := scorer.GenerateScorecard(findings, lastScan)
// Output based on format
switch qualityFormat {
case "json":
return json.NewEncoder(os.Stdout).Encode(scorecard)
case "strict":
fmt.Println(scorer.FormatStrictScorecard(findings, lastScan))
printQualityEvidenceSummary(findings)
return nil
default:
fmt.Println(scorer.FormatScorecard(scorecard))
printQualityEvidenceSummary(findings)
return nil
}
}
func runQualityNext(cmd *cobra.Command, args []string) error {
// Load previous scan results
dataDir := filepath.Join(".", "devour_data", "quality")
statusFile := filepath.Join(dataDir, "status.json")
var findings []quality.Finding
if data, err := os.ReadFile(statusFile); err == nil {
var status struct {
Findings []quality.Finding `json:"findings"`
}
if err := json.Unmarshal(data, &status); err == nil {
findings = status.Findings
}
}
if len(findings) == 0 {
fmt.Println("No findings found. Run 'devour quality scan' first.")
return nil
}
// Get next priority finding
scorer := quality.NewScorer(qualityTargetScore)
next := scorer.GetNextPriority(findings)
if next == nil {
fmt.Println("🎉 No open issues to fix!")
return nil
}
// Filter by tier if specified
if tier > 0 {
if int(next.Severity) != tier {
// Find next in specified tier
for _, finding := range findings {
if finding.Status == quality.StatusOpen && int(finding.Severity) == tier {
next = &finding
break
}
}
if next == nil || int(next.Severity) != tier {
fmt.Printf("No open issues found in tier %d.\n", tier)
return nil
}
}
}
// Display finding
fmt.Printf("Next Priority Issue (T%d)\n", int(next.Severity))
fmt.Println("=======================================")
fmt.Printf("File: %s:%d\n", next.File, next.Line)
fmt.Printf("Title: %s\n", next.Title)
fmt.Printf("Score: %d\n", next.Score)
fmt.Printf("ID: %s\n", next.ID)
fmt.Printf("\nDescription:\n%s\n", next.Description)
if next.Metadata != nil {
if urls := strings.TrimSpace(next.Metadata["docs_evidence_urls"]); urls != "" {
fmt.Printf("\nEvidence Docs:\n%s\n", urls)
}
if rationale := strings.TrimSpace(next.Metadata["docs_evidence_rationale"]); rationale != "" {
fmt.Printf("\nRationale:\n%s\n", rationale)
}
if confidence := strings.TrimSpace(next.Metadata["docs_evidence_confidence"]); confidence != "" {
fmt.Printf("Evidence confidence: %s\n", confidence)
}
}
if explain {
fmt.Printf("\nExplanation:\n")
fmt.Printf("This is a T%d severity issue. ", int(next.Severity))
switch next.Severity {
case quality.SeverityT1:
fmt.Println("T1 issues are typically auto-fixable like unused imports or debug logs.")
case quality.SeverityT2:
fmt.Println("T2 issues require quick manual fixes like unused variables or dead exports.")
case quality.SeverityT3:
fmt.Println("T3 issues need judgment calls like near-duplicates or single-use abstractions.")
case quality.SeverityT4:
fmt.Println("T4 issues require major refactoring like god components or mixed concerns.")
}
fmt.Printf("\nTo fix: devour quality resolve fixed %s --note \"Describe what you fixed\"\n", next.ID)
}
return nil
}
func runQualityResolve(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
return fmt.Errorf("usage: devour quality resolve <status> <id>")
}
status := quality.Status(args[0])
id := args[1]
// Validate status
validStatuses := map[quality.Status]bool{
quality.StatusFixed: true,
quality.StatusWontfix: true,
quality.StatusFalsePositive: true,
quality.StatusIgnored: true,
}
if !validStatuses[status] {
return fmt.Errorf("invalid status: %s", status)
}
if resolveNote == "" {
return fmt.Errorf("--note is required when resolving findings")
}
// Load current findings
dataDir := filepath.Join(".", "devour_data", "quality")
statusFile := filepath.Join(dataDir, "status.json")
var findings []quality.Finding
if data, err := os.ReadFile(statusFile); err == nil {
var statusData struct {
Findings []quality.Finding `json:"findings"`
}
if err := json.Unmarshal(data, &statusData); err == nil {
findings = statusData.Findings
}
}
// Find and update the finding
found := false
for i, finding := range findings {
if finding.ID == id {
findings[i].Status = status
findings[i].UpdatedAt = time.Now()
if finding.Metadata == nil {
findings[i].Metadata = make(map[string]string)
}
findings[i].Metadata["resolution_note"] = resolveNote
if attest != "" {
findings[i].Metadata["attestation"] = attest
}
found = true
break
}
}
if !found {
return fmt.Errorf("finding not found: %s", id)
}
// Save updated findings
if err := os.MkdirAll(dataDir, 0755); err != nil {
return fmt.Errorf("failed to create data directory: %w", err)
}
statusData := struct {
Findings []quality.Finding `json:"findings"`
Timestamp time.Time `json:"timestamp"`
}{
Findings: findings,
Timestamp: time.Now(),
}
data, err := json.MarshalIndent(statusData, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal status: %w", err)
}
if err := os.WriteFile(statusFile, data, 0644); err != nil {
return fmt.Errorf("failed to save status: %w", err)
}
fmt.Printf("Resolved: %s as %s\n", id, status)
if resolveNote != "" {
fmt.Printf("Note: %s\n", resolveNote)
}
return nil
}
func outputScanResult(result *quality.ScanResult, format string) error {
// Save results to data directory
dataDir := filepath.Join(".", "devour_data", "quality")
if err := os.MkdirAll(dataDir, 0755); err != nil {
return fmt.Errorf("failed to create data directory: %w", err)
}
statusFile := filepath.Join(dataDir, "status.json")
statusData := struct {
Findings []quality.Finding `json:"findings"`
Timestamp time.Time `json:"timestamp"`
}{
Findings: result.Findings,
Timestamp: result.Timestamp,
}
data, err := json.MarshalIndent(statusData, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal results: %w", err)
}
if err := os.WriteFile(statusFile, data, 0644); err != nil {
return fmt.Errorf("failed to save results: %w", err)
}
// Note: Scorecard generation is now handled by the dedicated 'devour scorecard' command
if !qualityNoBadge && qualityBadgePath != "" {
fmt.Printf("💡 Use 'devour scorecard' to generate beautiful scorecard banners\n")
}
// Output based on format
switch format {
case "json":
return json.NewEncoder(os.Stdout).Encode(result)
default:
return formatScanResultText(result)
}
}
func formatScanResultText(result *quality.ScanResult) error {
fmt.Println("Code Quality Scan Results")
fmt.Println("=======================================")
fmt.Printf("Files checked: %d\n", result.FilesChecked)
fmt.Printf("Duration: %s\n", result.Duration)
fmt.Printf("Score: %d (strict: %d)\n", result.Score, result.StrictScore)
fmt.Printf("Findings: %d\n\n", len(result.Findings))
if len(result.Findings) == 0 {
fmt.Println("No code quality issues found.")
return nil
}
bySeverity := make(map[quality.Severity][]quality.Finding)
for _, finding := range result.Findings {
bySeverity[finding.Severity] = append(bySeverity[finding.Severity], finding)
}
tiers := []quality.Severity{quality.SeverityT4, quality.SeverityT3, quality.SeverityT2, quality.SeverityT1}
tierNames := map[quality.Severity]string{
quality.SeverityT1: "T1 (Auto-fixable)",
quality.SeverityT2: "T2 (Quick manual)",
quality.SeverityT3: "T3 (Needs judgment)",
quality.SeverityT4: "T4 (Major refactor)",
}
for _, severity := range tiers {
findings := bySeverity[severity]
if len(findings) == 0 {
continue
}
fmt.Printf("[%s] %d issues\n", tierNames[severity], len(findings))
for _, finding := range findings {
fmt.Printf(" - %s:%d - %s (score: %d)\n",
filepath.Base(finding.File), finding.Line, finding.Title, finding.Score)
}
fmt.Println()
}
fmt.Println("Run 'devour quality next' to see the highest priority issue to fix.")
fmt.Println("Run 'devour quality status' for detailed scorecard.")
return nil
}
func runQualityFix(cmd *cobra.Command, args []string) error {
dataDir := filepath.Join(".", "devour_data", "quality")
statusFile := filepath.Join(dataDir, "status.json")
var findings []quality.Finding
if data, err := os.ReadFile(statusFile); err == nil {
var status struct {
Findings []quality.Finding `json:"findings"`
}
if err := json.Unmarshal(data, &status); err == nil {
findings = status.Findings
}
}
if len(findings) == 0 {
fmt.Println("No findings found. Run 'devour quality scan' first.")
return nil
}
availableFixers := []plugins.Fixer{
fixers.NewUnusedImportFixer(),
fixers.NewFormattingFixer(),
}
ctx := context.Background()
fixed := 0
var errors []string
if fixAll {
for _, finding := range findings {
if finding.Status != quality.StatusOpen || finding.Severity != quality.SeverityT1 {
continue
}
for _, fixer := range availableFixers {
if fixer.CanFix(finding) {
result, err := fixer.Fix(ctx, finding, fixDryRun)
if err != nil {
errors = append(errors, fmt.Sprintf("%s: %v", finding.ID, err))
continue
}
if result.Success {
fixed++
fmt.Printf("[OK] %s\n", result.Message)
}
break
}
}
}
} else {
if len(args) < 1 {
return fmt.Errorf("specify a finding ID or use --all")
}
targetID := args[0]
var target *quality.Finding
for i := range findings {
if findings[i].ID == targetID {
target = &findings[i]
break
}
}
if target == nil {
return fmt.Errorf("finding not found: %s", targetID)
}
for _, fixer := range availableFixers {
if fixer.CanFix(*target) {
result, err := fixer.Fix(ctx, *target, fixDryRun)
if err != nil {
return fmt.Errorf("fix failed: %w", err)
}
fmt.Printf("[OK] %s\n", result.Message)
fixed = 1
break
}
}
}
if fixDryRun {
fmt.Printf("\nDry run complete. %d issues would be fixed.\n", fixed)
} else {
fmt.Printf("\nFixed %d issues.\n", fixed)
}
if len(errors) > 0 {
fmt.Printf("\nErrors:\n")
for _, e := range errors {
fmt.Printf(" • %s\n", e)
}
}
return nil
}
func runQualityReview(cmd *cobra.Command, args []string) error {
dataDir := filepath.Join(".", "devour_data")
if reviewPrepare {
return prepareReviewPacket(dataDir)
}
if reviewImport != "" {
return importReviewResponses(dataDir, reviewImport)
}
return fmt.Errorf("use --prepare to generate a review packet or --import <file> to import responses")
}
func prepareReviewPacket(dataDir string) error {
statusFile := filepath.Join(dataDir, "quality", "status.json")
var findings []quality.Finding
var lastScan time.Time
if data, err := os.ReadFile(statusFile); err == nil {
var status struct {
Findings []quality.Finding `json:"findings"`
Timestamp time.Time `json:"timestamp"`
}
if err := json.Unmarshal(data, &status); err == nil {
findings = status.Findings
lastScan = status.Timestamp
}
}
scorer := quality.NewScorer(qualityTargetScore)
scorecard := scorer.GenerateScorecard(findings, lastScan)
gen := review.NewPacketGenerator(dataDir)
packet, err := gen.Generate(findings, scorecard, "go")
if err != nil {
return fmt.Errorf("failed to generate review packet: %w", err)
}
filename := fmt.Sprintf("review-%s.json", time.Now().Format("20060102-150405"))
if err := gen.Save(packet, filename); err != nil {
return fmt.Errorf("failed to save review packet: %w", err)
}
fmt.Printf("Review packet generated: %s/review/%s\n", dataDir, filename)
fmt.Printf("Findings to review: %d\n", len(packet.Findings))
fmt.Printf("Questions: %d\n", len(packet.Questions))
return nil
}
func importReviewResponses(dataDir string, filename string) error {
gen := review.NewPacketGenerator(dataDir)
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("failed to read responses file: %w", err)
}
var responses map[string]string
var respData struct {
Responses map[string]string `json:"responses"`
}
if err := json.Unmarshal(data, &respData); err == nil {
responses = respData.Responses
} else {
var simpleResponses map[string]string
if err := json.Unmarshal(data, &simpleResponses); err != nil {
return fmt.Errorf("failed to parse responses: %w", err)
}
responses = simpleResponses
}
if err := gen.ImportReview(filepath.Base(filename), responses); err != nil {
return fmt.Errorf("failed to import responses: %w", err)
}
fmt.Printf("Imported %d review responses\n", len(responses))
return nil
}
func printQualityEvidenceSummary(findings []quality.Finding) {
totalWithEvidence := 0
for _, f := range findings {
if f.Metadata != nil && strings.TrimSpace(f.Metadata["docs_evidence_urls"]) != "" {
totalWithEvidence++
}
}
if totalWithEvidence == 0 {
return
}
fmt.Printf("\nEvidence-linked findings: %d/%d\n", totalWithEvidence, len(findings))
for _, f := range findings {
if f.Metadata == nil {
continue
}
urls := strings.TrimSpace(f.Metadata["docs_evidence_urls"])
if urls == "" {
continue
}
fmt.Printf(" • %s:%d - %s\n %s\n", filepath.Base(f.File), f.Line, f.Title, urls)
break
}
} }
+27
View File
@@ -0,0 +1,27 @@
package cmd
import "github.com/spf13/cobra"
var reviewCmd = &cobra.Command{
Use: "review [desloppify-review-args...]",
Short: "Run holistic review via desloppify",
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
forward := []string{"review"}
if len(args) == 0 {
forward = append(forward,
"--run-batches",
"--runner", "codex",
"--parallel",
"--scan-after-import",
)
} else {
forward = append(forward, args...)
}
return runDesloppifyFromCommand(cmd, forward, true)
},
}
func init() {
rootCmd.AddCommand(reviewCmd)
}
+23 -72
View File
@@ -1,90 +1,41 @@
package cmd package cmd
import ( import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var ( var (
scorecardCompact bool scorecardPath string
scorecardDetailed bool scorecardBadgePath string
scorecardOutput string scorecardResetSubjective bool
scorecardSkipSlow bool
) )
var scorecardCmd = &cobra.Command{ var scorecardCmd = &cobra.Command{
Use: "scorecard", Use: "scorecard",
Short: "Generate Devour quality scorecards", Short: "Generate a scorecard badge via desloppify",
Long: `Generate beautiful dark-themed scorecards showing code quality metrics. Long: `Generate the quality scorecard badge by delegating to desloppify.
Creates both compact and detailed PNG banners with:
- Modern dark theme design
- Glass morphism effects
- Devour brand colors
- Professional typography
This runs a scan and writes badge output to --badge-path.
Examples: Examples:
devour scorecard # Generate both compact and detailed devour scorecard
devour scorecard --compact # Generate only compact banner devour scorecard --path . --badge-path scorecard.png
devour scorecard --detailed # Generate only detailed banner devour scorecard --reset-subjective`,
devour scorecard --output custom # Custom output filename`, RunE: func(cmd *cobra.Command, args []string) error {
Run: func(cmd *cobra.Command, args []string) { forward := []string{"scan", "--path", scorecardPath, "--badge-path", scorecardBadgePath}
generateScorecards() if scorecardResetSubjective {
forward = append(forward, "--reset-subjective")
}
if scorecardSkipSlow {
forward = append(forward, "--skip-slow")
}
return runDesloppifyFromCommand(cmd, forward, true)
}, },
} }
func init() { func init() {
scorecardCmd.Flags().BoolVar(&scorecardCompact, "compact", false, "Generate compact banner only") scorecardCmd.Flags().StringVar(&scorecardPath, "path", ".", "Path to scan")
scorecardCmd.Flags().BoolVar(&scorecardDetailed, "detailed", false, "Generate detailed banner only") scorecardCmd.Flags().StringVar(&scorecardBadgePath, "badge-path", "scorecard.png", "Badge output path")
scorecardCmd.Flags().StringVarP(&scorecardOutput, "output", "o", "lighthouse_scorecard", "Output filename prefix") scorecardCmd.Flags().BoolVar(&scorecardResetSubjective, "reset-subjective", false, "Reset subjective scores before scan")
} scorecardCmd.Flags().BoolVar(&scorecardSkipSlow, "skip-slow", false, "Skip slow detectors")
func generateScorecards() {
// Get the current working directory (project root)
workingDir, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting working directory: %v\n", err)
os.Exit(1)
}
// Path to the Python script relative to working directory
pythonScriptPath := filepath.Join(workingDir, "cmd", "banner_generator", "main.py")
// Check if Python script exists
if _, err := os.Stat(pythonScriptPath); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "Error: Python scorecard generator not found at %s\n", pythonScriptPath)
os.Exit(1)
}
// Build Python command arguments
args := []string{pythonScriptPath}
if scorecardCompact {
args = append(args, "--compact")
} else if scorecardDetailed {
args = append(args, "--detailed")
}
if scorecardOutput != "lighthouse_scorecard" {
args = append(args, "--output", scorecardOutput)
}
// Execute Python script
fmt.Println("🎨 Generating Devour Scorecards...")
fmt.Printf("📂 Using generator: %s\n", pythonScriptPath)
cmd := exec.Command("python3", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = workingDir
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error generating scorecards: %v\n", err)
os.Exit(1)
}
fmt.Println("✅ Scorecard generation complete!")
} }
-519
View File
@@ -1,519 +0,0 @@
#!/usr/bin/env python3
"""
Devour Scorecard Generator - 1:1 recreation of desloppify scorecard style.
Generates visual health summary PNG with the exact same data structure and visual design.
"""
from __future__ import annotations
import json
import logging
import os
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Tuple, Any, Optional
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
print("Error: PIL/Pillow required. Install with: pip install Pillow")
sys.exit(1)
logger = logging.getLogger(__name__)
# Visual constants matching desloppify theme
SCALE = 2 # 2x for retina/high-DPI
BG = (248, 248, 246) # Light gray background
FRAME = (222, 222, 220) # Border frame
BORDER = (200, 200, 198) # Inner border
ACCENT = (88, 166, 255) # Blue accent
TEXT = (40, 44, 52) # Dark text
DIM = (140, 140, 140) # Dimmed text
BG_SCORE = (255, 255, 255) # Score background
BG_TABLE = (255, 255, 255) # Table background
BG_ROW_ALT = (250, 250, 248) # Alternating row background
@dataclass
class ScorecardData:
"""Data structure matching desloppify scorecard format."""
project_name: str
version: str
main_score: float
strict_score: float
dimensions: List[Tuple[str, Dict[str, Any]]]
def score_color(score: float, *, muted: bool = False) -> Tuple[int, int, int]:
"""Color-code a score: deep sage >= 90, mustard 70-90, dusty rose < 70.
muted=True returns a desaturated variant for secondary display (strict column).
"""
if score >= 90:
base = (68, 120, 68) # deep sage
elif score >= 70:
base = (120, 140, 72) # olive green
else:
base = (145, 155, 80) # yellow-green
if not muted:
return base
# Pastel orange shades for strict column
if score >= 90:
return (195, 160, 115) # light sandy peach
if score >= 70:
return (200, 148, 100) # warm apricot
return (195, 125, 95) # soft coral
def fmt_score(score: float) -> str:
"""Format score with one decimal place, dropping .0 for integers."""
if score == int(score):
return str(int(score))
return f"{score:.1f}"
def scale(value: int) -> int:
"""Scale value by retina factor."""
return value * SCALE
def load_font(size: int, *, serif: bool = False, bold: bool = False, mono: bool = False) -> ImageFont.ImageFont:
"""Load a font with cross-platform fallback."""
size = size * SCALE
candidates = []
if mono:
candidates = [
"/System/Library/Fonts/SFNSMono.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
"/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
"DejaVuSansMono.ttf",
]
elif serif and bold:
candidates = [
"/System/Library/Fonts/Supplemental/Georgia Bold.ttf",
"/System/Library/Fonts/NewYork.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf",
"DejaVuSerif-Bold.ttf",
]
elif serif:
candidates = [
"/System/Library/Fonts/Supplemental/Georgia.ttf",
"/System/Library/Fonts/NewYork.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf",
"DejaVuSerif.ttf",
]
elif bold:
candidates = [
"/System/Library/Fonts/SFCompact.ttf",
"/System/Library/Fonts/HelveticaNeue.ttc",
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
"DejaVuSans-Bold.ttf",
]
else:
candidates = [
"/System/Library/Fonts/SFCompact.ttf",
"/System/Library/Fonts/HelveticaNeue.ttc",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
"DejaVuSans.ttf",
]
for path in candidates:
try:
if os.path.exists(path):
return ImageFont.truetype(path, size)
except OSError:
continue
# Fallback to default font
try:
return ImageFont.load_default()
except:
return ImageFont.truetype("arial.ttf", size)
def draw_left_panel(
draw: ImageDraw.ImageDraw,
main_score: float,
strict_score: float,
project_name: str,
version: str,
*,
lp_left: int,
lp_right: int,
lp_top: int,
lp_bot: int,
) -> None:
"""Draw left panel with title, scores, and project info."""
# Fonts
font_title = load_font(16, bold=True)
font_big = load_font(48, bold=True)
font_version = load_font(11)
font_strict = load_font(12, bold=True)
# Title
title = "CODE HEALTH"
title_bbox = draw.textbbox((0, 0), title, font=font_title)
title_width = title_bbox[2] - title_bbox[0]
title_y = lp_top + scale(8)
center_x = (lp_left + lp_right) // 2
draw.text(
(center_x - title_width / 2, title_y - title_bbox[1]),
title,
fill=TEXT,
font=font_title,
)
# Main score
score_y = title_y + scale(35)
score_text = fmt_score(main_score)
score_bbox = draw.textbbox((0, 0), score_text, font=font_big)
score_width = score_bbox[2] - score_bbox[0]
# Score background
score_bg_y = score_y - scale(5)
score_bg_h = scale(55)
draw.rectangle(
(lp_left + scale(10), score_bg_y, lp_right - scale(10), score_bg_y + score_bg_h),
fill=BG_SCORE,
outline=BORDER,
width=1,
)
draw.text(
(center_x - score_width / 2, score_y - score_bbox[1]),
score_text,
fill=score_color(main_score),
font=font_big,
)
# Strict score
strict_y = score_bg_y + score_bg_h + scale(12)
strict_text = f"Strict: {fmt_score(strict_score)}"
strict_bbox = draw.textbbox((0, 0), strict_text, font=font_strict)
strict_width = strict_bbox[2] - strict_bbox[0]
draw.text(
(center_x - strict_width / 2, strict_y - strict_bbox[1]),
strict_text,
fill=score_color(strict_score, muted=True),
font=font_strict,
)
# Project info
info_y = strict_y + scale(25)
# Project name
name_text = project_name.upper()
name_bbox = draw.textbbox((0, 0), name_text, font=font_version)
name_width = name_bbox[2] - name_bbox[0]
draw.text(
(center_x - name_width / 2, info_y - name_bbox[1]),
name_text,
fill=DIM,
font=font_version,
)
# Version
version_y = info_y + scale(18)
version_text = f"v{version}" if version else "dev"
version_bbox = draw.textbbox((0, 0), version_text, font=font_version)
version_width = version_bbox[2] - version_bbox[0]
draw.text(
(center_x - version_width / 2, version_y - version_bbox[1]),
version_text,
fill=DIM,
font=font_version,
)
def draw_vert_rule_with_ornament(
draw: ImageDraw.ImageDraw,
x: int,
y1: int,
y2: int,
mid_y: int,
color: Tuple[int, int, int],
accent: Tuple[int, int, int],
) -> None:
"""Draw vertical divider with ornament at center."""
# Vertical lines
draw.line([(x, y1), (x, mid_y - scale(8))], fill=color, width=1)
draw.line([(x, mid_y + scale(8)), (x, y2)], fill=color, width=1)
# Ornament circle
ornament_size = scale(6)
draw.ellipse(
(x - ornament_size // 2, mid_y - ornament_size // 2,
x + ornament_size // 2, mid_y + ornament_size // 2),
outline=accent,
width=2,
)
draw.ellipse(
(x - ornament_size // 2 + 2, mid_y - ornament_size // 2 + 2,
x + ornament_size // 2 - 2, mid_y + ornament_size // 2 - 2),
outline=color,
width=1,
)
def draw_right_panel(
draw: ImageDraw.ImageDraw,
active_dims: List[Tuple[str, Dict[str, Any]]],
row_h: int,
*,
table_x1: int,
table_x2: int,
table_top: int,
table_bot: int,
) -> None:
"""Draw right panel: two separate dimension tables side by side."""
font_row = load_font(11, mono=True)
font_strict = load_font(9, mono=True)
row_count = len(active_dims)
cols = 2
rows_per_col = (row_count + cols - 1) // cols
table_width = table_x2 - table_x1
grid_gap = scale(8)
grid_width = (table_width - grid_gap) // cols
for col_index in range(cols):
grid_x1 = table_x1 + col_index * (grid_width + grid_gap)
grid_x2 = grid_x1 + grid_width
draw.rounded_rectangle(
(grid_x1, table_top, grid_x2, table_bot),
radius=scale(4),
fill=BG_TABLE,
outline=BORDER,
width=1,
)
name_col_width = scale(120)
value_col_gap = scale(4)
value_col_width = scale(34)
total_content_width = (
name_col_width
+ value_col_gap
+ value_col_width
+ value_col_gap
+ value_col_width
)
block_left = grid_x1 + (grid_width - total_content_width) // 2
name_col_x = block_left
health_col_x = name_col_x + name_col_width + value_col_gap
strict_col_x = health_col_x + value_col_width + value_col_gap + scale(4)
rows_this_col = min(rows_per_col, row_count - col_index * rows_per_col)
content_height = rows_this_col * row_h
content_top = (table_top + table_bot) // 2 - content_height // 2
sample_bbox = draw.textbbox((0, 0), "Xg", font=font_row)
row_text_height = sample_bbox[3] - sample_bbox[1]
row_text_offset = sample_bbox[1]
start_idx = col_index * rows_per_col
for row_index in range(rows_this_col):
dim_idx = start_idx + row_index
if dim_idx >= row_count:
break
name, data = active_dims[dim_idx]
band_top = content_top + row_index * row_h
band_bottom = band_top + row_h
if row_index % 2 == 1:
draw.rectangle(
(grid_x1 + 1, band_top, grid_x2 - 1, band_bottom), fill=BG_ROW_ALT
)
text_y = band_top + (row_h - row_text_height) // 2 - row_text_offset + scale(1)
score = data.get("score", 100)
strict = data.get("strict", score)
max_name_width = name_col_width - scale(2)
while (
name
and draw.textlength(name + "\u2026", font=font_row) > max_name_width
):
name = name[:-1]
if draw.textlength(name, font=font_row) > max_name_width:
name = name.rstrip() + "\u2026"
draw.text((name_col_x, text_y), name, fill=TEXT, font=font_row)
draw.text(
(health_col_x, text_y),
f"{fmt_score(score)}%",
fill=score_color(score),
font=font_row,
)
strict_text = f"{fmt_score(strict)}%"
strict_bbox = draw.textbbox((0, 0), strict_text, font=font_strict)
strict_text_height = strict_bbox[3] - strict_bbox[1]
strict_y = band_top + (row_h - strict_text_height) // 2 - strict_bbox[1]
draw.text(
(strict_col_x, strict_y),
strict_text,
fill=score_color(strict, muted=True),
font=font_strict,
)
def generate_scorecard(data: ScorecardData, output_path: str | Path) -> Path:
"""Render a landscape scorecard PNG from scorecard data. Returns output path."""
output_path = Path(output_path)
# Layout — landscape (wide), dimensions first
row_count = len(data.dimensions)
row_h = scale(20)
width = scale(780)
divider_x = scale(260)
frame_inset = scale(5)
cols = 2
rows_per_col = (row_count + cols - 1) // cols
table_content_h = scale(14) + scale(4) + scale(6) + rows_per_col * row_h
content_h = max(table_content_h + scale(28), scale(150))
height = scale(12) + content_h
# Create image
img = Image.new("RGB", (width, height), BG)
draw = ImageDraw.Draw(img)
# Double frame
draw.rectangle((0, 0, width - 1, height - 1), outline=FRAME, width=scale(2))
draw.rectangle(
(frame_inset, frame_inset, width - frame_inset - 1, height - frame_inset - 1),
outline=BORDER,
width=1,
)
content_top = frame_inset + scale(1)
content_bot = height - frame_inset - scale(1)
content_mid_y = (content_top + content_bot) // 2
# Left panel: title + score + project name
draw_left_panel(
draw,
data.main_score,
data.strict_score,
data.project_name,
data.version,
lp_left=frame_inset + scale(11),
lp_right=divider_x - scale(11),
lp_top=content_top + scale(4),
lp_bot=content_bot - scale(4),
)
# Vertical divider with ornament
draw_vert_rule_with_ornament(
draw,
divider_x,
content_top + scale(12),
content_bot - scale(12),
content_mid_y,
BORDER,
ACCENT,
)
# Right panel: dimension table
draw_right_panel(
draw,
data.dimensions,
row_h,
table_x1=divider_x + scale(11),
table_x2=width - frame_inset - scale(11),
table_top=content_top + scale(4),
table_bot=content_bot - scale(4),
)
# Save image
output_path.parent.mkdir(parents=True, exist_ok=True)
img.save(str(output_path), "PNG", optimize=True)
return output_path
def load_devour_data(json_path: str) -> ScorecardData:
"""Load Devour scan results and convert to scorecard format."""
with open(json_path, 'r') as f:
data = json.load(f)
# Extract findings
findings = data.get('findings', [])
# Calculate scores
total_score = sum(f.get('score', 0) * int(f.get('severity', 1)) for f in findings)
strict_score = total_score # Simplified - would use strict scoring logic
# Convert to percentage (inverted)
main_score = max(0, 100 - (total_score / 1000 * 100))
strict_score_pct = max(0, 100 - (strict_score / 1000 * 100))
# Group by type for dimensions
type_counts = {}
type_scores = {}
for finding in findings:
ftype = finding.get('type', 'unknown')
type_counts[ftype] = type_counts.get(ftype, 0) + 1
type_scores[ftype] = type_scores.get(ftype, 0) + finding.get('score', 0)
# Create dimensions list
dimensions = []
for ftype, count in type_counts.items():
avg_score = 100 - (type_scores[ftype] / max(1, count) / 10 * 100)
dimensions.append((
ftype.replace('_', ' ').title(),
{
'score': max(0, min(100, avg_score)),
'strict': max(0, min(100, avg_score * 0.8)), # Strict is lower
'count': count
}
))
# Sort by score (lowest first)
dimensions.sort(key=lambda x: x[1]['score'])
return ScorecardData(
project_name="Devour",
version="1.0.0",
main_score=main_score,
strict_score=strict_score_pct,
dimensions=dimensions[:8], # Limit to 8 dimensions
)
def main():
"""Main entry point."""
if len(sys.argv) != 3:
print("Usage: python devour_scorecard.py <devour_results.json> <output.png>")
sys.exit(1)
json_path = sys.argv[1]
output_path = sys.argv[2]
if not os.path.exists(json_path):
print(f"Error: Input file {json_path} not found")
sys.exit(1)
try:
# Load and convert data
data = load_devour_data(json_path)
# Generate scorecard
result_path = generate_scorecard(data, output_path)
print(f"Scorecard generated: {result_path}")
except Exception as e:
print(f"Error generating scorecard: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
+7 -4
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
@@ -87,7 +88,7 @@ func init() {
func runScrape(cmd *cobra.Command, args []string) error { func runScrape(cmd *cobra.Command, args []string) error {
cfg, err := loadAppConfig() cfg, err := loadAppConfig()
if err != nil { if err != nil {
return err return fmt.Errorf("load app config for scrape command: %w", err)
} }
if scrapeSources != "" { if scrapeSources != "" {
@@ -122,7 +123,7 @@ func runScrape(cmd *cobra.Command, args []string) error {
outputDir := resolveOutputDir(cfg, scrapeOutput) outputDir := resolveOutputDir(cfg, scrapeOutput)
count, err := scrapeOne(cmd, cfg, source, outputDir) count, err := scrapeOne(cmd, cfg, source, outputDir)
if err != nil { if err != nil {
return err return fmt.Errorf("scrape source %q: %w", sourceURL, err)
} }
if cfg.Indexing.Enabled { if cfg.Indexing.Enabled {
@@ -151,7 +152,7 @@ func scrapeFromConfig(cmd *cobra.Command, cfg *appconfig.Config, configPath stri
Sources []appconfig.SourceConfig `yaml:"sources"` Sources []appconfig.SourceConfig `yaml:"sources"`
} }
if wrapErr := yaml.Unmarshal(raw, &wrapped); wrapErr != nil { if wrapErr := yaml.Unmarshal(raw, &wrapped); wrapErr != nil {
return fmt.Errorf("parse sources file: %w", err) return fmt.Errorf("parse sources file: %w", wrapErr)
} }
list = wrapped.Sources list = wrapped.Sources
} }
@@ -167,6 +168,7 @@ func scrapeFromConfig(cmd *cobra.Command, cfg *appconfig.Config, configPath stri
success := 0 success := 0
failures := 0 failures := 0
totalDocs := 0 totalDocs := 0
sourceErrors := make([]error, 0)
for _, srcCfg := range list { for _, srcCfg := range list {
source := sourceFromConfig(srcCfg) source := sourceFromConfig(srcCfg)
if source.Type == "" { if source.Type == "" {
@@ -189,6 +191,7 @@ func scrapeFromConfig(cmd *cobra.Command, cfg *appconfig.Config, configPath stri
if srcErr != nil { if srcErr != nil {
failures++ failures++
fmt.Printf("✗ %s failed: %v\n", source.Name, srcErr) fmt.Printf("✗ %s failed: %v\n", source.Name, srcErr)
sourceErrors = append(sourceErrors, fmt.Errorf("%s: %w", source.Name, srcErr))
continue continue
} }
totalDocs += count totalDocs += count
@@ -204,7 +207,7 @@ func scrapeFromConfig(cmd *cobra.Command, cfg *appconfig.Config, configPath stri
fmt.Printf("\nSummary: %d succeeded, %d failed, %d docs written\n", success, failures, totalDocs) fmt.Printf("\nSummary: %d succeeded, %d failed, %d docs written\n", success, failures, totalDocs)
if failures > 0 { if failures > 0 {
return fmt.Errorf("one or more sources failed") return fmt.Errorf("one or more sources failed: %w", errors.Join(sourceErrors...))
} }
return nil return nil
} }
+35 -18
View File
@@ -42,7 +42,7 @@ func init() {
func runServe(cmd *cobra.Command, args []string) error { func runServe(cmd *cobra.Command, args []string) error {
if _, err := loadAppConfig(); err != nil { if _, err := loadAppConfig(); err != nil {
return err return fmt.Errorf("load app config for server startup: %w", err)
} }
srvCfg := &server.Config{ srvCfg := &server.Config{
@@ -65,14 +65,17 @@ func runServe(cmd *cobra.Command, args []string) error {
} }
srv := server.NewServer(srvCfg) srv := server.NewServer(srvCfg)
return srv.Start(context.Background()) if err := srv.Start(context.Background()); err != nil {
return fmt.Errorf("start rpc server: %w", err)
}
return nil
} }
func handleServeMethod(ctx context.Context, method string, params json.RawMessage) (any, error) { func handleServeMethod(ctx context.Context, method string, params json.RawMessage) (any, error) {
// The method implementation needs full typed config. Load per-call to avoid stale state. // The method implementation needs full typed config. Load per-call to avoid stale state.
loadedCfg, err := loadAppConfig() loadedCfg, err := loadAppConfig()
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("load app config for rpc method %q: %w", method, err)
} }
switch strings.TrimSpace(method) { switch strings.TrimSpace(method) {
@@ -83,23 +86,31 @@ func handleServeMethod(ctx context.Context, method string, params json.RawMessag
Threshold float64 `json:"threshold"` Threshold float64 `json:"threshold"`
} }
if len(params) > 0 { if len(params) > 0 {
_ = json.Unmarshal(params, &req) if err := json.Unmarshal(params, &req); err != nil {
return nil, fmt.Errorf("decode devour_query params: %w", err)
}
} }
engine := search.NewEngine(loadedCfg) engine := search.NewEngine(loadedCfg)
results, stats, err := engine.Search(ctx, req.Query, search.SearchOptions{Limit: req.Limit, Threshold: req.Threshold}) results, stats, err := engine.Search(ctx, req.Query, search.SearchOptions{Limit: req.Limit, Threshold: req.Threshold})
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("run devour_query search: %w", err)
} }
return map[string]any{"query": req.Query, "count": len(results), "results": results, "indexed": stats.Documents}, nil return map[string]any{"query": req.Query, "count": len(results), "results": results, "indexed": stats.Documents}, nil
case "devour_status": case "devour_status":
docsStats, err := projectstate.CollectDocsStats(loadedCfg.Storage.DocsDir) docsStats, err := projectstate.CollectDocsStats(loadedCfg.Storage.DocsDir)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("collect docs stats: %w", err)
}
state, err := projectstate.LoadSourceState(loadedCfg.Storage.MetadataDir)
if err != nil {
return nil, fmt.Errorf("load source state: %w", err)
} }
state, _ := projectstate.LoadSourceState(loadedCfg.Storage.MetadataDir)
engine := search.NewEngine(loadedCfg) engine := search.NewEngine(loadedCfg)
idxStats, _ := engine.EnsureIndexed(ctx) idxStats, err := engine.EnsureIndexed(ctx)
if err != nil {
return nil, fmt.Errorf("ensure search index: %w", err)
}
return map[string]any{ return map[string]any{
"documents": docsStats.DocumentCount, "documents": docsStats.DocumentCount,
"storage_bytes": docsStats.StorageBytes, "storage_bytes": docsStats.StorageBytes,
@@ -122,7 +133,7 @@ func handleServeMethod(ctx context.Context, method string, params json.RawMessag
Exclude []string `json:"exclude"` Exclude []string `json:"exclude"`
} }
if err := json.Unmarshal(params, &req); err != nil { if err := json.Unmarshal(params, &req); err != nil {
return nil, err return nil, fmt.Errorf("decode devour_scrape params: %w", err)
} }
if strings.TrimSpace(req.Source) == "" { if strings.TrimSpace(req.Source) == "" {
return nil, fmt.Errorf("source is required") return nil, fmt.Errorf("source is required")
@@ -148,15 +159,17 @@ func handleServeMethod(ctx context.Context, method string, params json.RawMessag
prevFormat := scrapeFormat prevFormat := scrapeFormat
prevOutput := scrapeOutput prevOutput := scrapeOutput
prevAllowEmpty := scrapeAllowEmpty prevAllowEmpty := scrapeAllowEmpty
defer func() {
scrapeFormat = prevFormat
scrapeOutput = prevOutput
scrapeAllowEmpty = prevAllowEmpty
}()
scrapeFormat = coalesceString(req.Format, "json") scrapeFormat = coalesceString(req.Format, "json")
scrapeOutput = req.Output scrapeOutput = req.Output
scrapeAllowEmpty = false scrapeAllowEmpty = false
count, err := scrapeOne(nil, loadedCfg, source, resolveOutputDir(loadedCfg, req.Output)) count, err := scrapeOne(nil, loadedCfg, source, resolveOutputDir(loadedCfg, req.Output))
scrapeFormat = prevFormat
scrapeOutput = prevOutput
scrapeAllowEmpty = prevAllowEmpty
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("run scrape for %q: %w", req.Source, err)
} }
return map[string]any{"source": req.Source, "type": st, "documents": count}, nil return map[string]any{"source": req.Source, "type": st, "documents": count}, nil
@@ -166,7 +179,7 @@ func handleServeMethod(ctx context.Context, method string, params json.RawMessag
Limit int `json:"limit"` Limit int `json:"limit"`
} }
if err := json.Unmarshal(params, &req); err != nil { if err := json.Unmarshal(params, &req); err != nil {
return nil, err return nil, fmt.Errorf("decode devour_ask params: %w", err)
} }
if strings.TrimSpace(req.Question) == "" { if strings.TrimSpace(req.Question) == "" {
return nil, fmt.Errorf("question is required") return nil, fmt.Errorf("question is required")
@@ -178,7 +191,7 @@ func handleServeMethod(ctx context.Context, method string, params json.RawMessag
engine := search.NewEngine(loadedCfg) engine := search.NewEngine(loadedCfg)
results, _, err := engine.Search(ctx, req.Question, search.SearchOptions{Limit: limit}) results, _, err := engine.Search(ctx, req.Question, search.SearchOptions{Limit: limit})
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("run devour_ask search: %w", err)
} }
summary := "No relevant docs found." summary := "No relevant docs found."
if len(results) > 0 { if len(results) > 0 {
@@ -194,15 +207,19 @@ func handleServeMethod(ctx context.Context, method string, params json.RawMessag
Rebuild bool `json:"rebuild"` Rebuild bool `json:"rebuild"`
} }
if len(params) > 0 { if len(params) > 0 {
_ = json.Unmarshal(params, &req) if err := json.Unmarshal(params, &req); err != nil {
return nil, fmt.Errorf("decode devour_sync params: %w", err)
}
} }
syncForce = req.Force syncForce = req.Force
syncRebuild = req.Rebuild syncRebuild = req.Rebuild
syncSource = req.Source syncSource = req.Source
err := runSync(nil, nil) defer func() {
syncForce, syncRebuild, syncSource = prevForce, prevRebuild, prevSource syncForce, syncRebuild, syncSource = prevForce, prevRebuild, prevSource
}()
err := runSync(nil, nil)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("run devour_sync: %w", err)
} }
return map[string]any{"ok": true}, nil return map[string]any{"ok": true}, nil
+43
View File
@@ -2,9 +2,11 @@
package ai package ai
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"os" "os"
"strings" "strings"
@@ -77,6 +79,8 @@ func (e *APIError) Error() string {
return e.Message return e.Message
} }
const maxHTTPErrorBodyBytes = 2048
// Embed generates embeddings for texts. // Embed generates embeddings for texts.
func (c *OpenAIClient) Embed(ctx context.Context, texts []string) ([][]float32, error) { func (c *OpenAIClient) Embed(ctx context.Context, texts []string) ([][]float32, error) {
if c.config.APIKey == "" { if c.config.APIKey == "" {
@@ -145,6 +149,10 @@ func (c *OpenAIClient) embedBatch(ctx context.Context, model string, texts []str
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return nil, formatHTTPStatusError("embeddings", resp)
}
var embeddingResp EmbeddingResponse var embeddingResp EmbeddingResponse
if err := json.NewDecoder(resp.Body).Decode(&embeddingResp); err != nil { if err := json.NewDecoder(resp.Body).Decode(&embeddingResp); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err) return nil, fmt.Errorf("failed to decode response: %w", err)
@@ -252,6 +260,10 @@ func (c *OpenAIClient) QueryWithContext(ctx context.Context, query string, conte
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return "", formatHTTPStatusError("chat/completions", resp)
}
var chatResp ChatResponse var chatResp ChatResponse
if err := json.NewDecoder(resp.Body).Decode(&chatResp); err != nil { if err := json.NewDecoder(resp.Body).Decode(&chatResp); err != nil {
return "", fmt.Errorf("failed to decode response: %w", err) return "", fmt.Errorf("failed to decode response: %w", err)
@@ -296,3 +308,34 @@ func (c *MockClient) Embed(ctx context.Context, texts []string) ([][]float32, er
func (c *MockClient) QueryWithContext(ctx context.Context, query string, context []string) (string, error) { func (c *MockClient) QueryWithContext(ctx context.Context, query string, context []string) (string, error) {
return "This is a mock response.", nil return "This is a mock response.", nil
} }
func formatHTTPStatusError(endpoint string, resp *http.Response) error {
body, err := io.ReadAll(io.LimitReader(resp.Body, maxHTTPErrorBodyBytes))
if err != nil {
return fmt.Errorf("openai %s returned status %d (%s) and body read failed: %w", endpoint, resp.StatusCode, http.StatusText(resp.StatusCode), err)
}
return fmt.Errorf(
"openai %s returned status %d (%s): %s",
endpoint,
resp.StatusCode,
http.StatusText(resp.StatusCode),
extractHTTPErrorMessage(body),
)
}
func extractHTTPErrorMessage(body []byte) string {
trimmed := bytes.TrimSpace(body)
if len(trimmed) == 0 {
return "<empty body>"
}
var payload struct {
Error *APIError `json:"error"`
}
if err := json.Unmarshal(trimmed, &payload); err == nil && payload.Error != nil && strings.TrimSpace(payload.Error.Message) != "" {
return strings.TrimSpace(payload.Error.Message)
}
return string(trimmed)
}
+3 -3
View File
@@ -204,7 +204,7 @@ func Load(explicitPath string) (*Config, error) {
path, err := findConfigPath(explicitPath) path, err := findConfigPath(explicitPath)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("resolve config path: %w", err)
} }
if path == "" { if path == "" {
cfg.ApplyDefaults() cfg.ApplyDefaults()
@@ -328,7 +328,7 @@ func findConfigPath(explicitPath string) (string, error) {
if strings.TrimSpace(explicitPath) != "" { if strings.TrimSpace(explicitPath) != "" {
p, err := filepath.Abs(explicitPath) p, err := filepath.Abs(explicitPath)
if err != nil { if err != nil {
return "", err return "", fmt.Errorf("resolve absolute config path %q: %w", explicitPath, err)
} }
if _, err := os.Stat(p); err != nil { if _, err := os.Stat(p); err != nil {
return "", fmt.Errorf("config file not found: %s", explicitPath) return "", fmt.Errorf("config file not found: %s", explicitPath)
@@ -361,7 +361,7 @@ func (c *Config) EnsureStorageDirs() error {
continue continue
} }
if err := os.MkdirAll(dir, 0o755); err != nil { if err := os.MkdirAll(dir, 0o755); err != nil {
return err return fmt.Errorf("create storage directory %q: %w", dir, err)
} }
} }
return nil return nil
-438
View File
@@ -1,438 +0,0 @@
package quality
import (
"fmt"
"sort"
)
type NarrativeGenerator struct {
targetScore int
}
func NewNarrativeGenerator(targetScore int) *NarrativeGenerator {
if targetScore <= 0 {
targetScore = 95
}
return &NarrativeGenerator{targetScore: targetScore}
}
func (g *NarrativeGenerator) Generate(findings []Finding, scorecard *Scorecard, history []StateSnapshot) *Narrative {
phase := g.determinePhase(findings, scorecard)
headline := g.generateHeadline(phase, scorecard)
dimensions := g.analyzeDimensions(findings)
actions := g.generateActions(findings, phase)
strategy := g.generateStrategy(findings, dimensions)
tools := g.generateTools(findings)
debt := g.analyzeDebt(findings, scorecard)
strictTarget := g.calculateStrictTarget(scorecard)
reminders := g.generateReminders(findings, history)
riskFlags := g.identifyRisks(findings, history)
return &Narrative{
Phase: phase,
Headline: headline,
Dimensions: dimensions,
Actions: actions,
Strategy: strategy,
Tools: tools,
Debt: debt,
Milestone: g.generateMilestone(phase, scorecard),
WhyNow: g.explainWhyNow(phase, findings),
RiskFlags: riskFlags,
StrictTarget: strictTarget,
Reminders: reminders,
}
}
func (g *NarrativeGenerator) determinePhase(findings []Finding, scorecard *Scorecard) string {
openCount := 0
t4Count := 0
t3Count := 0
for _, f := range findings {
if f.Status == StatusOpen {
openCount++
if f.Severity == SeverityT4 {
t4Count++
} else if f.Severity == SeverityT3 {
t3Count++
}
}
}
if openCount == 0 {
return "maintenance"
}
if t4Count > 0 {
return "critical"
}
if t3Count > 5 || openCount > 20 {
return "debt_reduction"
}
if openCount > 5 {
return "cleanup"
}
return "polish"
}
func (g *NarrativeGenerator) generateHeadline(phase string, scorecard *Scorecard) string {
switch phase {
case "maintenance":
return "Codebase is healthy! Focus on preventing new debt."
case "critical":
return fmt.Sprintf("Critical issues detected (%d strict score). Address T4 findings first.", scorecard.StrictScore)
case "debt_reduction":
return fmt.Sprintf("Significant technical debt (%d open issues). Systematic cleanup recommended.", scorecard.TotalScore)
case "cleanup":
return fmt.Sprintf("Minor issues detected (%d open). Quick wins available.", scorecard.TotalScore)
default:
return fmt.Sprintf("Codebase in good shape (%d open issues).", scorecard.TotalScore)
}
}
func (g *NarrativeGenerator) analyzeDimensions(findings []Finding) *NarrativeDimensions {
dimensionScores := make(map[Dimension][]Finding)
for _, f := range findings {
if f.Status == StatusOpen {
dim := g.classifyDimension(f)
dimensionScores[dim] = append(dimensionScores[dim], f)
}
}
var lowest []*DimensionInfo
var biggestGap []*DimensionInfo
var stagnant []*DimensionInfo
for dim, dimFindings := range dimensionScores {
info := &DimensionInfo{
Name: string(dim),
Issues: len(dimFindings),
}
impact := 0
for _, f := range dimFindings {
impact += f.Score * int(f.Severity)
}
info.Impact = float64(impact)
lowest = append(lowest, info)
}
sort.Slice(lowest, func(i, j int) bool {
return lowest[i].Impact > lowest[j].Impact
})
if len(lowest) > 5 {
lowest = lowest[:5]
}
return &NarrativeDimensions{
LowestDimensions: lowest,
BiggestGapDimensions: biggestGap,
StagnantDimensions: stagnant,
}
}
func (g *NarrativeGenerator) classifyDimension(f Finding) Dimension {
switch f.Type {
case "complexity", "complexity_ast":
return DimensionCodeQuality
case "duplication", "dupes":
return DimensionDuplication
case "dead_code", "unused_import", "unused":
return DimensionFileHealth
case "security":
return DimensionSecurity
case "naming":
return DimensionNamingQuality
case "import_cycle", "cycles":
return DimensionAbstractionFit
default:
return DimensionCodeQuality
}
}
func (g *NarrativeGenerator) generateActions(findings []Finding, phase string) []string {
var actions []string
t1AutoFixable := 0
t2Quick := 0
t3Judgment := 0
t4Major := 0
for _, f := range findings {
if f.Status != StatusOpen {
continue
}
switch f.Severity {
case SeverityT1:
t1AutoFixable++
case SeverityT2:
t2Quick++
case SeverityT3:
t3Judgment++
case SeverityT4:
t4Major++
}
}
if t4Major > 0 {
actions = append(actions, fmt.Sprintf("Address %d T4 (major refactor) issues - these require architectural changes", t4Major))
}
if t3Judgment > 0 {
actions = append(actions, fmt.Sprintf("Review %d T3 (needs judgment) issues - decide if they need fixing", t3Judgment))
}
if t1AutoFixable > 0 {
actions = append(actions, fmt.Sprintf("Run auto-fixer for %d T1 (auto-fixable) issues", t1AutoFixable))
}
if t2Quick > 0 {
actions = append(actions, fmt.Sprintf("Quick manual fixes available for %d T2 issues", t2Quick))
}
if len(actions) == 0 {
actions = append(actions, "No immediate actions required - maintain code quality")
}
return actions
}
func (g *NarrativeGenerator) generateStrategy(findings []Finding, dimensions *NarrativeDimensions) *NarrativeStrategy {
autoFixable := 0
total := 0
for _, f := range findings {
if f.Status == StatusOpen {
total++
if f.Severity == SeverityT1 {
autoFixable++
}
}
}
var recommendation string
var coverage float64
if total > 0 {
coverage = float64(autoFixable) / float64(total) * 100
}
if coverage > 50 {
recommendation = "Use auto-fixers first, then address remaining issues manually"
} else if autoFixable > 0 {
recommendation = "Start with auto-fixers for quick wins, then prioritize by impact"
} else {
recommendation = "Prioritize by severity and impact, starting with T4 issues"
}
return &NarrativeStrategy{
FixerLeverage: &FixerLeverage{
AutoFixableCount: autoFixable,
TotalCount: total,
Coverage: coverage,
Recommendation: recommendation,
},
CanParallelize: len(findings) > 3,
Hint: g.generateHint(findings),
}
}
func (g *NarrativeGenerator) generateHint(findings []Finding) string {
for _, f := range findings {
if f.Status == StatusOpen && f.Severity == SeverityT1 {
return "T1 issues can be auto-fixed with 'devour quality fix'"
}
}
for _, f := range findings {
if f.Status == StatusOpen && f.Severity == SeverityT4 {
return "T4 issues require planning - consider creating a dedicated branch"
}
}
return "Focus on one category at a time for best results"
}
func (g *NarrativeGenerator) generateTools(findings []Finding) *NarrativeTools {
fixers := []interface{}{}
for _, f := range findings {
if f.Status == StatusOpen && f.Severity == SeverityT1 {
fixers = append(fixers, map[string]string{
"name": f.Type,
"description": fmt.Sprintf("Fix %s issues", f.Type),
})
}
}
return &NarrativeTools{
Fixers: fixers,
Plan: &PlanTool{
Command: "devour quality plan",
Description: "Generate prioritized action plan",
},
Badge: &BadgeTool{
Generated: true,
InReadme: false,
Path: "scorecard.png",
},
}
}
func (g *NarrativeGenerator) analyzeDebt(findings []Finding, scorecard *Scorecard) *NarrativeDebt {
wontfixCount := 0
for _, f := range findings {
if f.Status == StatusWontfix {
wontfixCount++
}
}
var worstDimension string
var worstGap float64
dimensionImpact := make(map[string]float64)
for _, f := range findings {
if f.Status == StatusOpen {
dim := string(g.classifyDimension(f))
dimensionImpact[dim] += float64(f.Score * int(f.Severity))
}
}
for dim, impact := range dimensionImpact {
if impact > worstGap {
worstGap = impact
worstDimension = dim
}
}
return &NarrativeDebt{
OverallGap: float64(scorecard.StrictScore),
WontfixCount: wontfixCount,
WorstDimension: worstDimension,
WorstGap: worstGap,
Trend: "stable",
}
}
func (g *NarrativeGenerator) calculateStrictTarget(scorecard *Scorecard) *StrictTarget {
gap := float64(scorecard.StrictScore) / float64(g.targetScore) * 100
var state string
var warning *string
switch {
case gap >= 100:
state = "at_target"
case gap >= 80:
state = "near_target"
case gap >= 50:
state = "in_progress"
w := "Significant gap to target - consider focused effort"
warning = &w
default:
state = "needs_work"
w := "Large gap to target - prioritize high-impact fixes"
warning = &w
}
return &StrictTarget{
Target: float64(g.targetScore),
Current: float64(scorecard.StrictScore),
Gap: gap,
State: state,
Warning: warning,
}
}
func (g *NarrativeGenerator) generateReminders(findings []Finding, history []StateSnapshot) []string {
var reminders []string
autoFixable := 0
for _, f := range findings {
if f.Status == StatusOpen && f.Severity == SeverityT1 {
autoFixable++
}
}
if autoFixable > 0 {
reminders = append(reminders, fmt.Sprintf("%d auto-fixable issues available - use 'devour quality fix'", autoFixable))
}
if len(history) > 0 {
latest := history[len(history)-1]
if latest.Findings == len(findings) {
reminders = append(reminders, "No progress since last scan - consider tackling a specific category")
}
}
return reminders
}
func (g *NarrativeGenerator) identifyRisks(findings []Finding, history []StateSnapshot) []string {
var risks []string
t4Count := 0
for _, f := range findings {
if f.Status == StatusOpen && f.Severity == SeverityT4 {
t4Count++
}
}
if t4Count > 3 {
risks = append(risks, fmt.Sprintf("High number of T4 issues (%d) indicates architectural debt", t4Count))
}
if len(history) >= 3 {
trend := 0
for i := len(history) - 3; i < len(history); i++ {
trend += history[i].Findings
}
avg := trend / 3
if len(findings) > int(float64(avg)*1.2) {
risks = append(risks, "Finding count is trending upward - debt is accumulating")
}
}
return risks
}
func (g *NarrativeGenerator) generateMilestone(phase string, scorecard *Scorecard) string {
switch phase {
case "maintenance":
return "Maintain current quality level"
case "critical":
return "Reduce T4 issues to zero"
case "debt_reduction":
return fmt.Sprintf("Reduce strict score below %d", g.targetScore)
case "cleanup":
return "Clear all T1 and T2 issues"
default:
return "Continue quality improvement"
}
}
func (g *NarrativeGenerator) explainWhyNow(phase string, findings []Finding) string {
for _, f := range findings {
if f.Status == StatusOpen && f.Severity == SeverityT4 {
return "T4 issues compound over time - addressing them early prevents architectural decay"
}
}
t1Count := 0
for _, f := range findings {
if f.Status == StatusOpen && f.Severity == SeverityT1 {
t1Count++
}
}
if t1Count > 5 {
return "Quick wins available - auto-fixers can clear low-hanging fruit in minutes"
}
return "Consistent small improvements compound into significant quality gains"
}
-754
View File
@@ -1,754 +0,0 @@
package quality
import (
"testing"
"time"
)
func TestNewNarrativeGenerator(t *testing.T) {
tests := []struct {
name string
targetScore int
expected int
}{
{"default target", 0, 95},
{"custom target", 85, 85},
{"negative target", -10, 95},
{"zero target", 0, 95},
{"high target", 100, 100},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gen := NewNarrativeGenerator(tt.targetScore)
if gen.targetScore != tt.expected {
t.Errorf("NewNarrativeGenerator() targetScore = %v, want %v", gen.targetScore, tt.expected)
}
})
}
}
func TestNarrativeGenerator_determinePhase(t *testing.T) {
gen := NewNarrativeGenerator(95)
tests := []struct {
name string
findings []Finding
expected string
}{
{
name: "no open issues",
findings: []Finding{{Status: StatusFixed}},
expected: "maintenance",
},
{
name: "critical phase with T4",
findings: []Finding{
{Status: StatusOpen, Severity: SeverityT4},
},
expected: "critical",
},
{
name: "debt reduction with many T3",
findings: []Finding{
{Status: StatusOpen, Severity: SeverityT3},
{Status: StatusOpen, Severity: SeverityT3},
{Status: StatusOpen, Severity: SeverityT3},
{Status: StatusOpen, Severity: SeverityT3},
{Status: StatusOpen, Severity: SeverityT3},
{Status: StatusOpen, Severity: SeverityT3},
},
expected: "debt_reduction",
},
{
name: "debt reduction with many open issues",
findings: func() []Finding {
var f []Finding
for i := 0; i < 25; i++ {
f = append(f, Finding{Status: StatusOpen, Severity: SeverityT2})
}
return f
}(),
expected: "debt_reduction",
},
{
name: "cleanup phase",
findings: func() []Finding {
var f []Finding
for i := 0; i < 10; i++ {
f = append(f, Finding{Status: StatusOpen, Severity: SeverityT2})
}
return f
}(),
expected: "cleanup",
},
{
name: "polish phase",
findings: []Finding{
{Status: StatusOpen, Severity: SeverityT2},
{Status: StatusOpen, Severity: SeverityT2},
},
expected: "polish",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
scorecard := &Scorecard{TotalScore: 100}
phase := gen.determinePhase(tt.findings, scorecard)
if phase != tt.expected {
t.Errorf("determinePhase() = %v, want %v", phase, tt.expected)
}
})
}
}
func TestNarrativeGenerator_generateHeadline(t *testing.T) {
gen := NewNarrativeGenerator(95)
tests := []struct {
name string
phase string
scorecard *Scorecard
expected string
}{
{
name: "maintenance phase",
phase: "maintenance",
scorecard: &Scorecard{StrictScore: 50},
expected: "Codebase is healthy! Focus on preventing new debt.",
},
{
name: "critical phase",
phase: "critical",
scorecard: &Scorecard{StrictScore: 150},
expected: "Critical issues detected (150 strict score). Address T4 findings first.",
},
{
name: "debt reduction phase",
phase: "debt_reduction",
scorecard: &Scorecard{TotalScore: 200},
expected: "Significant technical debt (200 open issues). Systematic cleanup recommended.",
},
{
name: "cleanup phase",
phase: "cleanup",
scorecard: &Scorecard{TotalScore: 15},
expected: "Minor issues detected (15 open). Quick wins available.",
},
{
name: "polish phase",
phase: "polish",
scorecard: &Scorecard{TotalScore: 3},
expected: "Codebase in good shape (3 open issues).",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
headline := gen.generateHeadline(tt.phase, tt.scorecard)
if headline != tt.expected {
t.Errorf("generateHeadline() = %v, want %v", headline, tt.expected)
}
})
}
}
func TestNarrativeGenerator_classifyDimension(t *testing.T) {
gen := NewNarrativeGenerator(95)
tests := []struct {
name string
finding Finding
expected Dimension
}{
{
name: "complexity",
finding: Finding{Type: "complexity"},
expected: DimensionCodeQuality,
},
{
name: "complexity_ast",
finding: Finding{Type: "complexity_ast"},
expected: DimensionCodeQuality,
},
{
name: "duplication",
finding: Finding{Type: "duplication"},
expected: DimensionDuplication,
},
{
name: "dead_code",
finding: Finding{Type: "dead_code"},
expected: DimensionFileHealth,
},
{
name: "security",
finding: Finding{Type: "security"},
expected: DimensionSecurity,
},
{
name: "naming",
finding: Finding{Type: "naming"},
expected: DimensionNamingQuality,
},
{
name: "import_cycle",
finding: Finding{Type: "import_cycle"},
expected: DimensionAbstractionFit,
},
{
name: "unknown type",
finding: Finding{Type: "unknown"},
expected: DimensionCodeQuality,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dimension := gen.classifyDimension(tt.finding)
if dimension != tt.expected {
t.Errorf("classifyDimension() = %v, want %v", dimension, tt.expected)
}
})
}
}
func TestNarrativeGenerator_generateActions(t *testing.T) {
gen := NewNarrativeGenerator(95)
tests := []struct {
name string
findings []Finding
phase string
expected []string
}{
{
name: "mixed severities",
findings: []Finding{
{Status: StatusOpen, Severity: SeverityT4},
{Status: StatusOpen, Severity: SeverityT3},
{Status: StatusOpen, Severity: SeverityT2},
{Status: StatusOpen, Severity: SeverityT1},
},
phase: "critical",
expected: []string{
"Address 1 T4 (major refactor) issues - these require architectural changes",
"Review 1 T3 (needs judgment) issues - decide if they need fixing",
"Run auto-fixer for 1 T1 (auto-fixable) issues",
"Quick manual fixes available for 1 T2 issues",
},
},
{
name: "no open issues",
findings: []Finding{{Status: StatusFixed}},
phase: "maintenance",
expected: []string{"No immediate actions required - maintain code quality"},
},
{
name: "only T1 issues",
findings: []Finding{
{Status: StatusOpen, Severity: SeverityT1},
{Status: StatusOpen, Severity: SeverityT1},
},
phase: "polish",
expected: []string{
"Run auto-fixer for 2 T1 (auto-fixable) issues",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actions := gen.generateActions(tt.findings, tt.phase)
if len(actions) != len(tt.expected) {
t.Errorf("generateActions() length = %v, want %v", len(actions), len(tt.expected))
}
for i, action := range actions {
if i < len(tt.expected) && action != tt.expected[i] {
t.Errorf("generateActions()[%d] = %v, want %v", i, action, tt.expected[i])
}
}
})
}
}
func TestNarrativeGenerator_generateStrategy(t *testing.T) {
gen := NewNarrativeGenerator(95)
tests := []struct {
name string
findings []Finding
expected string
parallel bool
}{
{
name: "high auto-fixable coverage",
findings: []Finding{
{Status: StatusOpen, Severity: SeverityT1},
{Status: StatusOpen, Severity: SeverityT1},
{Status: StatusOpen, Severity: SeverityT2},
},
expected: "Use auto-fixers first, then address remaining issues manually",
parallel: false,
},
{
name: "some auto-fixable",
findings: []Finding{
{Status: StatusOpen, Severity: SeverityT1},
{Status: StatusOpen, Severity: SeverityT3},
{Status: StatusOpen, Severity: SeverityT4},
},
expected: "Start with auto-fixers for quick wins, then prioritize by impact",
parallel: false,
},
{
name: "no auto-fixable",
findings: []Finding{
{Status: StatusOpen, Severity: SeverityT3},
{Status: StatusOpen, Severity: SeverityT4},
},
expected: "Prioritize by severity and impact, starting with T4 issues",
parallel: false,
},
{
name: "no findings",
findings: []Finding{},
expected: "Prioritize by severity and impact, starting with T4 issues",
parallel: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dimensions := &NarrativeDimensions{}
strategy := gen.generateStrategy(tt.findings, dimensions)
if strategy.FixerLeverage.Recommendation != tt.expected {
t.Errorf("generateStrategy() recommendation = %v, want %v", strategy.FixerLeverage.Recommendation, tt.expected)
}
if strategy.CanParallelize != tt.parallel {
t.Errorf("generateStrategy() CanParallelize = %v, want %v", strategy.CanParallelize, tt.parallel)
}
})
}
}
func TestNarrativeGenerator_generateHint(t *testing.T) {
gen := NewNarrativeGenerator(95)
tests := []struct {
name string
findings []Finding
expected string
}{
{
name: "has T1 issues",
findings: []Finding{
{Status: StatusOpen, Severity: SeverityT1},
{Status: StatusOpen, Severity: SeverityT2},
},
expected: "T1 issues can be auto-fixed with 'devour quality fix'",
},
{
name: "has T4 issues but no T1",
findings: []Finding{
{Status: StatusOpen, Severity: SeverityT4},
{Status: StatusOpen, Severity: SeverityT3},
},
expected: "T4 issues require planning - consider creating a dedicated branch",
},
{
name: "no T1 or T4 issues",
findings: []Finding{
{Status: StatusOpen, Severity: SeverityT2},
{Status: StatusOpen, Severity: SeverityT3},
},
expected: "Focus on one category at a time for best results",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hint := gen.generateHint(tt.findings)
if hint != tt.expected {
t.Errorf("generateHint() = %v, want %v", hint, tt.expected)
}
})
}
}
func TestNarrativeGenerator_generateTools(t *testing.T) {
gen := NewNarrativeGenerator(95)
findings := []Finding{
{Status: StatusOpen, Severity: SeverityT1, Type: "dead_code"},
{Status: StatusOpen, Severity: SeverityT2, Type: "naming"},
}
tools := gen.generateTools(findings)
if tools.Plan.Command != "devour quality plan" {
t.Errorf("generateTools() Plan.Command = %v, want %v", tools.Plan.Command, "devour quality plan")
}
if !tools.Badge.Generated {
t.Error("generateTools() Badge.Generated should be true")
}
if len(tools.Fixers) != 1 {
t.Errorf("generateTools() Fixers length = %v, want 1", len(tools.Fixers))
}
}
func TestNarrativeGenerator_analyzeDebt(t *testing.T) {
gen := NewNarrativeGenerator(95)
findings := []Finding{
{Status: StatusOpen, Severity: SeverityT4, Type: "security", Score: 10},
{Status: StatusWontfix, Severity: SeverityT2, Type: "naming", Score: 5},
{Status: StatusOpen, Severity: SeverityT3, Type: "complexity", Score: 8},
}
scorecard := &Scorecard{StrictScore: 150}
debt := gen.analyzeDebt(findings, scorecard)
if debt.WontfixCount != 1 {
t.Errorf("analyzeDebt() WontfixCount = %v, want 1", debt.WontfixCount)
}
if debt.OverallGap != 150.0 {
t.Errorf("analyzeDebt() OverallGap = %v, want 150.0", debt.OverallGap)
}
if debt.WorstDimension != "Security" {
t.Errorf("analyzeDebt() WorstDimension = %v, want Security", debt.WorstDimension)
}
}
func TestNarrativeGenerator_calculateStrictTarget(t *testing.T) {
gen := NewNarrativeGenerator(100)
tests := []struct {
name string
scorecard *Scorecard
expected string
hasWarning bool
}{
{
name: "at target",
scorecard: &Scorecard{StrictScore: 100},
expected: "at_target",
hasWarning: false,
},
{
name: "near target",
scorecard: &Scorecard{StrictScore: 85},
expected: "near_target",
hasWarning: false,
},
{
name: "in progress",
scorecard: &Scorecard{StrictScore: 60},
expected: "in_progress",
hasWarning: true,
},
{
name: "needs work",
scorecard: &Scorecard{StrictScore: 30},
expected: "needs_work",
hasWarning: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
target := gen.calculateStrictTarget(tt.scorecard)
if target.State != tt.expected {
t.Errorf("calculateStrictTarget() State = %v, want %v", target.State, tt.expected)
}
if (target.Warning != nil) != tt.hasWarning {
t.Errorf("calculateStrictTarget() Warning presence = %v, want %v", target.Warning != nil, tt.hasWarning)
}
})
}
}
func TestNarrativeGenerator_generateReminders(t *testing.T) {
gen := NewNarrativeGenerator(95)
tests := []struct {
name string
findings []Finding
history []StateSnapshot
expected []string
}{
{
name: "auto-fixable available",
findings: []Finding{
{Status: StatusOpen, Severity: SeverityT1},
{Status: StatusOpen, Severity: SeverityT1},
},
history: []StateSnapshot{},
expected: []string{
"2 auto-fixable issues available - use 'devour quality fix'",
},
},
{
name: "no progress",
findings: []Finding{{Status: StatusOpen, Severity: SeverityT2}},
history: []StateSnapshot{{Findings: 1, Timestamp: time.Now()}},
expected: []string{
"No progress since last scan - consider tackling a specific category",
},
},
{
name: "no reminders",
findings: []Finding{{Status: StatusOpen, Severity: SeverityT3}},
history: []StateSnapshot{},
expected: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reminders := gen.generateReminders(tt.findings, tt.history)
if len(reminders) != len(tt.expected) {
t.Errorf("generateReminders() length = %v, want %v", len(reminders), len(tt.expected))
}
for i, reminder := range reminders {
if i < len(tt.expected) && reminder != tt.expected[i] {
t.Errorf("generateReminders()[%d] = %v, want %v", i, reminder, tt.expected[i])
}
}
})
}
}
func TestNarrativeGenerator_identifyRisks(t *testing.T) {
gen := NewNarrativeGenerator(95)
tests := []struct {
name string
findings []Finding
history []StateSnapshot
expected []string
}{
{
name: "high T4 count",
findings: func() []Finding {
var f []Finding
for i := 0; i < 5; i++ {
f = append(f, Finding{Status: StatusOpen, Severity: SeverityT4})
}
return f
}(),
history: []StateSnapshot{},
expected: []string{
"High number of T4 issues (5) indicates architectural debt",
},
},
{
name: "upward trend",
findings: func() []Finding {
var f []Finding
for i := 0; i < 25; i++ {
f = append(f, Finding{Status: StatusOpen, Severity: SeverityT2})
}
return f
}(),
history: []StateSnapshot{
{Findings: 10, Timestamp: time.Now().Add(-3 * time.Hour)},
{Findings: 12, Timestamp: time.Now().Add(-2 * time.Hour)},
{Findings: 15, Timestamp: time.Now().Add(-1 * time.Hour)},
},
expected: []string{
"Finding count is trending upward - debt is accumulating",
},
},
{
name: "no risks",
findings: []Finding{{Status: StatusOpen, Severity: SeverityT2}},
history: []StateSnapshot{},
expected: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
risks := gen.identifyRisks(tt.findings, tt.history)
if len(risks) != len(tt.expected) {
t.Errorf("identifyRisks() length = %v, want %v", len(risks), len(tt.expected))
}
for i, risk := range risks {
if i < len(tt.expected) && risk != tt.expected[i] {
t.Errorf("identifyRisks()[%d] = %v, want %v", i, risk, tt.expected[i])
}
}
})
}
}
func TestNarrativeGenerator_generateMilestone(t *testing.T) {
gen := NewNarrativeGenerator(95)
tests := []struct {
name string
phase string
scorecard *Scorecard
expected string
}{
{
name: "maintenance",
phase: "maintenance",
scorecard: &Scorecard{},
expected: "Maintain current quality level",
},
{
name: "critical",
phase: "critical",
scorecard: &Scorecard{},
expected: "Reduce T4 issues to zero",
},
{
name: "debt reduction",
phase: "debt_reduction",
scorecard: &Scorecard{},
expected: "Reduce strict score below 95",
},
{
name: "cleanup",
phase: "cleanup",
scorecard: &Scorecard{},
expected: "Clear all T1 and T2 issues",
},
{
name: "polish",
phase: "polish",
scorecard: &Scorecard{},
expected: "Continue quality improvement",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
milestone := gen.generateMilestone(tt.phase, tt.scorecard)
if milestone != tt.expected {
t.Errorf("generateMilestone() = %v, want %v", milestone, tt.expected)
}
})
}
}
func TestNarrativeGenerator_explainWhyNow(t *testing.T) {
gen := NewNarrativeGenerator(95)
tests := []struct {
name string
phase string
findings []Finding
expected string
}{
{
name: "has T4 issues",
findings: []Finding{
{Status: StatusOpen, Severity: SeverityT4},
},
expected: "T4 issues compound over time - addressing them early prevents architectural decay",
},
{
name: "many T1 issues",
findings: func() []Finding {
var f []Finding
for i := 0; i < 6; i++ {
f = append(f, Finding{Status: StatusOpen, Severity: SeverityT1})
}
return f
}(),
expected: "Quick wins available - auto-fixers can clear low-hanging fruit in minutes",
},
{
name: "few T1 issues",
findings: []Finding{
{Status: StatusOpen, Severity: SeverityT1},
{Status: StatusOpen, Severity: SeverityT2},
},
expected: "Consistent small improvements compound into significant quality gains",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
whyNow := gen.explainWhyNow(tt.phase, tt.findings)
if whyNow != tt.expected {
t.Errorf("explainWhyNow() = %v, want %v", whyNow, tt.expected)
}
})
}
}
func TestNarrativeGenerator_Generate(t *testing.T) {
gen := NewNarrativeGenerator(95)
findings := []Finding{
{Status: StatusOpen, Severity: SeverityT2, Type: "naming", Score: 5},
{Status: StatusOpen, Severity: SeverityT1, Type: "dead_code", Score: 3},
}
scorecard := &Scorecard{
TotalScore: 8,
StrictScore: 15,
TargetScore: 95,
LastScan: time.Now(),
}
history := []StateSnapshot{
{Findings: 10, Timestamp: time.Now().Add(-1 * time.Hour)},
}
narrative := gen.Generate(findings, scorecard, history)
if narrative.Phase == "" {
t.Error("Generate() Phase should not be empty")
}
if narrative.Headline == "" {
t.Error("Generate() Headline should not be empty")
}
if narrative.Dimensions == nil {
t.Error("Generate() Dimensions should not be nil")
}
if len(narrative.Actions) == 0 {
t.Error("Generate() Actions should not be empty")
}
if narrative.Strategy == nil {
t.Error("Generate() Strategy should not be nil")
}
if narrative.Tools == nil {
t.Error("Generate() Tools should not be nil")
}
if narrative.Debt == nil {
t.Error("Generate() Debt should not be nil")
}
if narrative.Milestone == "" {
t.Error("Generate() Milestone should not be empty")
}
if narrative.WhyNow == "" {
t.Error("Generate() WhyNow should not be empty")
}
if narrative.StrictTarget == nil {
t.Error("Generate() StrictTarget should not be nil")
}
}
@@ -239,7 +239,7 @@ func (d *SingleUseDetector) getFuncLOC(file string, startLine int) (int, error)
fset := token.NewFileSet() fset := token.NewFileSet()
node, err := parser.ParseFile(fset, file, nil, 0) node, err := parser.ParseFile(fset, file, nil, 0)
if err != nil { if err != nil {
return 0, err return 0, fmt.Errorf("parse %s for function loc lookup: %w", file, err)
} }
loc := 0 loc := 0
@@ -43,6 +43,21 @@ func (d *LargeFileDetector) Detect(ctx context.Context, path string, config *qua
for _, file := range files { for _, file := range files {
loc, err := countLines(file) loc, err := countLines(file)
if err != nil { if err != nil {
findings = append(findings, quality.Finding{
ID: fmt.Sprintf("detector_read_error::large_file::%s", file),
Type: "detector_error",
Title: "Large file detector could not read file",
Description: fmt.Sprintf("Failed to count lines in %s: %v", filepath.Base(file), err),
File: file,
Line: 1,
Severity: quality.SeverityT2,
Score: 0,
Status: quality.StatusOpen,
Metadata: map[string]string{
"detector": "large_file",
"error": err.Error(),
},
})
continue continue
} }
@@ -99,18 +114,21 @@ func (d *GodStructDetector) Detect(ctx context.Context, path string, config *qua
var findings []quality.Finding var findings []quality.Finding
for _, file := range files { for _, file := range files {
fileFindings := d.analyzeFile(file) fileFindings, err := d.analyzeFile(file)
if err != nil {
return nil, fmt.Errorf("analyze god struct in %q: %w", file, err)
}
findings = append(findings, fileFindings...) findings = append(findings, fileFindings...)
} }
return findings, nil return findings, nil
} }
func (d *GodStructDetector) analyzeFile(path string) []quality.Finding { func (d *GodStructDetector) analyzeFile(path string) ([]quality.Finding, error) {
fset := token.NewFileSet() fset := token.NewFileSet()
node, err := parser.ParseFile(fset, path, nil, 0) node, err := parser.ParseFile(fset, path, nil, 0)
if err != nil { if err != nil {
return nil return nil, fmt.Errorf("parse %s: %w", path, err)
} }
methodCounts := make(map[string]int) methodCounts := make(map[string]int)
@@ -198,7 +216,7 @@ func (d *GodStructDetector) analyzeFile(path string) []quality.Finding {
} }
} }
return findings return findings, nil
} }
type DebugLogDetector struct { type DebugLogDetector struct {
@@ -227,22 +245,25 @@ func (d *DebugLogDetector) Detect(ctx context.Context, path string, config *qual
var findings []quality.Finding var findings []quality.Finding
for _, file := range files { for _, file := range files {
fileFindings := d.analyzeFile(file) fileFindings, err := d.analyzeFile(file)
if err != nil {
return nil, fmt.Errorf("analyze debug logs in %q: %w", file, err)
}
findings = append(findings, fileFindings...) findings = append(findings, fileFindings...)
} }
return findings, nil return findings, nil
} }
func (d *DebugLogDetector) analyzeFile(path string) []quality.Finding { func (d *DebugLogDetector) analyzeFile(path string) ([]quality.Finding, error) {
fset := token.NewFileSet() fset := token.NewFileSet()
node, err := parser.ParseFile(fset, path, nil, 0) node, err := parser.ParseFile(fset, path, nil, 0)
if err != nil { if err != nil {
return nil return nil, fmt.Errorf("parse %s: %w", path, err)
} }
normPath := filepath.ToSlash(path) normPath := filepath.ToSlash(path)
if strings.Contains(normPath, "internal/ui/") || strings.Contains(normPath, "examples/") { if strings.Contains(normPath, "internal/ui/") || strings.Contains(normPath, "examples/") {
return nil return nil, nil
} }
debugPatterns := []string{ debugPatterns := []string{
@@ -324,7 +345,7 @@ func (d *DebugLogDetector) analyzeFile(path string) []quality.Finding {
return true return true
}) })
return findings return findings, nil
} }
type GodFunctionDetector struct { type GodFunctionDetector struct {
@@ -37,7 +37,7 @@ func (d *TestCoverageDetector) Detect(ctx context.Context, path string, config *
_, err := exec.LookPath("go") _, err := exec.LookPath("go")
if err != nil { if err != nil {
return nil, nil return nil, fmt.Errorf("go toolchain is not available: %w", err)
} }
if _, err := os.Stat(coverFile); os.IsNotExist(err) { if _, err := os.Stat(coverFile); os.IsNotExist(err) {
@@ -48,13 +48,13 @@ func (d *TestCoverageDetector) Detect(ctx context.Context, path string, config *
} }
if _, err := os.Stat(coverFile); os.IsNotExist(err) { if _, err := os.Stat(coverFile); os.IsNotExist(err) {
return nil, nil return nil, fmt.Errorf("coverage profile was not generated at %q", coverFile)
} }
} }
coverage, err := d.parseCoverageFile(coverFile) coverage, err := d.parseCoverageFile(coverFile)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("parse coverage profile %q: %w", coverFile, err)
} }
var findings []quality.Finding var findings []quality.Finding
@@ -210,7 +210,7 @@ func (d *UntestedFuncDetector) Detect(ctx context.Context, path string, config *
coverFile := filepath.Join(path, "coverage.out") coverFile := filepath.Join(path, "coverage.out")
data, err := os.ReadFile(coverFile) data, err := os.ReadFile(coverFile)
if err != nil { if err != nil {
return nil, nil return nil, fmt.Errorf("read coverage profile %q: %w", coverFile, err)
} }
uncoveredFuncs := make(map[string][]UncoveredFunc) uncoveredFuncs := make(map[string][]UncoveredFunc)
+17 -5
View File
@@ -82,8 +82,12 @@ func (p *GoPlugin) AnalyzeFile(ctx context.Context, path string, config *quality
analysis := &plugins.FileAnalysis{ analysis := &plugins.FileAnalysis{
Path: path, Path: path,
Package: node.Name.Name, Package: node.Name.Name,
LOC: countLOC(path),
} }
loc, err := countLOC(path)
if err != nil {
return nil, fmt.Errorf("count loc for %s: %w", path, err)
}
analysis.LOC = loc
analysis.Imports = p.extractImports(node, fset) analysis.Imports = p.extractImports(node, fset)
analysis.Functions = p.extractFunctions(node, path, fset) analysis.Functions = p.extractFunctions(node, path, fset)
@@ -349,16 +353,24 @@ func (p *GoPlugin) LoadTypesInfo(ctx context.Context, path string) (*types.Info,
return pkgs[0].TypesInfo, pkgs[0].Fset, nil return pkgs[0].TypesInfo, pkgs[0].Fset, nil
} }
func countLOC(path string) int { func countLOC(path string) (int, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
return 0 return 0, fmt.Errorf("read file for loc %q: %w", path, err)
} }
return strings.Count(string(data), "\n") + 1 return strings.Count(string(data), "\n") + 1, nil
}
var pluginRegistrationErr error
// RegistrationError returns a plugin registration error captured during init, if any.
func RegistrationError() error {
return pluginRegistrationErr
} }
func init() { func init() {
if err := plugins.Register(New()); err != nil { if err := plugins.Register(New()); err != nil {
panic(fmt.Sprintf("failed to register go plugin: %v", err)) pluginRegistrationErr = fmt.Errorf("register go quality plugin: %w", err)
_, _ = fmt.Fprintf(os.Stderr, "warning: %v\n", pluginRegistrationErr)
} }
} }
-315
View File
@@ -1,315 +0,0 @@
package review
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/yourorg/devour/internal/quality"
)
type ReviewPacket struct {
Generated time.Time `json:"generated"`
ProjectPath string `json:"project_path"`
Language string `json:"language"`
Scorecard *quality.Scorecard `json:"scorecard"`
Findings []FindingReview `json:"findings"`
Context ReviewContext `json:"context"`
Questions []ReviewQuestion `json:"questions"`
}
type FindingReview struct {
ID string `json:"id"`
Type string `json:"type"`
Title string `json:"title"`
Description string `json:"description"`
File string `json:"file"`
Line int `json:"line"`
Severity quality.Severity `json:"severity"`
Score int `json:"score"`
Status quality.Status `json:"status"`
NeedsReview bool `json:"needs_review"`
Context string `json:"context"`
Metadata map[string]string `json:"metadata"`
}
type ReviewContext struct {
TotalFiles int `json:"total_files"`
TotalLOC int `json:"total_loc"`
FindingsByDim map[string]int `json:"findings_by_dimension"`
TopIssues []string `json:"top_issues"`
Trends map[string]string `json:"trends"`
}
type ReviewQuestion struct {
ID string `json:"id"`
Category string `json:"category"`
Question string `json:"question"`
Options []string `json:"options,omitempty"`
}
type PacketGenerator struct {
dataDir string
}
func NewPacketGenerator(dataDir string) *PacketGenerator {
return &PacketGenerator{dataDir: dataDir}
}
func (g *PacketGenerator) Generate(findings []quality.Finding, scorecard *quality.Scorecard, lang string) (*ReviewPacket, error) {
packet := &ReviewPacket{
Generated: time.Now(),
ProjectPath: g.dataDir,
Language: lang,
Scorecard: scorecard,
Findings: g.convertFindings(findings),
Context: g.buildContext(findings),
Questions: g.generateQuestions(findings),
}
return packet, nil
}
func (g *PacketGenerator) convertFindings(findings []quality.Finding) []FindingReview {
var reviews []FindingReview
for _, f := range findings {
if f.Status != quality.StatusOpen {
continue
}
review := FindingReview{
ID: f.ID,
Type: f.Type,
Title: f.Title,
Description: f.Description,
File: f.File,
Line: f.Line,
Severity: f.Severity,
Score: f.Score,
Status: f.Status,
NeedsReview: f.Severity >= quality.SeverityT3,
Metadata: f.Metadata,
}
review.Context = g.generateContext(f)
reviews = append(reviews, review)
}
return reviews
}
func (g *PacketGenerator) generateContext(f quality.Finding) string {
switch f.Type {
case "complexity", "complexity_ast":
return "This function may be difficult to maintain. Consider if it can be simplified or broken down."
case "duplication":
return "Similar code exists elsewhere. Consider extracting common functionality."
case "dead_code":
return "This code appears unused. Verify before removing - it may be called via reflection or external tools."
case "security":
return "Potential security concern. Review carefully and consider security implications."
case "import_cycle":
return "Circular dependency detected. This can cause initialization issues and makes code harder to understand."
default:
return "Review this finding and decide if it needs addressing."
}
}
func (g *PacketGenerator) buildContext(findings []quality.Finding) ReviewContext {
byDim := make(map[string]int)
var topIssues []string
for _, f := range findings {
if f.Status == quality.StatusOpen {
dim := g.classifyDimension(f)
byDim[dim]++
}
}
topCount := 0
for _, f := range findings {
if f.Status == quality.StatusOpen && topCount < 5 {
topIssues = append(topIssues, fmt.Sprintf("%s: %s", f.Type, f.Title))
topCount++
}
}
return ReviewContext{
FindingsByDim: byDim,
TopIssues: topIssues,
Trends: make(map[string]string),
}
}
func (g *PacketGenerator) classifyDimension(f quality.Finding) string {
switch f.Type {
case "complexity", "complexity_ast":
return "Code Quality"
case "duplication":
return "Duplication"
case "dead_code", "unused_import", "unused":
return "File Health"
case "security":
return "Security"
case "naming":
return "Naming Quality"
case "import_cycle":
return "Architecture"
default:
return "Other"
}
}
func (g *PacketGenerator) generateQuestions(findings []quality.Finding) []ReviewQuestion {
var questions []ReviewQuestion
hasDupes := false
hasComplex := false
hasDead := false
for _, f := range findings {
if f.Status != quality.StatusOpen {
continue
}
switch f.Type {
case "duplication":
hasDupes = true
case "complexity", "complexity_ast":
hasComplex = true
case "dead_code":
hasDead = true
}
}
if hasDupes {
questions = append(questions, ReviewQuestion{
ID: "dupe_strategy",
Category: "duplication",
Question: "How should duplicated code be consolidated?",
Options: []string{
"Extract to shared utility",
"Keep separate (different use cases)",
"Refactor to common interface",
},
})
}
if hasComplex {
questions = append(questions, ReviewQuestion{
ID: "complexity_strategy",
Category: "complexity",
Question: "What's the best approach for complex functions?",
Options: []string{
"Break into smaller functions",
"Introduce helper types",
"Accept current complexity",
},
})
}
if hasDead {
questions = append(questions, ReviewQuestion{
ID: "dead_code_strategy",
Category: "maintenance",
Question: "Should unused code be removed?",
Options: []string{
"Remove if truly unused",
"Keep for future use",
"Mark as deprecated",
},
})
}
questions = append(questions, ReviewQuestion{
ID: "priority",
Category: "planning",
Question: "Which area should be prioritized for improvement?",
Options: []string{
"Security issues first",
"Complexity reduction",
"Dead code cleanup",
"Architecture improvements",
},
})
return questions
}
func (g *PacketGenerator) Save(packet *ReviewPacket, filename string) error {
reviewDir := filepath.Join(g.dataDir, "review")
if err := os.MkdirAll(reviewDir, 0755); err != nil {
return fmt.Errorf("failed to create review directory: %w", err)
}
path := filepath.Join(reviewDir, filename)
data, err := json.MarshalIndent(packet, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal packet: %w", err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
return fmt.Errorf("failed to write packet: %w", err)
}
return nil
}
func (g *PacketGenerator) Load(filename string) (*ReviewPacket, error) {
path := filepath.Join(g.dataDir, "review", filename)
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read packet: %w", err)
}
var packet ReviewPacket
if err := json.Unmarshal(data, &packet); err != nil {
return nil, fmt.Errorf("failed to parse packet: %w", err)
}
return &packet, nil
}
func (g *PacketGenerator) ImportReview(filename string, responses map[string]string) error {
_, err := g.Load(filename)
if err != nil {
return err
}
findingsPath := filepath.Join(g.dataDir, "quality", "status.json")
data, err := os.ReadFile(findingsPath)
if err != nil {
return fmt.Errorf("failed to read findings: %w", err)
}
var state struct {
Findings []quality.Finding `json:"findings"`
}
if err := json.Unmarshal(data, &state); err != nil {
return fmt.Errorf("failed to parse findings: %w", err)
}
for _, f := range state.Findings {
if response, ok := responses[f.ID]; ok {
if f.Metadata == nil {
f.Metadata = make(map[string]string)
}
f.Metadata["review_response"] = response
f.Metadata["reviewed_at"] = time.Now().Format(time.RFC3339)
}
}
updatedData, err := json.MarshalIndent(state, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal updated findings: %w", err)
}
if err := os.WriteFile(findingsPath, updatedData, 0644); err != nil {
return fmt.Errorf("failed to write updated findings: %w", err)
}
return nil
}
+17
View File
@@ -76,6 +76,23 @@ func (s *Scanner) Scan(ctx context.Context) (*ScanResult, error) {
findings, err := s.runDetectorSafely(ctx, detector, name) findings, err := s.runDetectorSafely(ctx, detector, name)
if err != nil { if err != nil {
log.Printf("Detector %s failed: %v", name, err) log.Printf("Detector %s failed: %v", name, err)
allFindings = append(allFindings, Finding{
ID: fmt.Sprintf("detector_error::%s", name),
Type: "detector_error",
Title: fmt.Sprintf("Detector failed: %s", name),
Description: fmt.Sprintf("Detector %s failed during scan: %v", name, err),
File: s.config.Path,
Line: 1,
Severity: SeverityT2,
Score: 0,
Status: StatusOpen,
Metadata: map[string]string{
"detector": name,
"error": err.Error(),
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
continue continue
} }
+19 -2
View File
@@ -30,7 +30,24 @@ func TestScannerRecoversDetectorPanic(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("scan should recover detector panic, got err: %v", err) t.Fatalf("scan should recover detector panic, got err: %v", err)
} }
if len(result.Findings) != 1 { if len(result.Findings) != 2 {
t.Fatalf("expected findings from healthy detector only, got %d", len(result.Findings)) t.Fatalf("expected healthy finding plus detector_error, got %d", len(result.Findings))
}
hasOK := false
hasDetectorError := false
for _, f := range result.Findings {
if f.ID == "ok" {
hasOK = true
}
if f.Type == "detector_error" {
hasDetectorError = true
}
}
if !hasOK {
t.Fatalf("expected to keep finding from healthy detector")
}
if !hasDetectorError {
t.Fatalf("expected detector_error finding for panicing detector")
} }
} }
+5 -2
View File
@@ -271,8 +271,11 @@ func TestScanner_Scan_WithFailingDetector(t *testing.T) {
} }
// Should succeed despite failing detector // Should succeed despite failing detector
if len(result.Findings) != 0 { if len(result.Findings) != 1 {
t.Errorf("Scan() expected 0 findings, got %d", len(result.Findings)) t.Errorf("Scan() expected 1 detector_error finding, got %d", len(result.Findings))
}
if len(result.Findings) == 1 && result.Findings[0].Type != "detector_error" {
t.Errorf("Scan() expected detector_error finding, got %q", result.Findings[0].Type)
} }
} }
+1 -23
View File
@@ -5,7 +5,6 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -70,28 +69,7 @@ func (s *AstroDocsScraper) DetectChanges(ctx context.Context, source *Source, la
} }
func (s *AstroDocsScraper) fetchPage(ctx context.Context, url string) (string, error) { func (s *AstroDocsScraper) fetchPage(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) return fetchExternalPage(ctx, s.client, s.config.UserAgent, url)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", s.config.UserAgent)
resp, err := s.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
} }
func (s *AstroDocsScraper) generateHash(content string) string { func (s *AstroDocsScraper) generateHash(content string) string {
+1 -23
View File
@@ -5,7 +5,6 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -75,28 +74,7 @@ func (s *CloudflareDocsScraper) DetectChanges(ctx context.Context, source *Sourc
} }
func (s *CloudflareDocsScraper) fetchPage(ctx context.Context, url string) (string, error) { func (s *CloudflareDocsScraper) fetchPage(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) return fetchExternalPage(ctx, s.client, s.config.UserAgent, url)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", s.config.UserAgent)
resp, err := s.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
} }
func (s *CloudflareDocsScraper) generateHash(content string) string { func (s *CloudflareDocsScraper) generateHash(content string) string {
+1 -23
View File
@@ -5,7 +5,6 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -70,28 +69,7 @@ func (s *DockerDocsScraper) DetectChanges(ctx context.Context, source *Source, l
} }
func (s *DockerDocsScraper) fetchPage(ctx context.Context, url string) (string, error) { func (s *DockerDocsScraper) fetchPage(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) return fetchExternalPage(ctx, s.client, s.config.UserAgent, url)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", s.config.UserAgent)
resp, err := s.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
} }
func (s *DockerDocsScraper) generateHash(content string) string { func (s *DockerDocsScraper) generateHash(content string) string {
+1 -23
View File
@@ -6,7 +6,6 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -91,28 +90,7 @@ func (s *GoDocsScraper) DetectChanges(ctx context.Context, source *Source, lastH
} }
func (s *GoDocsScraper) fetchPage(ctx context.Context, url string) (string, error) { func (s *GoDocsScraper) fetchPage(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) return fetchExternalPage(ctx, s.client, s.config.UserAgent, url)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", s.config.UserAgent)
resp, err := s.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
} }
func (s *GoDocsScraper) generateHash(content string) string { func (s *GoDocsScraper) generateHash(content string) string {
+1 -23
View File
@@ -5,7 +5,6 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -85,28 +84,7 @@ func (s *JavaDocsScraper) DetectChanges(ctx context.Context, source *Source, las
} }
func (s *JavaDocsScraper) fetchPage(ctx context.Context, url string) (string, error) { func (s *JavaDocsScraper) fetchPage(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) return fetchExternalPage(ctx, s.client, s.config.UserAgent, url)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", s.config.UserAgent)
resp, err := s.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
} }
func (s *JavaDocsScraper) generateHash(content string) string { func (s *JavaDocsScraper) generateHash(content string) string {
+1 -23
View File
@@ -5,7 +5,6 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -80,28 +79,7 @@ func (s *MCPDocsScraper) DetectChanges(ctx context.Context, source *Source, last
} }
func (s *MCPDocsScraper) fetchPage(ctx context.Context, url string) (string, error) { func (s *MCPDocsScraper) fetchPage(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) return fetchExternalPage(ctx, s.client, s.config.UserAgent, url)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", s.config.UserAgent)
resp, err := s.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
} }
func (s *MCPDocsScraper) generateHash(content string) string { func (s *MCPDocsScraper) generateHash(content string) string {
+1 -23
View File
@@ -5,7 +5,6 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -90,28 +89,7 @@ func (s *NuxtDocsScraper) DetectChanges(ctx context.Context, source *Source, las
} }
func (s *NuxtDocsScraper) fetchPage(ctx context.Context, url string) (string, error) { func (s *NuxtDocsScraper) fetchPage(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) return fetchExternalPage(ctx, s.client, s.config.UserAgent, url)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", s.config.UserAgent)
resp, err := s.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
} }
func (s *NuxtDocsScraper) generateHash(content string) string { func (s *NuxtDocsScraper) generateHash(content string) string {
+1 -23
View File
@@ -5,7 +5,6 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -100,28 +99,7 @@ func (s *PythonDocsScraper) DetectChanges(ctx context.Context, source *Source, l
} }
func (s *PythonDocsScraper) fetchPage(ctx context.Context, url string) (string, error) { func (s *PythonDocsScraper) fetchPage(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) return fetchExternalPage(ctx, s.client, s.config.UserAgent, url)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", s.config.UserAgent)
resp, err := s.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
} }
func (s *PythonDocsScraper) generateHash(content string) string { func (s *PythonDocsScraper) generateHash(content string) string {
+1 -23
View File
@@ -5,7 +5,6 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -80,28 +79,7 @@ func (s *ReactDocsScraper) DetectChanges(ctx context.Context, source *Source, la
} }
func (s *ReactDocsScraper) fetchPage(ctx context.Context, url string) (string, error) { func (s *ReactDocsScraper) fetchPage(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) return fetchExternalPage(ctx, s.client, s.config.UserAgent, url)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", s.config.UserAgent)
resp, err := s.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
} }
func (s *ReactDocsScraper) generateHash(content string) string { func (s *ReactDocsScraper) generateHash(content string) string {
+1 -23
View File
@@ -6,7 +6,6 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -106,28 +105,7 @@ func (s *RustDocsScraper) DetectChanges(ctx context.Context, source *Source, las
} }
func (s *RustDocsScraper) fetchPage(ctx context.Context, url string) (string, error) { func (s *RustDocsScraper) fetchPage(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) return fetchExternalPage(ctx, s.client, s.config.UserAgent, url)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", s.config.UserAgent)
resp, err := s.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
} }
func (s *RustDocsScraper) generateHash(content string) string { func (s *RustDocsScraper) generateHash(content string) string {
+1 -23
View File
@@ -5,7 +5,6 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -80,28 +79,7 @@ func (s *SpringDocsScraper) DetectChanges(ctx context.Context, source *Source, l
} }
func (s *SpringDocsScraper) fetchPage(ctx context.Context, url string) (string, error) { func (s *SpringDocsScraper) fetchPage(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) return fetchExternalPage(ctx, s.client, s.config.UserAgent, url)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", s.config.UserAgent)
resp, err := s.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
} }
func (s *SpringDocsScraper) generateHash(content string) string { func (s *SpringDocsScraper) generateHash(content string) string {
+1 -23
View File
@@ -5,7 +5,6 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -85,28 +84,7 @@ func (s *TSDocsScraper) DetectChanges(ctx context.Context, source *Source, lastH
} }
func (s *TSDocsScraper) fetchPage(ctx context.Context, url string) (string, error) { func (s *TSDocsScraper) fetchPage(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) return fetchExternalPage(ctx, s.client, s.config.UserAgent, url)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", s.config.UserAgent)
resp, err := s.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
} }
func (s *TSDocsScraper) generateHash(content string) string { func (s *TSDocsScraper) generateHash(content string) string {
+65
View File
@@ -1,8 +1,14 @@
package scraper package scraper
import ( import (
"context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"strings"
basescraper "github.com/yourorg/devour/internal/scraper" basescraper "github.com/yourorg/devour/internal/scraper"
) )
@@ -19,3 +25,62 @@ func generateDocID(urlStr string) string {
hash := sha256.Sum256([]byte(urlStr)) hash := sha256.Sum256([]byte(urlStr))
return hex.EncodeToString(hash[:12]) return hex.EncodeToString(hash[:12])
} }
func fetchExternalPage(ctx context.Context, client *http.Client, userAgent, targetURL string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", targetURL, nil)
if err != nil {
return "", err
}
req.Header.Set("User-Agent", userAgent)
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
snippet, readErr := readErrorSnippet(resp.Body)
if readErr != nil {
return "", fmt.Errorf("GET %s returned HTTP %d and body read failed: %w", summarizeURL(targetURL), resp.StatusCode, readErr)
}
return "", fmt.Errorf("GET %s returned HTTP %d: %s", summarizeURL(targetURL), resp.StatusCode, snippet)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
func readErrorSnippet(body io.Reader) (string, error) {
const maxErrorBodyBytes = 512
data, err := io.ReadAll(io.LimitReader(body, maxErrorBodyBytes))
if err != nil {
return "", err
}
msg := strings.TrimSpace(string(data))
if msg == "" {
return "<empty body>", nil
}
return msg, nil
}
func summarizeURL(rawURL string) string {
parsedURL, err := url.Parse(rawURL)
if err != nil || parsedURL.Host == "" {
return rawURL
}
path := parsedURL.EscapedPath()
if path == "" {
path = "/"
}
return parsedURL.Host + path
}

Some files were not shown because too many files have changed in this diff Show More