mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-03 20:13:03 +00:00
i dont like commits
This commit is contained in:
@@ -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
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
File diff suppressed because one or more lines are too long
+6464
-1799
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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!")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
Vendored
+1
-23
@@ -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
@@ -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 {
|
||||||
|
|||||||
Vendored
+1
-23
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
Vendored
+1
-23
@@ -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 {
|
||||||
|
|||||||
Vendored
+65
@@ -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
Reference in New Issue
Block a user