diff --git a/.codeboarding/analysis.json b/.codeboarding/analysis.json index f6cabd1..5e22522 100644 --- a/.codeboarding/analysis.json +++ b/.codeboarding/analysis.json @@ -1,20 +1,20 @@ { "metadata": { - "generated_at": "2026-06-13T22:34:30.929104+00:00", - "commit_hash": "f8b3b03d4d7f729a89675622d8c13787e40f992b", + "generated_at": "2026-06-14T00:18:08.327277+00:00", + "commit_hash": "39342f8d7afcb0f61464f4213b4165af14c482a1", "repo_name": "CodeBoarding-action", "depth_level": 2, "file_coverage_summary": { - "total_files": 18, - "analyzed": 4, - "not_analyzed": 14, + "total_files": 20, + "analyzed": 5, + "not_analyzed": 15, "not_analyzed_by_reason": { "other": 9, - "codeboardingignore": 5 + "codeboardingignore": 6 } } }, - "description": "The CodeBoarding-action architecture is a stateful pipeline that transforms raw code changes into architectural insights. It orchestrates a flow from environment validation and structural analysis to differential visualization and developer-centric reporting, utilizing a baseline-driven incremental diffing approach to optimize LLM processing.", + "description": "The CodeBoarding-action system operates as a linear pipeline within a GitHub Action environment, transforming raw source code changes into visual architectural insights through orchestration, structural diffing, visualization, and integration layers.", "files": { "scripts/engine_adapter.py": { "method_keys": [ @@ -72,7 +72,6 @@ "scripts/diff_to_mermaid.py|scripts.diff_to_mermaid._display_status", "scripts/diff_to_mermaid.py|scripts.diff_to_mermaid._Scope", "scripts/diff_to_mermaid.py|scripts.diff_to_mermaid._Scope.__init__", - "scripts/diff_to_mermaid.py|scripts.diff_to_mermaid._Scope.resolve", "scripts/diff_to_mermaid.py|scripts.diff_to_mermaid._filter_changed", "scripts/diff_to_mermaid.py|scripts.diff_to_mermaid._filter_changed.touches", "scripts/diff_to_mermaid.py|scripts.diff_to_mermaid._init_directive", @@ -94,6 +93,23 @@ "scripts/build_cta.py|scripts.build_cta.build_cta.link", "scripts/build_cta.py|scripts.build_cta.main" ] + }, + "scripts/submit_feedback.py": { + "method_keys": [ + "scripts/submit_feedback.py|scripts.submit_feedback.telemetry_disabled", + "scripts/submit_feedback.py|scripts.submit_feedback.resolve_key", + "scripts/submit_feedback.py|scripts.submit_feedback.resolve_host", + "scripts/submit_feedback.py|scripts.submit_feedback.resolve_command", + "scripts/submit_feedback.py|scripts.submit_feedback.resolve_max_chars", + "scripts/submit_feedback.py|scripts.submit_feedback.extract_feedback", + "scripts/submit_feedback.py|scripts.submit_feedback.cap_feedback", + "scripts/submit_feedback.py|scripts.submit_feedback._first", + "scripts/submit_feedback.py|scripts.submit_feedback.distinct_id", + "scripts/submit_feedback.py|scripts.submit_feedback.build_properties", + "scripts/submit_feedback.py|scripts.submit_feedback.build_payload", + "scripts/submit_feedback.py|scripts.submit_feedback.post", + "scripts/submit_feedback.py|scripts.submit_feedback.main" + ] } }, "methods_index": { @@ -419,13 +435,6 @@ "end_line": 298, "type": "METHOD" }, - "scripts/diff_to_mermaid.py|scripts.diff_to_mermaid._Scope.resolve": { - "file_path": "scripts/diff_to_mermaid.py", - "qualified_name": "scripts.diff_to_mermaid._Scope.resolve", - "start_line": 300, - "end_line": 309, - "type": "METHOD" - }, "scripts/diff_to_mermaid.py|scripts.diff_to_mermaid._filter_changed": { "file_path": "scripts/diff_to_mermaid.py", "qualified_name": "scripts.diff_to_mermaid._filter_changed", @@ -537,41 +546,126 @@ "start_line": 156, "end_line": 192, "type": "FUNCTION" + }, + "scripts/submit_feedback.py|scripts.submit_feedback.telemetry_disabled": { + "file_path": "scripts/submit_feedback.py", + "qualified_name": "scripts.submit_feedback.telemetry_disabled", + "start_line": 27, + "end_line": 31, + "type": "FUNCTION" + }, + "scripts/submit_feedback.py|scripts.submit_feedback.resolve_key": { + "file_path": "scripts/submit_feedback.py", + "qualified_name": "scripts.submit_feedback.resolve_key", + "start_line": 34, + "end_line": 35, + "type": "FUNCTION" + }, + "scripts/submit_feedback.py|scripts.submit_feedback.resolve_host": { + "file_path": "scripts/submit_feedback.py", + "qualified_name": "scripts.submit_feedback.resolve_host", + "start_line": 38, + "end_line": 40, + "type": "FUNCTION" + }, + "scripts/submit_feedback.py|scripts.submit_feedback.resolve_command": { + "file_path": "scripts/submit_feedback.py", + "qualified_name": "scripts.submit_feedback.resolve_command", + "start_line": 43, + "end_line": 44, + "type": "FUNCTION" + }, + "scripts/submit_feedback.py|scripts.submit_feedback.resolve_max_chars": { + "file_path": "scripts/submit_feedback.py", + "qualified_name": "scripts.submit_feedback.resolve_max_chars", + "start_line": 47, + "end_line": 52, + "type": "FUNCTION" + }, + "scripts/submit_feedback.py|scripts.submit_feedback.extract_feedback": { + "file_path": "scripts/submit_feedback.py", + "qualified_name": "scripts.submit_feedback.extract_feedback", + "start_line": 55, + "end_line": 69, + "type": "FUNCTION" + }, + "scripts/submit_feedback.py|scripts.submit_feedback.cap_feedback": { + "file_path": "scripts/submit_feedback.py", + "qualified_name": "scripts.submit_feedback.cap_feedback", + "start_line": 72, + "end_line": 76, + "type": "FUNCTION" + }, + "scripts/submit_feedback.py|scripts.submit_feedback._first": { + "file_path": "scripts/submit_feedback.py", + "qualified_name": "scripts.submit_feedback._first", + "start_line": 79, + "end_line": 84, + "type": "FUNCTION" + }, + "scripts/submit_feedback.py|scripts.submit_feedback.distinct_id": { + "file_path": "scripts/submit_feedback.py", + "qualified_name": "scripts.submit_feedback.distinct_id", + "start_line": 87, + "end_line": 91, + "type": "FUNCTION" + }, + "scripts/submit_feedback.py|scripts.submit_feedback.build_properties": { + "file_path": "scripts/submit_feedback.py", + "qualified_name": "scripts.submit_feedback.build_properties", + "start_line": 94, + "end_line": 116, + "type": "FUNCTION" + }, + "scripts/submit_feedback.py|scripts.submit_feedback.build_payload": { + "file_path": "scripts/submit_feedback.py", + "qualified_name": "scripts.submit_feedback.build_payload", + "start_line": 119, + "end_line": 132, + "type": "FUNCTION" + }, + "scripts/submit_feedback.py|scripts.submit_feedback.post": { + "file_path": "scripts/submit_feedback.py", + "qualified_name": "scripts.submit_feedback.post", + "start_line": 135, + "end_line": 144, + "type": "FUNCTION" + }, + "scripts/submit_feedback.py|scripts.submit_feedback.main": { + "file_path": "scripts/submit_feedback.py", + "qualified_name": "scripts.submit_feedback.main", + "start_line": 147, + "end_line": 172, + "type": "FUNCTION" } }, "components": [ { - "name": "Action Orchestrator", - "description": "Acts as the central controller and entry point. It manages the GitHub Action lifecycle, validates the environment, and implements the logic to decide between a full re-analysis or an incremental update based on existing baselines.", + "name": "Analysis Orchestrator", + "description": "Acts as the central controller for the action. It validates the environment, manages the transition between full codebase scans and incremental PR analysis, and ensures metadata consistency across runs.", "key_entities": [ - { - "qualified_name": "scripts.engine_adapter.main", - "reference_file": "scripts/engine_adapter.py", - "reference_start_line": 488, - "reference_end_line": 558 - }, { "qualified_name": "scripts.engine_adapter.run_analyze", "reference_file": "scripts/engine_adapter.py", "reference_start_line": 327, "reference_end_line": 394 }, - { - "qualified_name": "scripts.engine_adapter._incremental_or_full", - "reference_file": "scripts/engine_adapter.py", - "reference_start_line": 252, - "reference_end_line": 300 - }, { "qualified_name": "scripts.engine_adapter.validate_base_analysis", "reference_file": "scripts/engine_adapter.py", "reference_start_line": 149, "reference_end_line": 204 + }, + { + "qualified_name": "scripts.engine_adapter._incremental_or_full", + "reference_file": "scripts/engine_adapter.py", + "reference_start_line": 252, + "reference_end_line": 300 } ], "source_cluster_ids": [ 1, - 7 + 8 ], "file_methods": [ { @@ -605,8 +699,8 @@ "can_expand": true, "components": [ { - "name": "Execution Lifecycle Controller", - "description": "Acts as the primary entry point and state machine for the GitHub Action, sequencing execution phases from setup to rendering.", + "name": "Execution Dispatcher", + "description": "Acts as the primary entry point and high-level coordinator. It sequences the execution phases (seed, analyze, render, health) and manages the global execution context for the GitHub Action.", "key_entities": [ { "qualified_name": "scripts.engine_adapter.main", @@ -646,26 +740,32 @@ "can_expand": true }, { - "name": "Strategy & Metadata Manager", - "description": "Implements the decision engine for incremental versus full analysis and manages persistence of architectural metadata.", + "name": "Analysis Strategy Manager", + "description": "Determines the optimal analysis mode (Full vs. Incremental) based on the Git context and manages the persistence of analysis metadata to ensure continuity between PR updates.", "key_entities": [ - { - "qualified_name": "scripts.engine_adapter._incremental_or_full", - "reference_file": "scripts/engine_adapter.py", - "reference_start_line": 252, - "reference_end_line": 300 - }, { "qualified_name": "scripts.engine_adapter.run_analyze", "reference_file": "scripts/engine_adapter.py", "reference_start_line": 327, "reference_end_line": 394 }, + { + "qualified_name": "scripts.engine_adapter._incremental_or_full", + "reference_file": "scripts/engine_adapter.py", + "reference_start_line": 252, + "reference_end_line": 300 + }, { "qualified_name": "scripts.engine_adapter._load_metadata", "reference_file": "scripts/engine_adapter.py", "reference_start_line": 77, "reference_end_line": 87 + }, + { + "qualified_name": "scripts.engine_adapter._metadata_commit", + "reference_file": "scripts/engine_adapter.py", + "reference_start_line": 97, + "reference_end_line": 99 } ], "source_cluster_ids": [ @@ -692,8 +792,8 @@ "can_expand": true }, { - "name": "Validation & Health Guardrails", - "description": "Ensures architectural synchronization integrity by validating baseline drift and generating health metrics.", + "name": "Quality & Environment Guard", + "description": "Validates the integrity of the analysis environment before execution and performs post-analysis health checks to report on the quality and coverage of the generated insights.", "key_entities": [ { "qualified_name": "scripts.engine_adapter.validate_base_analysis", @@ -738,78 +838,70 @@ ], "components_relations": [ { - "relation": "dispatches execution strategy requests to", - "src_name": "Execution Lifecycle Controller", - "dst_name": "Strategy & Metadata Manager", + "relation": "invokes to execute analysis phase", + "src_name": "Execution Dispatcher", + "dst_name": "Analysis Strategy Manager", "src_id": "1.1", "dst_id": "1.2", "edge_count": 5, "is_static": true }, { - "relation": "triggers pre/post-analysis checks via", - "src_name": "Execution Lifecycle Controller", - "dst_name": "Validation & Health Guardrails", + "relation": "triggers validation and health reporting", + "src_name": "Execution Dispatcher", + "dst_name": "Quality & Environment Guard", "src_id": "1.1", "dst_id": "1.3", "edge_count": 2, "is_static": true }, { - "relation": "provides analysis path to", - "src_name": "Strategy & Metadata Manager", - "dst_name": "Execution Lifecycle Controller", + "relation": "utilizes drift detection for baseline validation", + "src_name": "Analysis Strategy Manager", + "dst_name": "Quality & Environment Guard", "src_id": "1.2", - "dst_id": "1.1", - "edge_count": 0, - "is_static": false + "dst_id": "1.3", + "edge_count": 1, + "is_static": true }, { - "relation": "signals Go/No-Go status to", - "src_name": "Validation & Health Guardrails", - "dst_name": "Execution Lifecycle Controller", + "relation": "provides health metrics for metadata finalization", + "src_name": "Quality & Environment Guard", + "dst_name": "Analysis Strategy Manager", "src_id": "1.3", - "dst_id": "1.1", + "dst_id": "1.2", "edge_count": 0, "is_static": false - }, - { - "relation": "calls", - "src_name": "Strategy & Metadata Manager", - "dst_name": "Validation & Health Guardrails", - "src_id": "1.2", - "dst_id": "1.3", - "edge_count": 1, - "is_static": true } ] }, { - "name": "Structural Analyzer & Documenter", - "description": "Performs deep inspection of the codebase to map the project structure. It identifies components, methods, and file relationships, filtering for \"touched\" files to ensure only relevant changes are processed for documentation.", + "name": "Structural Diffing Engine", + "description": "Analyzes the codebase to identify structural modifications. It maps file-level changes to architectural components and extracts method-level differences to determine the scope of the impact.", "key_entities": [ { - "qualified_name": "scripts.build_component_files.render_component_files", - "reference_file": "scripts/build_component_files.py", - "reference_start_line": 124, - "reference_end_line": 175 + "qualified_name": "scripts.diff_to_mermaid.build_diff", + "reference_file": "scripts/diff_to_mermaid.py", + "reference_start_line": 210, + "reference_end_line": 217 }, { - "qualified_name": "scripts.build_component_files._walk", + "qualified_name": "scripts.build_component_files._changed_files_for", "reference_file": "scripts/build_component_files.py", - "reference_start_line": 56, - "reference_end_line": 62 + "reference_start_line": 88, + "reference_end_line": 101 }, { - "qualified_name": "scripts.diff_to_mermaid._filter_changed", - "reference_file": "scripts/diff_to_mermaid.py", - "reference_start_line": 312, - "reference_end_line": 354 + "qualified_name": "scripts.build_component_files._subtree_methods", + "reference_file": "scripts/build_component_files.py", + "reference_start_line": 77, + "reference_end_line": 85 } ], "source_cluster_ids": [ - 3, - 6 + 2, + 7, + 10 ], "file_methods": [ { @@ -819,20 +911,22 @@ "scripts.build_component_files._subtree_files", "scripts.build_component_files._subtree_methods", "scripts.build_component_files._changed_files_for", - "scripts.build_component_files._block", - "scripts.build_component_files.render_component_files" + "scripts.build_component_files.main" ] }, { "file_path": "scripts/diff_to_mermaid.py", "methods": [ - "scripts.diff_to_mermaid._comp_id", - "scripts.diff_to_mermaid._comp_name", - "scripts.diff_to_mermaid._sanitize", - "scripts.diff_to_mermaid._display_status", - "scripts.diff_to_mermaid._Scope.__init__", - "scripts.diff_to_mermaid._filter_changed", - "scripts.diff_to_mermaid._filter_changed.touches" + "scripts.diff_to_mermaid.load_analysis", + "scripts.diff_to_mermaid._file_methods", + "scripts.diff_to_mermaid._methods_by_file", + "scripts.diff_to_mermaid._has_structural_changes", + "scripts.diff_to_mermaid._has_method_changes", + "scripts.diff_to_mermaid._rel_key", + "scripts.diff_to_mermaid._diff_relations", + "scripts.diff_to_mermaid._diff_components", + "scripts.diff_to_mermaid.build_diff", + "scripts.diff_to_mermaid.main" ] } ], @@ -840,21 +934,44 @@ "can_expand": true, "components": [ { - "name": "Codebase Explorer & Metadata Extractor", - "description": "Performs the initial traversal of the project directory to identify files and extract granular metadata, such as method definitions and relationship keys.", - "key_entities": [], + "name": "Change Discovery & Scope Extractor", + "description": "Acts as the entry point for the engine by interfacing with the Git environment to identify modified files and drilling down into the specific code symbols affected within those files.", + "key_entities": [ + { + "qualified_name": "scripts.build_component_files._changed_files_for", + "reference_file": "scripts/build_component_files.py", + "reference_start_line": 88, + "reference_end_line": 101 + }, + { + "qualified_name": "scripts.build_component_files._subtree_methods", + "reference_file": "scripts/build_component_files.py", + "reference_start_line": 77, + "reference_end_line": 85 + }, + { + "qualified_name": "scripts.build_component_files.main", + "reference_file": "scripts/build_component_files.py", + "reference_start_line": 178, + "reference_end_line": 213 + } + ], "source_cluster_ids": [ - 9, - 10, - 11 + 0, + 1, + 2, + 3, + 4 ], "file_methods": [ { - "file_path": "scripts/diff_to_mermaid.py", + "file_path": "scripts/build_component_files.py", "methods": [ - "scripts.diff_to_mermaid._display_status", - "scripts.diff_to_mermaid._filter_changed", - "scripts.diff_to_mermaid._filter_changed.touches" + "scripts.build_component_files._walk", + "scripts.build_component_files._subtree_files", + "scripts.build_component_files._subtree_methods", + "scripts.build_component_files._changed_files_for", + "scripts.build_component_files.main" ] } ], @@ -862,39 +979,25 @@ "can_expand": true }, { - "name": "Change Detection Engine", - "description": "The core logic for incremental updates that compares the current structural state against a baseline to identify structural modifications.", - "key_entities": [ - { - "qualified_name": "scripts.diff_to_mermaid._filter_changed", - "reference_file": "scripts/diff_to_mermaid.py", - "reference_start_line": 312, - "reference_end_line": 354 - } - ], + "name": "Architectural Mapper & Identity Manager", + "description": "Translates physical file paths into logical architectural components, handling normalization, unique ID generation, and metadata management for component-level updates.", + "key_entities": [], "source_cluster_ids": [ - 3, - 4, 5, 6, 7, - 8 + 8, + 9 ], "file_methods": [ - { - "file_path": "scripts/build_component_files.py", - "methods": [ - "scripts.build_component_files._walk", - "scripts.build_component_files._subtree_methods", - "scripts.build_component_files.render_component_files" - ] - }, { "file_path": "scripts/diff_to_mermaid.py", "methods": [ - "scripts.diff_to_mermaid._comp_id", - "scripts.diff_to_mermaid._comp_name", - "scripts.diff_to_mermaid._Scope.__init__" + "scripts.diff_to_mermaid._file_methods", + "scripts.diff_to_mermaid._has_structural_changes", + "scripts.diff_to_mermaid._has_method_changes", + "scripts.diff_to_mermaid._diff_relations", + "scripts.diff_to_mermaid._diff_components" ] } ], @@ -902,41 +1005,38 @@ "can_expand": true }, { - "name": "Documentation Generator & Orchestrator", - "description": "Manages the high-level execution flow, renders component-level documentation, and generates final integration artifacts like GitHub CTA links.", + "name": "Impact Analyzer & Visualizer", + "description": "Compares component states before and after changes to determine modification types and aggregates findings into visual Mermaid.js flow and status reports.", "key_entities": [ { - "qualified_name": "scripts.build_component_files.render_component_files", - "reference_file": "scripts/build_component_files.py", - "reference_start_line": 124, - "reference_end_line": 175 + "qualified_name": "scripts.diff_to_mermaid.build_diff", + "reference_file": "scripts/diff_to_mermaid.py", + "reference_start_line": 210, + "reference_end_line": 217 }, { - "qualified_name": "scripts.build_component_files._walk", - "reference_file": "scripts/build_component_files.py", - "reference_start_line": 56, - "reference_end_line": 62 + "qualified_name": "scripts.diff_to_mermaid._diff_components", + "reference_file": "scripts/diff_to_mermaid.py", + "reference_start_line": 162, + "reference_end_line": 207 } ], "source_cluster_ids": [ - 0, - 1, - 2, - 12 + 10, + 11, + 12, + 13, + 14 ], "file_methods": [ - { - "file_path": "scripts/build_component_files.py", - "methods": [ - "scripts.build_component_files._subtree_files", - "scripts.build_component_files._changed_files_for", - "scripts.build_component_files._block" - ] - }, { "file_path": "scripts/diff_to_mermaid.py", "methods": [ - "scripts.diff_to_mermaid._sanitize" + "scripts.diff_to_mermaid.load_analysis", + "scripts.diff_to_mermaid._methods_by_file", + "scripts.diff_to_mermaid._rel_key", + "scripts.diff_to_mermaid.build_diff", + "scripts.diff_to_mermaid.main" ] } ], @@ -946,105 +1046,100 @@ ], "components_relations": [ { - "relation": "Supplies structured method lists and relationship keys", - "src_name": "Codebase Explorer & Metadata Extractor", - "dst_name": "Change Detection Engine", + "relation": "Passes identified file paths and method-level scopes to be resolved into logical component identities.", + "src_name": "Change Discovery & Scope Extractor", + "dst_name": "Architectural Mapper & Identity Manager", "src_id": "2.1", "dst_id": "2.2", - "edge_count": 2, - "is_static": true + "edge_count": 0, + "is_static": false }, { - "relation": "Provides filtered list of touched components and structural diffs", - "src_name": "Change Detection Engine", - "dst_name": "Documentation Generator & Orchestrator", + "relation": "Provides the mapped component metadata and sanitized names required to build the structural comparison.", + "src_name": "Architectural Mapper & Identity Manager", + "dst_name": "Impact Analyzer & Visualizer", "src_id": "2.2", "dst_id": "2.3", - "edge_count": 3, + "edge_count": 2, "is_static": true }, { - "relation": "Provides raw file system walk and component mapping", - "src_name": "Codebase Explorer & Metadata Extractor", - "dst_name": "Documentation Generator & Orchestrator", + "relation": "Supplies the raw change data used to evaluate _has_changes logic during the final visualization phase.", + "src_name": "Change Discovery & Scope Extractor", + "dst_name": "Impact Analyzer & Visualizer", "src_id": "2.1", "dst_id": "2.3", - "edge_count": 0, - "is_static": false - }, - { - "relation": "calls", - "src_name": "Change Detection Engine", - "dst_name": "Codebase Explorer & Metadata Extractor", - "src_id": "2.2", - "dst_id": "2.1", - "edge_count": 2, + "edge_count": 3, "is_static": true }, { "relation": "calls", - "src_name": "Documentation Generator & Orchestrator", - "dst_name": "Change Detection Engine", + "src_name": "Impact Analyzer & Visualizer", + "dst_name": "Architectural Mapper & Identity Manager", "src_id": "2.3", "dst_id": "2.2", - "edge_count": 2, + "edge_count": 3, "is_static": true } ] }, { - "name": "Diffing & Visualization Engine", - "description": "The logic core that compares the current structural analysis against the baseline. It identifies additions, deletions, and modifications in the architecture and translates these changes into Mermaid.js syntax for visual rendering.", + "name": "Mermaid Visualization Engine", + "description": "Translates the structural diff data into Mermaid.js syntax. It handles the recursive rendering of nested components, applies status indicators (added/modified/deleted), and filters noise to ensure diagram readability.", "key_entities": [ - { - "qualified_name": "scripts.diff_to_mermaid.build_diff", - "reference_file": "scripts/diff_to_mermaid.py", - "reference_start_line": 210, - "reference_end_line": 217 - }, { "qualified_name": "scripts.diff_to_mermaid.render_mermaid", "reference_file": "scripts/diff_to_mermaid.py", "reference_start_line": 396, "reference_end_line": 521 + }, + { + "qualified_name": "scripts.diff_to_mermaid.render_mermaid.build.emit_level", + "reference_file": "scripts/diff_to_mermaid.py", + "reference_start_line": 449, + "reference_end_line": 466 + }, + { + "qualified_name": "scripts.build_component_files.render_component_files", + "reference_file": "scripts/build_component_files.py", + "reference_start_line": 124, + "reference_end_line": 175 } ], "source_cluster_ids": [ - 2, - 4 + 3, + 5, + 6 ], "file_methods": [ { "file_path": "scripts/build_component_files.py", "methods": [ - "scripts.build_component_files.main" + "scripts.build_component_files._block", + "scripts.build_component_files.render_component_files" ] }, { "file_path": "scripts/diff_to_mermaid.py", "methods": [ - "scripts.diff_to_mermaid.load_analysis", - "scripts.diff_to_mermaid._file_methods", - "scripts.diff_to_mermaid._methods_by_file", - "scripts.diff_to_mermaid._has_structural_changes", - "scripts.diff_to_mermaid._has_method_changes", - "scripts.diff_to_mermaid._rel_key", - "scripts.diff_to_mermaid._diff_relations", + "scripts.diff_to_mermaid._comp_id", + "scripts.diff_to_mermaid._comp_name", "scripts.diff_to_mermaid._has_changes", - "scripts.diff_to_mermaid._diff_components", - "scripts.diff_to_mermaid.build_diff", + "scripts.diff_to_mermaid._sanitize", "scripts.diff_to_mermaid._esc", "scripts.diff_to_mermaid._truncate", + "scripts.diff_to_mermaid._display_status", "scripts.diff_to_mermaid._Scope", - "scripts.diff_to_mermaid._Scope.resolve", + "scripts.diff_to_mermaid._Scope.__init__", + "scripts.diff_to_mermaid._filter_changed", + "scripts.diff_to_mermaid._filter_changed.touches", "scripts.diff_to_mermaid._init_directive", "scripts.diff_to_mermaid._count_changed_components", "scripts.diff_to_mermaid._has_changed_relations", "scripts.diff_to_mermaid.render_mermaid", "scripts.diff_to_mermaid.render_mermaid.build", "scripts.diff_to_mermaid.render_mermaid.build.emit_edges", - "scripts.diff_to_mermaid.render_mermaid.build.emit_level", - "scripts.diff_to_mermaid.main" + "scripts.diff_to_mermaid.render_mermaid.build.emit_level" ] } ], @@ -1052,62 +1147,30 @@ "can_expand": true, "components": [ { - "name": "Structural Diff Engine", - "description": "Identifies additions, deletions, and modifications by comparing the current architectural analysis against the baseline to calculate the delta.", + "name": "Diagram Orchestration & Change Detection", + "description": "Acts as the entry point for the visualization process, identifying components requiring re-rendering by analyzing Git diffs and managing the lifecycle of generated diagram files.", "key_entities": [ { - "qualified_name": "scripts.diff_to_mermaid.build_diff", - "reference_file": "scripts/diff_to_mermaid.py", - "reference_start_line": 210, - "reference_end_line": 217 - }, - { - "qualified_name": "scripts.diff_to_mermaid._count_changed_components", - "reference_file": "scripts/diff_to_mermaid.py", - "reference_start_line": 379, - "reference_end_line": 386 - }, - { - "qualified_name": "scripts.diff_to_mermaid._methods_by_file", - "reference_file": "scripts/diff_to_mermaid.py", - "reference_start_line": 72, - "reference_end_line": 79 + "qualified_name": "scripts.build_component_files.render_component_files", + "reference_file": "scripts/build_component_files.py", + "reference_start_line": 124, + "reference_end_line": 175 } ], "source_cluster_ids": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10 + 15, + 16, + 17, + 18 ], "file_methods": [ - { - "file_path": "scripts/build_component_files.py", - "methods": [ - "scripts.build_component_files.main" - ] - }, { "file_path": "scripts/diff_to_mermaid.py", "methods": [ - "scripts.diff_to_mermaid._file_methods", - "scripts.diff_to_mermaid._has_structural_changes", - "scripts.diff_to_mermaid._has_method_changes", - "scripts.diff_to_mermaid._diff_relations", - "scripts.diff_to_mermaid._has_changes", - "scripts.diff_to_mermaid._diff_components", - "scripts.diff_to_mermaid._esc", - "scripts.diff_to_mermaid._Scope", - "scripts.diff_to_mermaid._Scope.resolve", - "scripts.diff_to_mermaid._count_changed_components", - "scripts.diff_to_mermaid._has_changed_relations" + "scripts.diff_to_mermaid.render_mermaid", + "scripts.diff_to_mermaid.render_mermaid.build", + "scripts.diff_to_mermaid.render_mermaid.build.emit_edges", + "scripts.diff_to_mermaid.render_mermaid.build.emit_level" ] } ], @@ -1115,148 +1178,119 @@ "can_expand": true }, { - "name": "Mermaid Syntax Renderer", - "description": "Translates calculated differences into Mermaid.js class diagram syntax with visual styling based on change status.", + "name": "Recursive Syntax Generator", + "description": "The core logic engine that translates internal component models into Mermaid syntax through recursive traversal of nested structures.", "key_entities": [ { "qualified_name": "scripts.diff_to_mermaid.render_mermaid", "reference_file": "scripts/diff_to_mermaid.py", "reference_start_line": 396, "reference_end_line": 521 + }, + { + "qualified_name": "scripts.diff_to_mermaid.render_mermaid.build.emit_level", + "reference_file": "scripts/diff_to_mermaid.py", + "reference_start_line": 449, + "reference_end_line": 466 } ], "source_cluster_ids": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, 11, 12, 13, - 14, - 15, - 16, - 17 + 14 ], "file_methods": [ { - "file_path": "scripts/diff_to_mermaid.py", + "file_path": "scripts/build_component_files.py", "methods": [ - "scripts.diff_to_mermaid.load_analysis", - "scripts.diff_to_mermaid._methods_by_file", - "scripts.diff_to_mermaid._rel_key", - "scripts.diff_to_mermaid.build_diff", - "scripts.diff_to_mermaid._truncate", - "scripts.diff_to_mermaid._init_directive", - "scripts.diff_to_mermaid.main" + "scripts.build_component_files._block", + "scripts.build_component_files.render_component_files" ] - } - ], - "component_id": "3.2", - "can_expand": true - }, - { - "name": "Architectural Scope Manager", - "description": "Maintains hierarchical context and naming integrity to ensure components are uniquely identified and correctly nested.", - "key_entities": [ - { - "qualified_name": "scripts.diff_to_mermaid._Scope", - "reference_file": "scripts/diff_to_mermaid.py", - "reference_start_line": 265, - "reference_end_line": 309 - } - ], - "source_cluster_ids": [ - 18, - 19, - 20, - 21 - ], - "file_methods": [ + }, { "file_path": "scripts/diff_to_mermaid.py", "methods": [ - "scripts.diff_to_mermaid.render_mermaid", - "scripts.diff_to_mermaid.render_mermaid.build", - "scripts.diff_to_mermaid.render_mermaid.build.emit_edges", - "scripts.diff_to_mermaid.render_mermaid.build.emit_level" + "scripts.diff_to_mermaid._comp_id", + "scripts.diff_to_mermaid._comp_name", + "scripts.diff_to_mermaid._has_changes", + "scripts.diff_to_mermaid._sanitize", + "scripts.diff_to_mermaid._esc", + "scripts.diff_to_mermaid._truncate", + "scripts.diff_to_mermaid._display_status", + "scripts.diff_to_mermaid._Scope", + "scripts.diff_to_mermaid._Scope.__init__", + "scripts.diff_to_mermaid._filter_changed", + "scripts.diff_to_mermaid._filter_changed.touches", + "scripts.diff_to_mermaid._init_directive", + "scripts.diff_to_mermaid._count_changed_components", + "scripts.diff_to_mermaid._has_changed_relations" ] } ], - "component_id": "3.3", + "component_id": "3.2", "can_expand": true } ], "components_relations": [ { - "relation": "Passes calculated delta and statistics to", - "src_name": "Structural Diff Engine", - "dst_name": "Mermaid Syntax Renderer", + "relation": "Passes filtered component subtrees and change-sets to initiate the string-building process", + "src_name": "Diagram Orchestration & Change Detection", + "dst_name": "Recursive Syntax Generator", "src_id": "3.1", "dst_id": "3.2", - "edge_count": 4, - "is_static": true - }, - { - "relation": "Provides unique identifiers and context to", - "src_name": "Architectural Scope Manager", - "dst_name": "Structural Diff Engine", - "src_id": "3.3", - "dst_id": "3.1", - "edge_count": 5, - "is_static": true - }, - { - "relation": "Supplies sanitized names and nesting levels to", - "src_name": "Architectural Scope Manager", - "dst_name": "Mermaid Syntax Renderer", - "src_id": "3.3", - "dst_id": "3.2", - "edge_count": 2, - "is_static": true - }, - { - "relation": "calls", - "src_name": "Mermaid Syntax Renderer", - "dst_name": "Structural Diff Engine", - "src_id": "3.2", - "dst_id": "3.1", - "edge_count": 3, + "edge_count": 8, "is_static": true }, { - "relation": "calls", - "src_name": "Mermaid Syntax Renderer", - "dst_name": "Architectural Scope Manager", + "relation": "Uses internal recursion to handle nested architectural layers", + "src_name": "Recursive Syntax Generator", + "dst_name": "Recursive Syntax Generator", "src_id": "3.2", - "dst_id": "3.3", - "edge_count": 1, - "is_static": true + "dst_id": "3.2", + "edge_count": 0, + "is_static": false } ] }, { - "name": "Integration & Feedback Provider", - "description": "Finalizes the process by generating user-facing outputs. It creates the \"Call to Action\" (CTA) for PR comments, generates webview links for interactive diagrams, and detects local editor settings to provide deep-linking capabilities.", + "name": "UX & Integration Layer", + "description": "Manages the final presentation of data to the user, including GitHub comments, feedback loops, and external integrations. It handles telemetry and user feedback via PostHog, closing the feedback loop between the user and the tool. Key class/method: scripts.submit_feedback.py.", "key_entities": [ { - "qualified_name": "scripts.build_cta.build_cta", + "qualified_name": "scripts.build_cta.main", "reference_file": "scripts/build_cta.py", - "reference_start_line": 80, - "reference_end_line": 137 + "reference_start_line": 156, + "reference_end_line": 192 }, { - "qualified_name": "scripts.build_cta.build_webview_link", + "qualified_name": "scripts.build_cta.detect_editors", "reference_file": "scripts/build_cta.py", - "reference_start_line": 62, - "reference_end_line": 77 + "reference_start_line": 38, + "reference_end_line": 50 }, { - "qualified_name": "scripts.build_cta.detect_editors", + "qualified_name": "scripts.build_cta.webview_url", "reference_file": "scripts/build_cta.py", - "reference_start_line": 36, - "reference_end_line": 48 + "reference_start_line": 64, + "reference_end_line": 79 } ], "source_cluster_ids": [ - 5, - 8 + 4, + 9, + 11 ], "file_methods": [ { @@ -1269,40 +1303,64 @@ "scripts.build_cta.build_cta.link", "scripts.build_cta.main" ] + }, + { + "file_path": "scripts/submit_feedback.py", + "methods": [ + "scripts.submit_feedback.telemetry_disabled", + "scripts.submit_feedback.resolve_key", + "scripts.submit_feedback.resolve_host", + "scripts.submit_feedback.resolve_command", + "scripts.submit_feedback.resolve_max_chars", + "scripts.submit_feedback.extract_feedback", + "scripts.submit_feedback.cap_feedback", + "scripts.submit_feedback._first", + "scripts.submit_feedback.distinct_id", + "scripts.submit_feedback.build_properties", + "scripts.submit_feedback.build_payload", + "scripts.submit_feedback.post", + "scripts.submit_feedback.main" + ] } ], "component_id": "4", "can_expand": true, "components": [ { - "name": "CTA Orchestrator", - "description": "Generates the final Markdown 'Call to Action' block for Pull Request comments by aggregating health status, interactive links, and instructional text.", - "key_entities": [ - { - "qualified_name": "scripts.build_cta.build_cta", - "reference_file": "scripts/build_cta.py", - "reference_start_line": 91, - "reference_end_line": 153 - }, - { - "qualified_name": "scripts.build_cta._join_or", - "reference_file": "scripts/build_cta.py", - "reference_start_line": 82, - "reference_end_line": 88 - } - ], + "name": "Visual Report Engine", + "description": "Translates structural analysis and diff data into human-readable formats, primarily Mermaid.js diagrams and component-level documentation.", + "key_entities": [], "source_cluster_ids": [ 0, 1, - 2 + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 ], "file_methods": [ { "file_path": "scripts/build_cta.py", "methods": [ + "scripts.build_cta.detect_editors", + "scripts.build_cta.webview_url", "scripts.build_cta._join_or", "scripts.build_cta.build_cta", - "scripts.build_cta.build_cta.link" + "scripts.build_cta.build_cta.link", + "scripts.build_cta.main" + ] + }, + { + "file_path": "scripts/submit_feedback.py", + "methods": [ + "scripts.submit_feedback.cap_feedback", + "scripts.submit_feedback._first", + "scripts.submit_feedback.build_properties", + "scripts.submit_feedback.build_payload" ] } ], @@ -1310,26 +1368,44 @@ "can_expand": true }, { - "name": "Environment Resolver", - "description": "Detects local developer configurations to enable 'Open in IDE' functionality by mapping editor markers to URI schemes.", + "name": "IDE & Environment Bridge", + "description": "Enhances generated reports with interactive Call-to-Action elements and generates deep links for local editor navigation.", "key_entities": [ + { + "qualified_name": "scripts.build_cta.main", + "reference_file": "scripts/build_cta.py", + "reference_start_line": 156, + "reference_end_line": 192 + }, { "qualified_name": "scripts.build_cta.detect_editors", "reference_file": "scripts/build_cta.py", "reference_start_line": 38, "reference_end_line": 50 + }, + { + "qualified_name": "scripts.build_cta.webview_url", + "reference_file": "scripts/build_cta.py", + "reference_start_line": 64, + "reference_end_line": 79 } ], "source_cluster_ids": [ - 3, - 4 + 10, + 11, + 12, + 13, + 14 ], "file_methods": [ { - "file_path": "scripts/build_cta.py", + "file_path": "scripts/submit_feedback.py", "methods": [ - "scripts.build_cta.detect_editors", - "scripts.build_cta.main" + "scripts.submit_feedback.resolve_command", + "scripts.submit_feedback.extract_feedback", + "scripts.submit_feedback.distinct_id", + "scripts.submit_feedback.post", + "scripts.submit_feedback.main" ] } ], @@ -1337,24 +1413,30 @@ "can_expand": true }, { - "name": "Webview Link Generator", - "description": "Constructs parameters and URLs for viewing architectural changes in the hosted CodeBoarding web application.", + "name": "Engagement & Telemetry Handler", + "description": "Manages the outbound feedback loop by collecting user interactions and submitting telemetry data to PostHog.", "key_entities": [ { - "qualified_name": "scripts.build_cta.webview_url", - "reference_file": "scripts/build_cta.py", - "reference_start_line": 64, - "reference_end_line": 79 + "qualified_name": "scripts.submit_feedback.main", + "reference_file": "scripts/submit_feedback.py", + "reference_start_line": 147, + "reference_end_line": 172 } ], "source_cluster_ids": [ - 5 + 15, + 16, + 17, + 18 ], "file_methods": [ { - "file_path": "scripts/build_cta.py", + "file_path": "scripts/submit_feedback.py", "methods": [ - "scripts.build_cta.webview_url" + "scripts.submit_feedback.telemetry_disabled", + "scripts.submit_feedback.resolve_key", + "scripts.submit_feedback.resolve_host", + "scripts.submit_feedback.resolve_max_chars" ] } ], @@ -1364,30 +1446,39 @@ ], "components_relations": [ { - "relation": "queries for local IDE markers", - "src_name": "CTA Orchestrator", - "dst_name": "Environment Resolver", + "relation": "provides payload for interactive CTAs", + "src_name": "Visual Report Engine", + "dst_name": "IDE & Environment Bridge", "src_id": "4.1", "dst_id": "4.2", - "edge_count": 1, + "edge_count": 3, "is_static": true }, { - "relation": "requests formatted visualization URLs", - "src_name": "CTA Orchestrator", - "dst_name": "Webview Link Generator", + "relation": "supplies context for feedback collection", + "src_name": "Visual Report Engine", + "dst_name": "Engagement & Telemetry Handler", "src_id": "4.1", "dst_id": "4.3", - "edge_count": 1, + "edge_count": 2, "is_static": true }, { - "relation": "provides context for deep-link generation", - "src_name": "Environment Resolver", - "dst_name": "CTA Orchestrator", + "relation": "triggers report payload construction", + "src_name": "IDE & Environment Bridge", + "dst_name": "Visual Report Engine", "src_id": "4.2", "dst_id": "4.1", - "edge_count": 1, + "edge_count": 2, + "is_static": true + }, + { + "relation": "configures telemetry settings and host routing", + "src_name": "IDE & Environment Bridge", + "dst_name": "Engagement & Telemetry Handler", + "src_id": "4.2", + "dst_id": "4.3", + "edge_count": 2, "is_static": true } ] @@ -1395,56 +1486,56 @@ ], "components_relations": [ { - "relation": "triggers structural mapping and file rendering", - "src_name": "Action Orchestrator", - "dst_name": "Structural Analyzer & Documenter", + "relation": "orchestrates codebase scanning and diff extraction", + "src_name": "Analysis Orchestrator", + "dst_name": "Structural Diffing Engine", "src_id": "1", "dst_id": "2", "edge_count": 0, "is_static": false }, { - "relation": "initiates baseline comparison and mermaid generation", - "src_name": "Action Orchestrator", - "dst_name": "Diffing & Visualization Engine", + "relation": "provides analysis metadata for final reporting", + "src_name": "Analysis Orchestrator", + "dst_name": "UX & Integration Layer", "src_id": "1", - "dst_id": "3", + "dst_id": "4", "edge_count": 0, "is_static": false }, { - "relation": "provides filtered change sets and method metadata", - "src_name": "Structural Analyzer & Documenter", - "dst_name": "Diffing & Visualization Engine", + "relation": "provides structural change data for diagram generation", + "src_name": "Structural Diffing Engine", + "dst_name": "Mermaid Visualization Engine", "src_id": "2", "dst_id": "3", - "edge_count": 2, + "edge_count": 4, "is_static": true }, { - "relation": "requests component naming and file-level filtering", - "src_name": "Diffing & Visualization Engine", - "dst_name": "Structural Analyzer & Documenter", + "relation": "queries specific file changes for component rendering", + "src_name": "Mermaid Visualization Engine", + "dst_name": "Structural Diffing Engine", "src_id": "3", "dst_id": "2", - "edge_count": 3, + "edge_count": 1, "is_static": true }, { - "relation": "provides visual syntax for report generation", - "src_name": "Diffing & Visualization Engine", - "dst_name": "Integration & Feedback Provider", + "relation": "provides rendered diagrams for GitHub comments", + "src_name": "Mermaid Visualization Engine", + "dst_name": "UX & Integration Layer", "src_id": "3", "dst_id": "4", "edge_count": 0, "is_static": false }, { - "relation": "delegates final CTA and webview link creation", - "src_name": "Action Orchestrator", - "dst_name": "Integration & Feedback Provider", - "src_id": "1", - "dst_id": "4", + "relation": "triggers feedback loops and telemetry based on run results", + "src_name": "UX & Integration Layer", + "dst_name": "Analysis Orchestrator", + "src_id": "4", + "dst_id": "1", "edge_count": 0, "is_static": false } diff --git a/.codeboarding/health/health_report.json b/.codeboarding/health/health_report.json index 460ad37..0786ac0 100644 --- a/.codeboarding/health/health_report.json +++ b/.codeboarding/health/health_report.json @@ -1,13 +1,13 @@ { "repository_name": "CodeBoarding-action", - "timestamp": "2026-06-13T22:34:02.126242+00:00", - "overall_score": 0.9997340425531914, + "timestamp": "2026-06-14T00:17:22.079366+00:00", + "overall_score": 0.9997797356828193, "check_summaries": [ { "check_name": "function_size", "description": "Checks that functions/methods do not exceed line count thresholds", "check_type": "standard", - "total_entities_checked": 62, + "total_entities_checked": 75, "findings_count": 0, "warning_count": 0, "score": 1.0, @@ -17,7 +17,7 @@ "check_name": "fan_out", "description": "Checks efferent coupling: how many other functions each function calls", "check_type": "standard", - "total_entities_checked": 62, + "total_entities_checked": 75, "findings_count": 0, "warning_count": 0, "score": 1.0, @@ -27,7 +27,7 @@ "check_name": "fan_in", "description": "Checks afferent coupling: how many other functions call each function", "check_type": "standard", - "total_entities_checked": 62, + "total_entities_checked": 75, "findings_count": 0, "warning_count": 0, "score": 1.0, diff --git a/README.md b/README.md index d60cc30..49ff795 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,14 @@ This table mirrors the engine and may lag it. The source of truth is the engine' The command needs the `issue_comment` trigger and runs from your default branch (a GitHub rule), so it only works once the workflow is merged there. On-demand runs on fork PRs are refused, so fork code is never analyzed with your secrets. +### Feedback command + +In review workflows that include `issue_comment`, anyone whose comment reaches the action can send product feedback with: + +```text +/codeboarding-feedback +``` + ## Keep your architecture versioned (sync mode) With `mode: sync`, the action analyzes the pushed commit and commits the results back to the branch (as `codeboarding[bot]`), so your architecture analysis stays versioned in git and tracks the code instead of drifting from it: diff --git a/action.yml b/action.yml index 1d57cea..8fedc23 100644 --- a/action.yml +++ b/action.yml @@ -70,6 +70,14 @@ inputs: description: 'Review mode: slash-command that triggers the action from a PR comment (issue_comment event). A comment whose first word is this runs the diagram on-demand.' required: false default: '/codeboarding' + feedback_command: + description: 'Review mode: slash-command for submitting explicit feedback to CodeBoarding. The command and following text are sent to CodeBoarding via PostHog (not anonymous telemetry, and no analysis runs). Opt out with CODEBOARDING_TELEMETRY=false or DO_NOT_TRACK=1.' + required: false + default: '/codeboarding-feedback' + feedback_max_chars: + description: 'Review mode: maximum number of feedback characters sent from /codeboarding-feedback (the text is truncated past this).' + required: false + default: '4000' mode: description: 'What the action does. "review" (default): post a Mermaid architecture-diff comment on the PR (pull_request / issue_comment events). "sync": analyze on push and commit the architecture (analysis.json + rendered docs) to target_branch, keeping it versioned and current (the baseline review mode diffs against). Events: push / workflow_dispatch / schedule. Run the two modes from separate workflow files with least-privilege permissions each.' required: false @@ -142,6 +150,26 @@ runs: COMMENT_BODY: ${{ github.event.comment.body }} AUTHOR_ASSOC: ${{ github.event.comment.author_association }} TRIGGER: ${{ inputs.trigger_command }} + # Feedback path (/codeboarding-feedback): a lightweight branch that sends + # the comment text to PostHog and exits before any checkout/engine setup. + # The script reads CODEBOARDING_POSTHOG_KEY/HOST and the opt-outs + # DO_NOT_TRACK / CODEBOARDING_TELEMETRY straight from the caller's env: a + # composite action inherits the calling workflow/job `env:` as real env + # vars, so those need no re-mapping here. POSTHOG_KEY/HOST below are a + # redundant alias the script only falls back to if the canonical vars are + # unset — kept as a hint that the destination is overridable. + FEEDBACK_COMMAND: ${{ inputs.feedback_command }} + FEEDBACK_MAX_CHARS: ${{ inputs.feedback_max_chars }} + POSTHOG_KEY: ${{ env.CODEBOARDING_POSTHOG_KEY }} + POSTHOG_HOST: ${{ env.CODEBOARDING_POSTHOG_HOST }} + SENDER_LOGIN: ${{ github.event.sender.login }} + SENDER_ID: ${{ github.event.sender.id }} + COMMENT_ID: ${{ github.event.comment.id }} + COMMENT_URL: ${{ github.event.comment.html_url }} + REPOSITORY_ID: ${{ github.event.repository.id }} + RUN_ATTEMPT: ${{ github.run_attempt }} + ACTION_PATH: ${{ github.action_path }} + ACTION_REF: ${{ github.action_ref || github.sha }} EVENT: ${{ github.event_name }} REPOSITORY: ${{ github.repository }} PR_NUMBER_PULL: ${{ github.event.pull_request.number }} @@ -243,6 +271,18 @@ runs: # word is the trigger; the payload lacks SHAs so we query the API. [ -n "$ISSUE_PR_URL" ] || skip "Comment is on a plain issue, not a PR." FIRST_WORD="$(printf '%s' "$COMMENT_BODY" | tr -d '\r' | awk 'NR==1{print $1; exit}')" + # Feedback command: forward the comment text to PostHog, then stop BEFORE + # the trusted-association check, checkout, engine setup, and LLM key. + # Sending user-written feedback runs no PR code, so it doesn't need the + # collaborator gate the analysis path does — the workflow's own `if:` + # decides who can reach this step. The script swallows its own failures + # and the guard has no `set -e`, so feedback can never block the action. + if [ "$FIRST_WORD" = "$FEEDBACK_COMMAND" ]; then + python3 "$ACTION_PATH/scripts/submit_feedback.py" + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "feedback_received=true" >> "$GITHUB_OUTPUT" + exit 0 + fi [ "$FIRST_WORD" = "$TRIGGER" ] || skip "Comment does not start with '$TRIGGER'." # SECURITY (pwn-request guard): issue_comment runs in the base repo WITH # secrets for ANY commenter. Only a trusted collaborator may trigger an @@ -281,6 +321,21 @@ runs: } >> "$GITHUB_OUTPUT" echo "Resolved PR #$PR_NUMBER (base=$BASE_REPO@$BASE_SHA head=$HEAD_REPO@$HEAD_SHA) via $EVENT" + # Feedback exits the guard with skip=true, so the analysis "Acknowledge + # command" step below (gated on skip != 'true') never fires for it; this one + # reacts instead, keyed on the feedback_received flag the guard set. + - name: Acknowledge feedback + if: steps.guard.outputs.feedback_received == 'true' + shell: bash + env: + GH_TOKEN: ${{ inputs.github_token }} + REPOSITORY: ${{ github.repository }} + COMMENT_ID: ${{ github.event.comment.id }} + run: | + # 👍 react to the feedback comment so the user knows it was received. + gh api -X POST "repos/${REPOSITORY}/issues/comments/${COMMENT_ID}/reactions" \ + -f content='+1' >/dev/null 2>&1 || true + - name: Acknowledge command if: steps.guard.outputs.skip != 'true' && github.event_name == 'issue_comment' shell: bash diff --git a/scripts/submit_feedback.py b/scripts/submit_feedback.py new file mode 100644 index 0000000..b0d9edc --- /dev/null +++ b/scripts/submit_feedback.py @@ -0,0 +1,176 @@ +"""Submit explicit user feedback (/codeboarding-feedback) to PostHog. + +Standard-library only, on purpose: this runs in the action's guard phase, before +the engine checkout and any dependency install, so it must not import third-party +packages. Unlike Core's anonymous telemetry, this event intentionally carries the +user-written feedback text and PR context — that difference is documented in the +README. All sending failures are swallowed; feedback must never break a PR. +""" + +from __future__ import annotations + +import json +import os +import sys +import urllib.error +import urllib.request + +# Public PostHog ingest key — the same write-only project key Core ships. +DEFAULT_POSTHOG_KEY = "phc_BQWpoXuPYQhW7mPWQcRv4yzSfuoAmh48EmXuUpeXPUB2" +DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com" +DEFAULT_COMMAND = "/codeboarding-feedback" +DEFAULT_MAX_CHARS = 4000 +EVENT_NAME = "codeboarding_feedback_submitted" +SOURCE = "github_action_feedback" + + +def telemetry_disabled(env: dict) -> bool: + """Mirror Core's opt-out: DO_NOT_TRACK or CODEBOARDING_TELEMETRY=false.""" + if env.get("DO_NOT_TRACK", "").strip().lower() in ("1", "true", "yes"): + return True + return env.get("CODEBOARDING_TELEMETRY", "true").strip().lower() == "false" + + +def resolve_key(env: dict) -> str: + return (env.get("CODEBOARDING_POSTHOG_KEY") or env.get("POSTHOG_KEY") or DEFAULT_POSTHOG_KEY).strip() + + +def resolve_host(env: dict) -> str: + host = (env.get("CODEBOARDING_POSTHOG_HOST") or env.get("POSTHOG_HOST") or DEFAULT_POSTHOG_HOST).strip() + return host.rstrip("/") or DEFAULT_POSTHOG_HOST + + +def resolve_command(env: dict) -> str: + return (env.get("FEEDBACK_COMMAND") or "").strip() or DEFAULT_COMMAND + + +def resolve_max_chars(env: dict) -> int: + try: + n = int((env.get("FEEDBACK_MAX_CHARS") or "").strip()) + except ValueError: + return DEFAULT_MAX_CHARS + return n if n > 0 else DEFAULT_MAX_CHARS + + +def extract_feedback(comment_body: str, command: str) -> str: + """Return everything after the leading command token, newlines preserved. + + The command is the first whitespace-delimited token of the comment. Only that + one token is removed; the remainder (including any later lines) is kept + verbatim, then outer whitespace is trimmed. Returns "" when the comment does + not actually start with the command, or carries no text after it. + """ + body = (comment_body or "").replace("\r\n", "\n").replace("\r", "\n").lstrip() + if not body: + return "" + parts = body.split(None, 1) # split once on the first run of whitespace + if parts[0] != command: + return "" + return parts[1].strip() if len(parts) > 1 else "" + + +def cap_feedback(text: str, max_chars: int) -> tuple[str, int, bool]: + """Return (capped_text, original_length, truncated).""" + original_length = len(text) + truncated = original_length > max_chars + return (text[:max_chars] if truncated else text), original_length, truncated + + +def _first(env: dict, *names: str) -> str: + for name in names: + value = (env.get(name) or "").strip() + if value: + return value + return "" + + +def distinct_id(env: dict) -> str: + sender_id = _first(env, "SENDER_ID") + if sender_id: + return f"github-user:{sender_id}" + return f"github-run:{_first(env, 'RUN_ID', 'GITHUB_RUN_ID')}" + + +def build_properties(env: dict, command: str, feedback_text: str, feedback_length: int, truncated: bool) -> dict: + props: dict = { + "source": SOURCE, + "command": command, + "feedback_text": feedback_text, + "feedback_length": feedback_length, + "feedback_truncated": truncated, + } + optional = { + "repository": _first(env, "REPOSITORY"), + "repository_id": _first(env, "REPOSITORY_ID"), + "pr_number": _first(env, "PR_NUMBER", "ISSUE_NUMBER"), + "comment_id": _first(env, "COMMENT_ID"), + "comment_url": _first(env, "COMMENT_URL"), + "author_association": _first(env, "AUTHOR_ASSOC", "AUTHOR_ASSOCIATION"), + "sender_login": _first(env, "SENDER_LOGIN"), + "sender_id": _first(env, "SENDER_ID"), + "run_id": _first(env, "RUN_ID", "GITHUB_RUN_ID"), + "run_attempt": _first(env, "RUN_ATTEMPT", "GITHUB_RUN_ATTEMPT"), + "action_ref": _first(env, "ACTION_REF", "GITHUB_ACTION_REF", "GITHUB_SHA"), + } + props.update({key: value for key, value in optional.items() if value}) + return props + + +def build_payload(env: dict) -> dict | None: + """Build the PostHog event payload, or None when there is nothing to send.""" + command = resolve_command(env) + feedback_text, feedback_length, truncated = cap_feedback( + extract_feedback(env.get("COMMENT_BODY", ""), command), resolve_max_chars(env) + ) + if not feedback_text: + return None + return { + "api_key": resolve_key(env), + "event": EVENT_NAME, + "distinct_id": distinct_id(env), + "properties": build_properties(env, command, feedback_text, feedback_length, truncated), + } + + +def post(payload: dict, host: str, timeout: int = 10) -> int: + """POST one event to PostHog's ingest endpoint; return the HTTP status.""" + request = urllib.request.Request( + f"{host}/i/v0/e/", + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(request, timeout=timeout) as response: + return response.status + + +def main(env: dict | None = None) -> int: + env = os.environ if env is None else env + + if telemetry_disabled(env): + print("Feedback disabled via DO_NOT_TRACK / CODEBOARDING_TELEMETRY; not sending.") + return 0 + + payload = build_payload(env) + if payload is None: + print("No feedback text after the command; nothing to send.") + return 0 + if not payload["api_key"]: + print("No PostHog key configured; skipping feedback send.") + return 0 + + truncated = payload["properties"].get("feedback_truncated") + try: + status = post(payload, resolve_host(env)) + print(f"Feedback submitted (HTTP {status}, truncated={truncated}).") + except urllib.error.HTTPError as exc: + print(f"Feedback endpoint returned HTTP {exc.code}; ignoring.") + except urllib.error.URLError as exc: + print(f"Feedback endpoint unreachable ({type(exc.reason).__name__}); ignoring.") + except Exception as exc: # never let feedback break the action + print(f"Feedback send failed ({type(exc).__name__}); ignoring.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_submit_feedback.py b/tests/test_submit_feedback.py new file mode 100644 index 0000000..29e1e75 --- /dev/null +++ b/tests/test_submit_feedback.py @@ -0,0 +1,196 @@ +"""Unit tests for scripts/submit_feedback.py — /codeboarding-feedback capture.""" + +import io +import json +import sys +import unittest +from contextlib import redirect_stdout +from pathlib import Path +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts")) +import submit_feedback as sf # noqa: E402 + +COMMAND = "/codeboarding-feedback" +HOST = "https://us.i.posthog.com" + + +def base_env(**overrides): + env = { + "COMMENT_BODY": f"{COMMAND} the diagram is great", + "FEEDBACK_COMMAND": COMMAND, + "REPOSITORY": "octo/repo", + "REPOSITORY_ID": "555", + "ISSUE_NUMBER": "42", + "COMMENT_ID": "99", + "COMMENT_URL": "https://github.com/octo/repo/pull/42#issuecomment-99", + "AUTHOR_ASSOC": "CONTRIBUTOR", + "SENDER_LOGIN": "octocat", + "SENDER_ID": "1234", + "GITHUB_RUN_ID": "777", + "RUN_ATTEMPT": "1", + "ACTION_REF": "v1", + } + env.update(overrides) + return env + + +class TestExtractFeedback(unittest.TestCase): + def test_extracts_text_after_command(self): + self.assertEqual(sf.extract_feedback(f"{COMMAND} hello there", COMMAND), "hello there") + + def test_preserves_multiline_feedback(self): + body = f"{COMMAND} first line\nsecond line\n\nfourth" + self.assertEqual(sf.extract_feedback(body, COMMAND), "first line\nsecond line\n\nfourth") + + def test_command_only_yields_empty(self): + self.assertEqual(sf.extract_feedback(COMMAND, COMMAND), "") + self.assertEqual(sf.extract_feedback(f"{COMMAND} ", COMMAND), "") + + def test_command_on_its_own_line_then_body(self): + self.assertEqual(sf.extract_feedback(f"{COMMAND}\nthe body", COMMAND), "the body") + + def test_leading_whitespace_and_crlf_normalized(self): + self.assertEqual(sf.extract_feedback(f" {COMMAND} a\r\nb\r\n", COMMAND), "a\nb") + + def test_wrong_command_yields_empty(self): + self.assertEqual(sf.extract_feedback("/codeboarding run it", COMMAND), "") + self.assertEqual(sf.extract_feedback(f"{COMMAND}-typo hi", COMMAND), "") + + +class TestCapFeedback(unittest.TestCase): + def test_short_text_not_truncated(self): + self.assertEqual(sf.cap_feedback("abc", 10), ("abc", 3, False)) + + def test_long_text_capped_and_marked(self): + capped, length, truncated = sf.cap_feedback("x" * 50, 10) + self.assertEqual(capped, "x" * 10) + self.assertEqual(length, 50) + self.assertTrue(truncated) + + +class TestOptOut(unittest.TestCase): + def test_do_not_track_disables(self): + self.assertTrue(sf.telemetry_disabled({"DO_NOT_TRACK": "1"})) + self.assertTrue(sf.telemetry_disabled({"DO_NOT_TRACK": "true"})) + + def test_codeboarding_telemetry_false_disables(self): + self.assertTrue(sf.telemetry_disabled({"CODEBOARDING_TELEMETRY": "false"})) + + def test_default_enabled(self): + self.assertFalse(sf.telemetry_disabled({})) + + +class TestResolvers(unittest.TestCase): + def test_key_and_host_defaults(self): + self.assertEqual(sf.resolve_key({}), sf.DEFAULT_POSTHOG_KEY) + self.assertEqual(sf.resolve_host({}), sf.DEFAULT_POSTHOG_HOST) + + def test_host_override_strips_trailing_slash(self): + self.assertEqual( + sf.resolve_host({"CODEBOARDING_POSTHOG_HOST": "https://eu.example.com/"}), "https://eu.example.com" + ) + + def test_max_chars_invalid_falls_back(self): + self.assertEqual(sf.resolve_max_chars({"FEEDBACK_MAX_CHARS": "nope"}), sf.DEFAULT_MAX_CHARS) + self.assertEqual(sf.resolve_max_chars({"FEEDBACK_MAX_CHARS": "0"}), sf.DEFAULT_MAX_CHARS) + self.assertEqual(sf.resolve_max_chars({"FEEDBACK_MAX_CHARS": "25"}), 25) + + def test_distinct_id_prefers_sender_then_run(self): + self.assertEqual(sf.distinct_id({"SENDER_ID": "5"}), "github-user:5") + self.assertEqual(sf.distinct_id({"GITHUB_RUN_ID": "9"}), "github-run:9") + + +class TestBuildPayload(unittest.TestCase): + def test_empty_feedback_returns_none(self): + self.assertIsNone(sf.build_payload(base_env(COMMENT_BODY=COMMAND))) + + def test_payload_shape(self): + payload = sf.build_payload(base_env()) + self.assertEqual(payload["event"], "codeboarding_feedback_submitted") + self.assertEqual(payload["distinct_id"], "github-user:1234") + self.assertEqual(payload["api_key"], sf.DEFAULT_POSTHOG_KEY) + props = payload["properties"] + self.assertEqual(props["source"], "github_action_feedback") + self.assertEqual(props["command"], COMMAND) + self.assertEqual(props["feedback_text"], "the diagram is great") + self.assertEqual(props["feedback_length"], len("the diagram is great")) + self.assertFalse(props["feedback_truncated"]) + self.assertEqual(props["repository"], "octo/repo") + self.assertEqual(props["repository_id"], "555") + self.assertEqual(props["pr_number"], "42") + self.assertEqual(props["comment_id"], "99") + self.assertEqual(props["author_association"], "CONTRIBUTOR") + self.assertEqual(props["sender_login"], "octocat") + self.assertEqual(props["run_id"], "777") + + def test_truncation_recorded_in_payload(self): + payload = sf.build_payload(base_env(COMMENT_BODY=f"{COMMAND} " + "y" * 50, FEEDBACK_MAX_CHARS="10")) + props = payload["properties"] + self.assertEqual(len(props["feedback_text"]), 10) + self.assertEqual(props["feedback_length"], 50) + self.assertTrue(props["feedback_truncated"]) + + def test_optional_props_omitted_when_absent(self): + payload = sf.build_payload({"COMMENT_BODY": f"{COMMAND} hi", "SENDER_ID": "1"}) + self.assertNotIn("repository", payload["properties"]) + self.assertNotIn("comment_url", payload["properties"]) + + +class TestMain(unittest.TestCase): + def _run(self, env): + with mock.patch.object(sf.urllib.request, "urlopen") as urlopen: + urlopen.return_value.__enter__.return_value.status = 200 + buf = io.StringIO() + with redirect_stdout(buf): + rc = sf.main(env) + return rc, urlopen, buf.getvalue() + + def test_sends_expected_json_shape(self): + rc, urlopen, _ = self._run(base_env()) + self.assertEqual(rc, 0) + urlopen.assert_called_once() + request = urlopen.call_args.args[0] + self.assertEqual(request.full_url, f"{HOST}/i/v0/e/") + self.assertEqual(request.get_method(), "POST") + self.assertEqual(request.headers.get("Content-type"), "application/json") + body = json.loads(request.data) + self.assertEqual(body["event"], "codeboarding_feedback_submitted") + self.assertEqual(body["distinct_id"], "github-user:1234") + self.assertEqual(body["properties"]["feedback_text"], "the diagram is great") + + def test_host_override_used(self): + _, urlopen, _ = self._run(base_env(CODEBOARDING_POSTHOG_HOST="https://eu.example.com")) + request = urlopen.call_args.args[0] + self.assertEqual(request.full_url, "https://eu.example.com/i/v0/e/") + + def test_do_not_track_skips_sending(self): + _, urlopen, out = self._run(base_env(DO_NOT_TRACK="1")) + urlopen.assert_not_called() + self.assertIn("disabled", out) + + def test_telemetry_false_skips_sending(self): + _, urlopen, _ = self._run(base_env(CODEBOARDING_TELEMETRY="false")) + urlopen.assert_not_called() + + def test_empty_feedback_not_sent(self): + _, urlopen, out = self._run(base_env(COMMENT_BODY=COMMAND)) + urlopen.assert_not_called() + self.assertIn("nothing to send", out) + + def test_does_not_print_feedback_text(self): + secret = "PLEASE_DO_NOT_LEAK_THIS_abc123" + _, _, out = self._run(base_env(COMMENT_BODY=f"{COMMAND} {secret}")) + self.assertNotIn(secret, out) + + def test_network_failure_is_swallowed(self): + with mock.patch.object(sf.urllib.request, "urlopen", side_effect=sf.urllib.error.URLError("down")): + buf = io.StringIO() + with redirect_stdout(buf): + rc = sf.main(base_env()) + self.assertEqual(rc, 0) + self.assertIn("ignoring", buf.getvalue()) + + +if __name__ == "__main__": + unittest.main()