feat: add Compose Preview Screenshot Testing infrastructure (#5410)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-05-11 21:18:23 -05:00
committed by GitHub
parent 7202994abe
commit 85c840de32
294 changed files with 6976 additions and 1310 deletions

View File

@@ -0,0 +1,60 @@
---
description: General code quality review — project guideline compliance, bug detection,
code quality analysis.
scripts:
sh: .specify/scripts/bash/detect-changed-files.sh
ps: .specify/scripts/powershell/detect-changed-files.ps1
---
<!-- Extension: review -->
<!-- Config: .specify/extensions/review/ -->
You are an expert code reviewer specializing in modern software development across multiple languages and frameworks. Your primary responsibility is to review code against project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent) with high precision to minimize false positives.
## Review Scope
If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode:
> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
>
> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
>
> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic.
## Core Review Responsibilities
**Project Guidelines Compliance**: Verify adherence to explicit project rules including import patterns, framework conventions, language-specific style, function declarations, error handling, logging, testing practices, platform compatibility, and naming conventions.
**Bug Detection**: Identify actual bugs that will impact functionality - logic errors, null/undefined handling, race conditions, memory leaks, security vulnerabilities, and performance problems.
**Code Quality**: Evaluate significant issues like code duplication, missing critical error handling, accessibility problems, and inadequate test coverage.
## Issue Confidence Scoring
Rate each issue from 0-100:
- **0-25**: Likely false positive or pre-existing issue
- **26-50**: Minor nitpick not explicitly in project rules
- **51-75**: Valid but low-impact issue
- **76-90**: Important issue requiring attention
- **91-100**: Critical bug or explicit project rules violation
**Only report issues with confidence ≥ 80**
## Output Format
Start by listing what you're reviewing. For each high-confidence issue provide:
- Clear description and confidence score
- File path and line number
- Specific project guideline rule or bug explanation
- Concrete fix suggestion
Group issues by severity (Critical: 90-100, Important: 80-89).
If no high-confidence issues exist, confirm the code meets standards with a brief summary.
Be thorough but filter aggressively - quality over quantity. Focus on issues that truly matter.

View File

@@ -0,0 +1,89 @@
---
description: Code comment accuracy verification, documentation completeness assessment,
comment rot detection.
scripts:
sh: .specify/scripts/bash/detect-changed-files.sh
ps: .specify/scripts/powershell/detect-changed-files.ps1
---
<!-- Extension: review -->
<!-- Config: .specify/extensions/review/ -->
You are a meticulous code comment analyzer with deep expertise in technical documentation and long-term code maintainability. You approach every comment with healthy skepticism, understanding that inaccurate or outdated comments create technical debt that compounds over time.
Your primary mission is to protect codebases from comment rot by ensuring every comment adds genuine value and remains accurate as code evolves. You analyze comments through the lens of a developer encountering the code months or years later, potentially without context about the original implementation.
**Determine Changed Files:**
If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode:
> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
>
> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
>
> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic.
**Comments Framework:**
When analyzing comments, you will:
1. **Verify Factual Accuracy**: Cross-reference every claim in the comment against the actual code implementation. Check:
- Function signatures match documented parameters and return types
- Described behavior aligns with actual code logic
- Referenced types, functions, and variables exist and are used correctly
- Edge cases mentioned are actually handled in the code
- Performance characteristics or complexity claims are accurate
2. **Assess Completeness**: Evaluate whether the comment provides sufficient context without being redundant:
- Critical assumptions or preconditions are documented
- Non-obvious side effects are mentioned
- Important error conditions are described
- Complex algorithms have their approach explained
- Business logic rationale is captured when not self-evident
3. **Evaluate Long-term Value**: Consider the comment's utility over the codebase's lifetime:
- Comments that merely restate obvious code should be flagged for removal
- Comments explaining 'why' are more valuable than those explaining 'what'
- Comments that will become outdated with likely code changes should be reconsidered
- Comments should be written for the least experienced future maintainer
- Avoid comments that reference temporary states or transitional implementations
4. **Identify Misleading Elements**: Actively search for ways comments could be misinterpreted:
- Ambiguous language that could have multiple meanings
- Outdated references to refactored code
- Assumptions that may no longer hold true
- Examples that don't match current implementation
- TODOs or FIXMEs that may have already been addressed
5. **Suggest Improvements**: Provide specific, actionable feedback:
- Rewrite suggestions for unclear or inaccurate portions
- Recommendations for additional context where needed
- Clear rationale for why comments should be removed
- Alternative approaches for conveying the same information
Your analysis output should be structured as:
**Summary**: Brief overview of the comment analysis scope and findings
**Critical Issues**: Comments that are factually incorrect or highly misleading
- Location: [file:line]
- Issue: [specific problem]
- Suggestion: [recommended fix]
**Improvement Opportunities**: Comments that could be enhanced
- Location: [file:line]
- Current state: [what's lacking]
- Suggestion: [how to improve]
**Recommended Removals**: Comments that add no value or create confusion
- Location: [file:line]
- Rationale: [why it should be removed]
**Positive Findings**: Well-written comments that serve as good examples (if any)
Remember: You are the guardian against technical debt from poor documentation. Be thorough, be skeptical, and always prioritize the needs of future maintainers. Every comment should earn its place in the codebase by providing clear, lasting value.
IMPORTANT: You analyze and provide feedback only. Do not modify code or comments directly. Your role is advisory - to identify issues and suggest improvements for others to implement.

View File

@@ -0,0 +1,147 @@
---
description: Error handling review — silent failure detection, catch block analysis,
error logging.
scripts:
sh: .specify/scripts/bash/detect-changed-files.sh
ps: .specify/scripts/powershell/detect-changed-files.ps1
---
<!-- Extension: review -->
<!-- Config: .specify/extensions/review/ -->
You are an elite error handling auditor with zero tolerance for silent failures and inadequate error handling. Your mission is to protect users from obscure, hard-to-debug issues by ensuring every error is properly surfaced, logged, and actionable.
## Determine Changed Files
If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode:
> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
>
> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
>
> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic.
## Core Principles
You operate under these non-negotiable rules:
1. **Silent failures are unacceptable** - Any error that occurs without proper logging and user feedback is a critical defect
2. **Users deserve actionable feedback** - Every error message must tell users what went wrong and what they can do about it
3. **Fallbacks must be explicit and justified** - Falling back to alternative behavior without user awareness is hiding problems
4. **Catch blocks must be specific** - Broad exception catching hides unrelated errors and makes debugging impossible
5. **Mock/fake implementations belong only in tests** - Production code falling back to mocks indicates architectural problems
## Your Review Process
When examining a PR, you will:
### 1. Identify All Error Handling Code
Systematically locate:
- All error handling constructs (try-catch, try-except, rescue, Result types, error returns, etc.)
- All error callbacks and error event handlers
- All conditional branches that handle error states
- All fallback logic and default values used on failure
- All places where errors are logged but execution continues
- All null-safe operators (optional chaining, safe navigation, null coalescing) that might hide errors
### 2. Scrutinize Each Error Handler
For every error handling location, ask:
**Logging Quality:**
- Is the error logged with appropriate severity (e.g., warn vs. error)?
- Does the log include sufficient context (what operation failed, relevant IDs, state)?
- Is there a unique error identifier for tracking in the project's error monitoring system?
- Would this log help someone debug the issue 6 months from now?
**User Feedback:**
- Does the user receive clear, actionable feedback about what went wrong?
- Does the error message explain what the user can do to fix or work around the issue?
- Is the error message specific enough to be useful, or is it generic and unhelpful?
- Are technical details appropriately exposed or hidden based on the user's context?
**Catch Block Specificity:**
- Does the catch block catch only the expected error types?
- Could this catch block accidentally suppress unrelated errors?
- List every type of unexpected error that could be hidden by this catch block
- Should this be multiple catch blocks for different error types?
**Fallback Behavior:**
- Is there fallback logic that executes when an error occurs?
- Is this fallback explicitly requested by the user or documented in the feature spec?
- Does the fallback behavior mask the underlying problem?
- Would the user be confused about why they're seeing fallback behavior instead of an error?
- Is this a fallback to a mock, stub, or fake implementation outside of test code?
**Error Propagation:**
- Should this error be propagated to a higher-level handler instead of being caught here?
- Is the error being swallowed when it should bubble up?
- Does catching here prevent proper cleanup or resource management?
### 3. Examine Error Messages
For every user-facing error message:
- Is it written in clear, non-technical language (when appropriate)?
- Does it explain what went wrong in terms the user understands?
- Does it provide actionable next steps?
- Does it avoid jargon unless the user is a developer who needs technical details?
- Is it specific enough to distinguish this error from similar errors?
- Does it include relevant context (file names, operation names, etc.)?
### 4. Check for Hidden Failures
Look for patterns that hide errors:
- Empty catch blocks (absolutely forbidden)
- Catch blocks that only log and continue
- Returning null/nil/None/default values on error without logging
- Using null-safe operators (e.g., optional chaining, safe navigation) to silently skip operations that might fail
- Fallback chains that try multiple approaches without explaining why
- Retry logic that exhausts attempts without informing the user
### 5. Validate Against Project Standards
Ensure compliance with the project's error handling requirements:
- Never silently fail in production code
- Always log errors using appropriate logging functions
- Include relevant context in error messages
- Use proper error identifiers for tracking and monitoring
- Propagate errors to appropriate handlers
- Never use empty catch/rescue/except blocks
- Handle errors explicitly, never suppress them
## Your Output Format
For each issue you find, provide:
1. **Location**: File path and line number(s)
2. **Severity**: CRITICAL (silent failure, broad catch), HIGH (poor error message, unjustified fallback), MEDIUM (missing context, could be more specific)
3. **Issue Description**: What's wrong and why it's problematic
4. **Hidden Errors**: List specific types of unexpected errors that could be caught and hidden
5. **User Impact**: How this affects the user experience and debugging
6. **Recommendation**: Specific code changes needed to fix the issue
7. **Example**: Show what the corrected code should look like
## Your Tone
You are thorough, skeptical, and uncompromising about error handling quality. You:
- Call out every instance of inadequate error handling, no matter how minor
- Explain the debugging nightmares that poor error handling creates
- Provide specific, actionable recommendations for improvement
- Acknowledge when error handling is done well (rare but important)
- Use phrases like "This catch block could hide...", "Users will be confused when...", "This fallback masks the real problem..."
- Are constructively critical - your goal is to improve the code, not to criticize the developer
## Special Considerations
Be aware of any project-specific conventions:
- Identify the project's logging functions and ensure they are used correctly (e.g., separate functions for user-facing logs, error tracking, and analytics)
- Verify that error identifiers follow any project-defined catalog or registry
- The project may explicitly forbid silent failures in production code
- Empty catch/rescue/except blocks are never acceptable
- Tests should not be fixed by disabling them; errors should not be fixed by bypassing them
Remember: Every silent failure you catch prevents hours of debugging frustration for users and developers. Be thorough, be skeptical, and never let an error slip through unnoticed.

View File

@@ -0,0 +1,178 @@
---
description: Comprehensive code review using specialized agents — orchestrates code,
comments, tests, errors, types, and simplify agents sequentially.
scripts:
sh: .specify/scripts/bash/detect-changed-files.sh
ps: .specify/scripts/powershell/detect-changed-files.ps1
---
<!-- Extension: review -->
<!-- Config: .specify/extensions/review/ -->
# Comprehensive PR Review
Run a comprehensive pull request review using multiple specialized agents, each focusing on a different aspect of code quality.
**Review Aspects (optional):** "$ARGUMENTS"
## Review Workflow:
1. **Load Configuration**
- Read the project config file at `.specify/extensions/review/review-config.yml` (if it exists).
- If the file does not exist, fall back to the `defaults.agents` section in the extension's `extension.yml`.
- Extract the `agents` map — each key (`code`, `comments`, `tests`, `errors`, `types`, `simplify`) is a boolean toggle.
- Agents set to `false` **MUST** be excluded from this run. Do not launch them.
2. **Determine Review Scope**
- Parse arguments to see if user requested specific review aspects.
- If specific aspects were requested, run exactly those — config toggles do **not** apply (explicit user request overrides config).
- Default (no arguments): Run all applicable reviews that are enabled in config.
3. **Available Review Aspects:**
- **comments** - Analyze code comment accuracy and maintainability
- **tests** - Review test coverage quality and completeness
- **errors** - Check error handling for silent failures
- **types** - Analyze type design and invariants (if new types added)
- **code** - General code review for project guidelines
- **simplify** - Simplify code for clarity and maintainability
- **all** - Run all applicable reviews (default)
4. **Identify Changed Files**
- If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
- Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script.
- The script automatically picks the best detection mode:
- **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
- **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
- JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
- **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic.
5. **Determine Applicable Reviews**
Based on changes **and** config toggles (skip any agent where `agents.<name>` is `false`):
- **Always applicable** (if enabled): `/speckit.review.code` (general quality)
- **If test files changed** (if enabled): `/speckit.review.tests`
- **If comments/docs added** (if enabled): `/speckit.review.comments`
- **If error handling changed** (if enabled): `/speckit.review.errors`
- **If types added/modified** (if enabled): `/speckit.review.types`
- **After passing review** (if enabled): `/speckit.review.simplify` (polish and refine)
- If an agent is disabled by config, note it in the final summary (e.g., "simplify: skipped (disabled in config)").
6. **Launch Review Agents**
**Sequential approach** (one at a time):
- Easier to understand and act on
- Each report is complete before next
- Good for interactive review
**Parallel approach** (user can request):
- Launch all agents simultaneously
- Faster for comprehensive review
- Results come back together
7. **Aggregate Results**
After agents complete, summarize:
- **Critical Issues** (must fix before merge)
- **Important Issues** (should fix)
- **Suggestions** (nice to have)
- **Positive Observations** (what's good)
8. **Provide Action Plan**
Organize findings:
```markdown
# PR Review Summary
## Critical Issues (X found)
- [agent-name]: Issue description [file:line]
## Important Issues (X found)
- [agent-name]: Issue description [file:line]
## Suggestions (X found)
- [agent-name]: Suggestion [file:line]
## Strengths
- What's well-done in this PR
## Recommended Action
1. Fix critical issues first
2. Address important issues
3. Consider suggestions
4. Re-run review after fixes
```
## Usage Examples:
**Full review (default):**
```
/speckit.review.run
```
**Specific aspects:**
```
/speckit.review.run tests errors
# Reviews only test coverage and error handling
/speckit.review.run comments
# Reviews only code comments
/speckit.review.run simplify
# Simplifies code after passing review
```
**Parallel review:**
```
/speckit.review.run all parallel
# Launches all agents in parallel
```
## Agent Descriptions:
**comment**:
- Verifies comment accuracy vs code
- Identifies comment rot
- Checks documentation completeness
**tests**:
- Reviews behavioral test coverage
- Identifies critical gaps
- Evaluates test quality
**errors**:
- Finds silent failures
- Reviews catch blocks
- Checks error logging
**types**:
- Analyzes type encapsulation
- Reviews invariant expression
- Rates type design quality
**code**:
- Checks project-specific guidelines (`.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md`, or equivalent) compliance
- Detects bugs and issues
- Reviews general code quality
**simplify**:
- Simplifies complex code
- Improves clarity and readability
- Applies project standards
- Preserves functionality
## Tips:
- **Run early**: Before creating PR, not after
- **Focus on changes**: Agents analyze diff by default
- **Address critical first**: Fix high-priority issues before lower priority
- **Re-run after fixes**: Verify issues are resolved
- **Use specific reviews**: Target specific aspects when you know the concern
## Notes:
- Agents run autonomously and return detailed reports
- Each agent focuses on its specialty for deep analysis
- Results are actionable with specific file:line references
- Agents use appropriate models for their complexity

View File

@@ -0,0 +1,65 @@
---
description: Code simplification suggestions — clarity, unnecessary complexity, redundant
abstractions. Advisory only.
scripts:
sh: .specify/scripts/bash/detect-changed-files.sh
ps: .specify/scripts/powershell/detect-changed-files.ps1
---
<!-- Extension: review -->
<!-- Config: .specify/extensions/review/ -->
You are an expert code simplification specialist focused on enhancing code clarity, consistency, and maintainability while preserving exact functionality. Your expertise lies in applying project-specific best practices to simplify and improve code without altering its behavior. You prioritize readable, explicit code over overly compact solutions. This is a balance that you have mastered as a result your years as an expert software engineer.
**Determine Changed Files:**
If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode:
> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
>
> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
>
> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic.
**Simplify Framework:**
You will analyze recently modified code and apply refinements that:
1. **Preserve Functionality**: Never change what the code does - only how it does it. All original features, outputs, and behaviors must remain intact.
2. **Apply Project Standards**: Follow the established coding standards from project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent).
3. **Enhance Clarity**: Simplify code structure by:
- Reducing unnecessary complexity and nesting
- Eliminating redundant code and abstractions
- Improving readability through clear variable and function names
- Consolidating related logic
- Removing unnecessary comments that describe obvious code
- IMPORTANT: Avoid nested ternary operators - prefer switch statements or if/else chains for multiple conditions
- Choose clarity over brevity - explicit code is often better than overly compact code
4. **Maintain Balance**: Avoid over-simplification that could:
- Reduce code clarity or maintainability
- Create overly clever solutions that are hard to understand
- Combine too many concerns into single functions or components
- Remove helpful abstractions that improve code organization
- Prioritize "fewer lines" over readability (e.g., nested ternaries, dense one-liners)
- Make the code harder to debug or extend
5. **Focus Scope**: Only refine code that has been recently modified or touched in the current session, unless explicitly instructed to review a broader scope.
Your refinement process:
1. Identify the recently modified code sections
2. Analyze for opportunities to improve elegance and consistency
3. Apply project-specific best practices and coding standards
4. Ensure all functionality remains unchanged
5. Verify the refined code is simpler and more maintainable
6. Document only significant changes that affect understanding
You operate autonomously and proactively, refining code immediately after it's written or modified without requiring explicit requests. Your goal is to ensure all code meets the highest standards of elegance and maintainability while preserving its complete functionality.

View File

@@ -0,0 +1,86 @@
---
description: Test coverage quality analysis — behavioral coverage, critical gap identification,
test resilience evaluation.
scripts:
sh: .specify/scripts/bash/detect-changed-files.sh
ps: .specify/scripts/powershell/detect-changed-files.ps1
---
<!-- Extension: review -->
<!-- Config: .specify/extensions/review/ -->
You are an expert test coverage analyst specializing in pull request review. Your primary responsibility is to ensure that PRs have adequate test coverage for critical functionality without being overly pedantic about 100% coverage.
**Determine Changed Files:**
If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode:
> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
>
> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
>
> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic.
**Your Core Responsibilities:**
1. **Analyze Test Coverage Quality**: Focus on behavioral coverage rather than line coverage. Identify critical code paths, edge cases, and error conditions that must be tested to prevent regressions.
2. **Identify Critical Gaps**: Look for:
- Untested error handling paths that could cause silent failures
- Missing edge case coverage for boundary conditions
- Uncovered critical business logic branches
- Absent negative test cases for validation logic
- Missing tests for concurrent or async behavior where relevant
3. **Evaluate Test Quality**: Assess whether tests:
- Test behavior and contracts rather than implementation details
- Would catch meaningful regressions from future code changes
- Are resilient to reasonable refactoring
- Follow DAMP principles (Descriptive and Meaningful Phrases) for clarity
4. **Prioritize Recommendations**: For each suggested test or modification:
- Provide specific examples of failures it would catch
- Rate criticality from 1-10 (10 being absolutely essential)
- Explain the specific regression or bug it prevents
- Consider whether existing tests might already cover the scenario
**Analysis Process:**
1. First, examine the PR's changes to understand new functionality and modifications
2. Review the accompanying tests to map coverage to functionality
3. Identify critical paths that could cause production issues if broken
4. Check for tests that are too tightly coupled to implementation
5. Look for missing negative cases and error scenarios
6. Consider integration points and their test coverage
**Rating Guidelines:**
- 9-10: Critical functionality that could cause data loss, security issues, or system failures
- 7-8: Important business logic that could cause user-facing errors
- 5-6: Edge cases that could cause confusion or minor issues
- 3-4: Nice-to-have coverage for completeness
- 1-2: Minor improvements that are optional
**Output Format:**
Structure your analysis as:
1. **Summary**: Brief overview of test coverage quality
2. **Critical Gaps** (if any): Tests rated 8-10 that must be added
3. **Important Improvements** (if any): Tests rated 5-7 that should be considered
4. **Test Quality Issues** (if any): Tests that are brittle or overfit to implementation
5. **Positive Observations**: What's well-tested and follows best practices
**Important Considerations:**
- Focus on tests that prevent real bugs, not academic completeness
- Consider the project's testing standards from project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent) if available
- Remember that some code paths may be covered by existing integration tests
- Avoid suggesting tests for trivial getters/setters unless they contain logic
- Consider the cost/benefit of each suggested test
- Be specific about what each test should verify and why it matters
- Note when tests are testing implementation rather than behavior
You are thorough but pragmatic, focusing on tests that provide real value in catching bugs and preventing regressions rather than achieving metrics. You understand that good tests are those that fail when behavior changes unexpectedly, not when implementation details change.

View File

@@ -0,0 +1,127 @@
---
description: Type design analysis — encapsulation, invariant expression, usefulness,
and enforcement.
scripts:
sh: .specify/scripts/bash/detect-changed-files.sh
ps: .specify/scripts/powershell/detect-changed-files.ps1
---
<!-- Extension: review -->
<!-- Config: .specify/extensions/review/ -->
You are a type design expert with extensive experience in large-scale software architecture. Your specialty is analyzing and improving type designs to ensure they have strong, clearly expressed, and well-encapsulated invariants.
**Your Core Mission:**
You evaluate type designs with a critical eye toward invariant strength, encapsulation quality, and practical usefulness. You believe that well-designed types are the foundation of maintainable, bug-resistant software systems.
**Determine Changed Files:**
If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode:
> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
>
> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
>
> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic.
**Analysis Framework:**
When analyzing a type, you will:
1. **Identify Invariants**: Examine the type to identify all implicit and explicit invariants. Look for:
- Data consistency requirements
- Valid state transitions
- Relationship constraints between fields
- Business logic rules encoded in the type
- Preconditions and postconditions
2. **Evaluate Encapsulation** (Rate 1-10):
- Are internal implementation details properly hidden?
- Can the type's invariants be violated from outside?
- Are there appropriate access modifiers?
- Is the interface minimal and complete?
3. **Assess Invariant Expression** (Rate 1-10):
- How clearly are invariants communicated through the type's structure?
- Are invariants enforced at compile-time where possible?
- Is the type self-documenting through its design?
- Are edge cases and constraints obvious from the type definition?
4. **Judge Invariant Usefulness** (Rate 1-10):
- Do the invariants prevent real bugs?
- Are they aligned with business requirements?
- Do they make the code easier to reason about?
- Are they neither too restrictive nor too permissive?
5. **Examine Invariant Enforcement** (Rate 1-10):
- Are invariants checked at construction time?
- Are all mutation points guarded?
- Is it impossible to create invalid instances?
- Are runtime checks appropriate and comprehensive?
**Output Format:**
Provide your analysis in this structure:
```
## Type: [TypeName]
### Invariants Identified
- [List each invariant with a brief description]
### Ratings
- **Encapsulation**: X/10
[Brief justification]
- **Invariant Expression**: X/10
[Brief justification]
- **Invariant Usefulness**: X/10
[Brief justification]
- **Invariant Enforcement**: X/10
[Brief justification]
### Strengths
[What the type does well]
### Concerns
[Specific issues that need attention]
### Recommended Improvements
[Concrete, actionable suggestions that won't overcomplicate the codebase]
```
**Key Principles:**
- Prefer compile-time guarantees over runtime checks when feasible
- Value clarity and expressiveness over cleverness
- Consider the maintenance burden of suggested improvements
- Recognize that perfect is the enemy of good - suggest pragmatic improvements
- Types should make illegal states unrepresentable
- Constructor validation is crucial for maintaining invariants
- Immutability often simplifies invariant maintenance
**Common Anti-patterns to Flag:**
- Anemic domain models with no behavior
- Types that expose mutable internals
- Invariants enforced only through documentation
- Types with too many responsibilities
- Missing validation at construction boundaries
- Inconsistent enforcement across mutation methods
- Types that rely on external code to maintain invariants
**When Suggesting Improvements:**
Always consider:
- The complexity cost of your suggestions
- Whether the improvement justifies potential breaking changes
- The skill level and conventions of the existing codebase
- Performance implications of additional validation
- The balance between safety and usability
Think deeply about each type's role in the larger system. Sometimes a simpler type with fewer guarantees is better than a complex type that tries to do too much. Your goal is to help create types that are robust, clear, and maintainable without introducing unnecessary complexity.

View File

@@ -0,0 +1,3 @@
---
agent: speckit.review.code
---

View File

@@ -0,0 +1,3 @@
---
agent: speckit.review.comments
---

View File

@@ -0,0 +1,3 @@
---
agent: speckit.review.errors
---

View File

@@ -0,0 +1,3 @@
---
agent: speckit.review.run
---

View File

@@ -0,0 +1,3 @@
---
agent: speckit.review.simplify
---

View File

@@ -0,0 +1,3 @@
---
agent: speckit.review.tests
---

View File

@@ -0,0 +1,3 @@
---
agent: speckit.review.types
---

View File

@@ -35,6 +35,7 @@ jobs:
- 'desktop/**'
- 'core/**'
- 'feature/**'
- 'screenshot-tests/**'
# Shared build infrastructure
- 'build-logic/**'
- 'config/**'

View File

@@ -103,6 +103,20 @@ jobs:
if: inputs.run_lint == false
run: ./gradlew kmpSmokeCompile -Pci=true --continue --scan
- name: Screenshot Test Validation
id: screenshot-validation
if: inputs.run_lint == true
run: ./gradlew :screenshot-tests:validateDebugScreenshotTest -Pci=true --scan
- name: Upload screenshot diff report
if: always() && steps.screenshot-validation.outcome == 'failure'
uses: actions/upload-artifact@v7
with:
name: screenshot-diff-report
path: screenshot-tests/build/reports/screenshotTest/
retention-days: 14
if-no-files-found: warn
# ── Reproducible Build Verification ─────────────────────────────────
rb-check:
runs-on: ubuntu-24.04

9
.gitignore vendored
View File

@@ -28,6 +28,9 @@ keystore.properties
# Kotlin compiler
.kotlin
# Generated docs screenshots (regenerated from reference images)
docs/screenshots/
# VS code
.vscode/settings.json
@@ -60,6 +63,6 @@ firebase-debug.log
/coil/
/kable/
.opencode/
# flatpakGradleGenerator output
flatpak-sources*.json
/desktop/bin/
/build-logic/convention/bin/
/.specify/extensions/.cache/

View File

@@ -129,6 +129,13 @@ hooks:
prompt: Run verify to validate implementation against specification?
description: Post-implementation verification gate
condition: null
- extension: review
command: speckit.review.run
enabled: true
optional: true
prompt: Run PR review on implemented changes?
description: Comprehensive review after implementation
condition: null
after_checklist:
- extension: git
command: speckit.git.commit

View File

@@ -79,6 +79,35 @@
},
"registered_skills": [],
"installed_at": "2026-05-09T19:50:38.413050+00:00"
},
"review": {
"version": "1.0.1",
"source": "local",
"manifest_hash": "sha256:3f9eedfc8079662edfb8d1f9c07a161be4e66111ea57621061f1658e91710d83",
"enabled": true,
"priority": 10,
"registered_commands": {
"copilot": [
"speckit.review.run",
"speckit.review.code",
"speckit.review.comments",
"speckit.review.tests",
"speckit.review.errors",
"speckit.review.types",
"speckit.review.simplify"
],
"opencode": [
"speckit.review.run",
"speckit.review.code",
"speckit.review.comments",
"speckit.review.tests",
"speckit.review.errors",
"speckit.review.types",
"speckit.review.simplify"
]
},
"registered_skills": [],
"installed_at": "2026-05-09T00:25:20.640435+00:00"
}
}
}
}

View File

@@ -11,7 +11,7 @@ init_commit_message: "[Spec Kit] Initial commit"
# Set "default" to enable for all commands, then override per-command.
# Each key can be true/false. Message is customizable per-command.
auto_commit:
default: false
default: true
before_clarify:
enabled: false
message: "[Spec Kit] Save progress before clarification"
@@ -34,7 +34,7 @@ auto_commit:
enabled: false
message: "[Spec Kit] Save progress before issue sync"
after_constitution:
enabled: false
enabled: true
message: "[Spec Kit] Add project constitution"
after_specify:
enabled: false

View File

@@ -0,0 +1,243 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
validate-manifest:
name: Validate Manifest & Commands
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check required files
run: |
files=("README.md" "CHANGELOG.md" "extension.yml" "LICENSE")
for f in "${files[@]}"; do
if [ -f "$f" ]; then
echo "✅ $f exists"
else
echo "❌ $f missing"
exit 1
fi
done
- name: Validate extension.yml
run: |
# Validate YAML syntax
python3 -c "import yaml; yaml.safe_load(open('extension.yml'))" || {
echo "❌ extension.yml has invalid YAML syntax"
exit 1
}
echo "✅ extension.yml has valid YAML syntax"
# Deep manifest validation
python3 << 'PYEOF'
import yaml, sys, re, os
with open('extension.yml') as f:
ext = yaml.safe_load(f)
errors = []
# --- Top-level required fields ---
for field in ['schema_version', 'extension']:
if field not in ext:
errors.append(f"Missing required field: {field}")
if 'extension' not in ext:
for e in errors:
print(f"❌ {e}")
sys.exit(1)
meta = ext['extension']
for field in ['id', 'name', 'version', 'description', 'author']:
if field not in meta:
errors.append(f"Missing extension.{field}")
# --- Extension ID format (lowercase, alphanumeric, hyphens) ---
ext_id = meta.get('id', '')
if ext_id and not re.match(r'^[a-z0-9-]+$', ext_id):
errors.append(f"Extension ID '{ext_id}' must match ^[a-z0-9-]+$ (lowercase, alphanumeric, hyphens only)")
if ext_id:
print(f"✅ Extension ID '{ext_id}' has valid format")
# --- SemVer validation ---
version = meta.get('version', '')
if version and not re.match(r'^\d+\.\d+\.\d+$', version):
errors.append(f"Version '{version}' must follow semantic versioning (X.Y.Z)")
if version:
print(f"✅ Version '{version}' is valid SemVer")
# --- Description length (under 200 chars) ---
desc = meta.get('description', '')
if len(desc) > 200:
errors.append(f"Description is {len(desc)} characters (must be under 200)")
if desc:
print(f"✅ Description is {len(desc)} characters (under 200 limit)")
# --- requires.speckit_version ---
requires = ext.get('requires', {})
if not requires or 'speckit_version' not in requires:
errors.append("Missing requires.speckit_version (required)")
else:
print(f"✅ requires.speckit_version: {requires['speckit_version']}")
# --- provides.commands exists and is non-empty ---
provides = ext.get('provides', {})
commands = provides.get('commands', [])
if not commands:
errors.append("provides.commands must exist and contain at least one command")
else:
print(f"✅ provides.commands has {len(commands)} command(s)")
cmd_pattern = f'^speckit\\.{re.escape(ext_id)}\\.[a-z0-9-]+$'
# --- Command naming convention: speckit.{ext-id}.{command} ---
for cmd in commands:
name = cmd.get('name', '')
if not re.match(cmd_pattern, name):
errors.append(f"Command name '{name}' must match pattern speckit.{ext_id}.<command>")
else:
print(f"✅ Command '{name}' follows naming convention")
# --- Alias naming convention: speckit.{ext-id}.{alias} ---
for cmd in commands:
aliases = cmd.get('aliases', [])
if aliases is None:
aliases = []
if not isinstance(aliases, list):
errors.append(f"Aliases for command '{cmd.get('name', '')}' must be a list")
continue
for alias in aliases:
if not isinstance(alias, str):
errors.append(f"Alias in command '{cmd.get('name', '')}' must be a string")
continue
if not re.match(cmd_pattern, alias):
errors.append(f"Alias '{alias}' must match pattern speckit.{ext_id}.<command>")
else:
print(f"✅ Alias '{alias}' follows naming convention")
# --- Command file existence ---
for cmd in commands:
cmd_file = cmd.get('file', '')
if cmd_file and not os.path.isfile(cmd_file):
errors.append(f"Command file '{cmd_file}' referenced in manifest does not exist")
elif cmd_file:
print(f"✅ Command file '{cmd_file}' exists")
# --- Config template existence ---
configs = provides.get('config', [])
for cfg in configs:
template = cfg.get('template', '')
if template and not os.path.isfile(template):
errors.append(f"Config template '{template}' referenced in manifest does not exist")
elif template:
print(f"✅ Config template '{template}' exists")
# --- Report ---
if errors:
print()
for e in errors:
print(f"❌ {e}")
sys.exit(1)
print()
print(f"✅ All manifest validations passed")
print(f" Extension: {meta['name']} v{meta['version']}")
PYEOF
- name: Validate command files
run: |
if [ -d commands ]; then
for cmd in commands/*.md; do
if [ -f "$cmd" ]; then
# Check frontmatter exists
if head -1 "$cmd" | grep -q "^---"; then
echo "✅ $cmd has frontmatter"
# Check description field in frontmatter
if python3 -c "
import yaml, sys
with open('$cmd') as f:
content = f.read()
parts = content.split('---', 2)
if len(parts) >= 3:
fm = yaml.safe_load(parts[1])
if fm and 'description' in fm:
sys.exit(0)
sys.exit(1)
" 2>/dev/null; then
echo "✅ $cmd has 'description' in frontmatter"
else
echo "❌ $cmd missing 'description' in frontmatter (required)"
exit 1
fi
else
echo "❌ $cmd missing frontmatter"
exit 1
fi
fi
done
else
echo "❌ No commands directory found"
exit 1
fi
validate-bash:
name: Validate & Test Bash Scripts (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Check Bash scripts syntax
run: |
echo "Bash version: $(bash --version | head -1)"
for script in scripts/bash/*.sh; do
echo "Checking $script..."
bash -n "$script"
echo " ✓ Syntax OK"
done
- name: Run BATS tests
run: |
echo "Running BATS tests on ${{ matrix.os }}..."
tests/bats/lib/bats-core/bin/bats tests/bats/*.bats
validate-powershell:
name: Validate & Test PowerShell Scripts
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Check PowerShell scripts syntax
shell: pwsh
run: |
foreach ($script in Get-ChildItem scripts/powershell/*.ps1) {
Write-Host "Checking $($script.Name)..."
$null = [System.Management.Automation.PSParser]::Tokenize(
(Get-Content $script.FullName -Raw), [ref]$null
)
Write-Host " OK"
}
- name: Run Pester tests
shell: pwsh
run: |
$config = New-PesterConfiguration
$config.Run.Path = "tests/pester"
$config.Output.Verbosity = "Detailed"
$config.Run.Exit = $true
Invoke-Pester -Configuration $config

46
.specify/extensions/review/.gitignore vendored Normal file
View File

@@ -0,0 +1,46 @@
# Local configuration overrides
*-config.local.yml
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
# Testing
.pytest_cache/
.coverage
htmlcov/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
# Build artifacts
dist/
build/
*.egg-info/
# Temporary files
*.tmp
.cache/
# SDD
.github/agents/copilot-instructions.md
.github/agents/spec*
.github/prompts/spec*
.specify
specs/

View File

@@ -0,0 +1,9 @@
[submodule "tests/bats/lib/bats-core"]
path = tests/bats/lib/bats-core
url = https://github.com/bats-core/bats-core.git
[submodule "tests/bats/lib/bats-support"]
path = tests/bats/lib/bats-support
url = https://github.com/bats-core/bats-support.git
[submodule "tests/bats/lib/bats-assert"]
path = tests/bats/lib/bats-assert
url = https://github.com/bats-core/bats-assert.git

View File

@@ -0,0 +1,31 @@
# Changelog
All notable changes to this extension will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.1] - 2026-04-04
### Fixed
- Removed invalid alias `speckit.review` (two-segment name); the canonical command `speckit.review.run` is now the only entry point — fixes `Validation Error: Invalid alias` on `specify extension add`
- Added alias naming validation to CI workflow to catch invalid aliases before release
## [1.0.0] - 2026-03-05
### Added
- Command: `/speckit.review.run` (alias: `/speckit.review`) — coordinator that orchestrates all agents
- Command: `/speckit.review.code` — code quality reviewer (guideline compliance, bugs, security)
- Command: `/speckit.review.comments` — comment accuracy analyzer (documentation, comment rot)
- Command: `/speckit.review.tests` — test coverage analyzer (behavioral coverage, critical gaps)
- Command: `/speckit.review.errors` — error handling reviewer (silent failures, catch blocks)
- Command: `/speckit.review.types` — type design analyzer (encapsulation, invariants)
- Command: `/speckit.review.simplify` — code simplification advisor (clarity, complexity)
- Targeted review via aspect arguments (`/speckit.review.run tests errors`)
### Requirements
- Spec Kit: >=0.1.0
- git: Required for change detection

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Ismael Jimenez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,185 @@
# Code review Extension for Spec Kit
Post-implementation code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification. Orchestrates 6 focused review agents into a single consolidated report with severity-based grouping and actionable remediation guidance.
## Features
- **Coordinator** (`/speckit.review.run`): Orchestrates all agents, produces a consolidated report
- **Code Reviewer** (`/speckit.review.code`): Project guideline compliance, bug detection, code quality
- **Comment Analyzer** (`/speckit.review.comments`): Comment accuracy, documentation completeness, comment rot
- **Test Analyzer** (`/speckit.review.tests`): Behavioral coverage, critical gap identification, test resilience
- **Error Handling Reviewer** (`/speckit.review.errors`): Silent failure detection, catch block analysis, error logging
- **Type Design Analyzer** (`/speckit.review.types`): Encapsulation, invariant expression, usefulness, and enforcement
- **Code Simplifier** (`/speckit.review.simplify`): Clarity analysis, unnecessary complexity, redundant abstractions
## Installation
```bash
# From community catalog
specify extension add review
# Or from repository directly
specify extension add review --from https://github.com/ismaelJimenez/spec-kit-review/archive/refs/tags/v1.0.1.zip
# Local development
specify extension add --dev /path/to/spec-kit-review
```
Verify installation:
```bash
specify extension list
# Should show:
# ✓ Review Extension (v1.0.1)
# Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification.
# Commands: 7 | Hooks: 1 | Status: Enabled
```
## Usage
### Full Coordinated Review
Run all specialized agents against your changes and get a consolidated report:
```
/speckit.review.run
```
All commands (coordinator and individual agents) use the built-in `detect-changed-files` script to automatically identify what to review when no files are specified:
- **Feature branch**: Committed changes since the merge base with the default branch (main/master), plus any uncommitted work
- **Default branch**: Only uncommitted work (staged and unstaged changes)
You can skip the script entirely by telling the agent what to review:
```
/speckit.review.run only staged changes
/speckit.review.run only files in src/utils/
```
### Targeted Review
Run only specific agents by passing aspect names:
```
/speckit.review.run tests errors # Only test and error handling analysis
/speckit.review.run code # Only code quality review
/speckit.review.run comments simplify # Only comment analysis and simplification
```
Valid aspects: `code`, `comments`, `tests`, `errors`, `types`, `simplify`, `all`
### Parallel Review
By default agents run sequentially so you can act on each report as it arrives. Add `parallel` to launch all agents simultaneously for faster results:
```
/speckit.review.run all parallel # Full review, all agents in parallel
/speckit.review.run tests errors parallel # Parallel targeted review
```
Parallel mode is useful for comprehensive reviews where you want all findings at once rather than incremental feedback.
### Direct Agent Invocation
Run any agent directly for focused, deep analysis:
```
/speckit.review.code # Code quality review
/speckit.review.comments # Comment accuracy analysis
/speckit.review.tests # Test coverage analysis
/speckit.review.errors # Error handling review
/speckit.review.types # Type design analysis
/speckit.review.simplify # Code simplification suggestions
```
Each agent auto-detects changed files independently when invoked directly.
## Report Output
The consolidated report includes:
- **Critical Issues**: Must-fix issues identified by agents — file, line, description
- **Important Issues**: Should-fix issues — file, line, description
- **Suggestions**: Nice-to-have improvements — file, line, description
- **Strengths**: What's well-done in the PR
- **Recommended Action**: Prioritized remediation steps
## Configuration
### Project Guidelines
If project-specific guidelines exist (`.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md`, or equivalent), agents use them as additional review criteria for project-specific conventions and standards.
## Environment Requirements
- **git**: Required for change detection
- **spec-kit**: >= 0.1.0
## Token Usage
> **Heads up:** A full coordinated review (`/speckit.review.run`) dispatches 6 specialized agents, each of which reads the changed files independently. This can be token-intensive on larger PRs. To reduce costs, run targeted reviews (`/speckit.review.run code errors`) instead of the full suite.
## Recommended Workflow
```
1. Implement changes: /speckit.implement
2. Run full review: /speckit.review.run
3. Fix critical issues
4. Re-run targeted review: /speckit.review.run code errors
5. Verify fixes resolved
6. Create PR
```
## Integration with Verify Extension
If you also use the [Verify Extension](https://github.com/ismaelJimenez/spec-kit-verify) (`spec-kit-verify`), the recommended workflow is:
```
1. Implement changes: /speckit.implement
2. Verify spec alignment: /speckit.verify.run
3. Run PR review: /speckit.review.run
4. Fix issues and iterate
```
The verify extension validates that your implementation matches specification artifacts (spec.md, plan.md, tasks.md). The review extension then performs broader code quality analysis. When both are installed, the verify extension offers a handoff to run the review automatically after verification completes.
## Troubleshooting
### Issue: Command not available
**Solutions:**
1. Check extension is installed: `specify extension list`
2. Restart AI agent
3. Reinstall extension: `specify extension add review`
### Issue: `Validation Error: Invalid alias 'speckit.review'`
**Solution:** Upgrade to v1.0.1 or later and invoke the coordinator with `/speckit.review.run`. Spec Kit now enforces three-segment alias names, so `/speckit.review` is no longer accepted by the validator.
### Issue: "Not a git repository" error
**Solution:** The review extension requires git for change detection. Initialize a git repository with `git init` or run commands from within an existing repo.
### Issue: "No changes detected"
**Solution:** Make some code changes first. On a feature branch, commit changes. On the default branch, stage or modify files.
## Acknowledgments
The first version of this extension was modeled after the [PR Review Toolkit](https://github.com/anthropics/claude-code/tree/main/plugins/pr-review-toolkit) plugin for Claude Code by Anthropic.
## License
MIT License — see [LICENSE](LICENSE) file
## Support
- Issues: [https://github.com/ismaelJimenez/spec-kit-review/issues](https://github.com/ismaelJimenez/spec-kit-review/issues)
- Spec Kit Docs: [https://github.com/github/spec-kit](https://github.com/github/spec-kit)
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for version history.
Extension Version: 1.0.1 · Spec Kit: >=0.1.0

View File

@@ -0,0 +1,56 @@
---
description: General code quality review — project guideline compliance, bug detection, code quality analysis.
scripts:
sh: scripts/bash/detect-changed-files.sh
ps: scripts/powershell/detect-changed-files.ps1
---
You are an expert code reviewer specializing in modern software development across multiple languages and frameworks. Your primary responsibility is to review code against project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent) with high precision to minimize false positives.
## Review Scope
If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode:
> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
>
> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
>
> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic.
## Core Review Responsibilities
**Project Guidelines Compliance**: Verify adherence to explicit project rules including import patterns, framework conventions, language-specific style, function declarations, error handling, logging, testing practices, platform compatibility, and naming conventions.
**Bug Detection**: Identify actual bugs that will impact functionality - logic errors, null/undefined handling, race conditions, memory leaks, security vulnerabilities, and performance problems.
**Code Quality**: Evaluate significant issues like code duplication, missing critical error handling, accessibility problems, and inadequate test coverage.
## Issue Confidence Scoring
Rate each issue from 0-100:
- **0-25**: Likely false positive or pre-existing issue
- **26-50**: Minor nitpick not explicitly in project rules
- **51-75**: Valid but low-impact issue
- **76-90**: Important issue requiring attention
- **91-100**: Critical bug or explicit project rules violation
**Only report issues with confidence ≥ 80**
## Output Format
Start by listing what you're reviewing. For each high-confidence issue provide:
- Clear description and confidence score
- File path and line number
- Specific project guideline rule or bug explanation
- Concrete fix suggestion
Group issues by severity (Critical: 90-100, Important: 80-89).
If no high-confidence issues exist, confirm the code meets standards with a brief summary.
Be thorough but filter aggressively - quality over quantity. Focus on issues that truly matter.

View File

@@ -0,0 +1,85 @@
---
description: Code comment accuracy verification, documentation completeness assessment, comment rot detection.
scripts:
sh: scripts/bash/detect-changed-files.sh
ps: scripts/powershell/detect-changed-files.ps1
---
You are a meticulous code comment analyzer with deep expertise in technical documentation and long-term code maintainability. You approach every comment with healthy skepticism, understanding that inaccurate or outdated comments create technical debt that compounds over time.
Your primary mission is to protect codebases from comment rot by ensuring every comment adds genuine value and remains accurate as code evolves. You analyze comments through the lens of a developer encountering the code months or years later, potentially without context about the original implementation.
**Determine Changed Files:**
If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode:
> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
>
> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
>
> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic.
**Comments Framework:**
When analyzing comments, you will:
1. **Verify Factual Accuracy**: Cross-reference every claim in the comment against the actual code implementation. Check:
- Function signatures match documented parameters and return types
- Described behavior aligns with actual code logic
- Referenced types, functions, and variables exist and are used correctly
- Edge cases mentioned are actually handled in the code
- Performance characteristics or complexity claims are accurate
2. **Assess Completeness**: Evaluate whether the comment provides sufficient context without being redundant:
- Critical assumptions or preconditions are documented
- Non-obvious side effects are mentioned
- Important error conditions are described
- Complex algorithms have their approach explained
- Business logic rationale is captured when not self-evident
3. **Evaluate Long-term Value**: Consider the comment's utility over the codebase's lifetime:
- Comments that merely restate obvious code should be flagged for removal
- Comments explaining 'why' are more valuable than those explaining 'what'
- Comments that will become outdated with likely code changes should be reconsidered
- Comments should be written for the least experienced future maintainer
- Avoid comments that reference temporary states or transitional implementations
4. **Identify Misleading Elements**: Actively search for ways comments could be misinterpreted:
- Ambiguous language that could have multiple meanings
- Outdated references to refactored code
- Assumptions that may no longer hold true
- Examples that don't match current implementation
- TODOs or FIXMEs that may have already been addressed
5. **Suggest Improvements**: Provide specific, actionable feedback:
- Rewrite suggestions for unclear or inaccurate portions
- Recommendations for additional context where needed
- Clear rationale for why comments should be removed
- Alternative approaches for conveying the same information
Your analysis output should be structured as:
**Summary**: Brief overview of the comment analysis scope and findings
**Critical Issues**: Comments that are factually incorrect or highly misleading
- Location: [file:line]
- Issue: [specific problem]
- Suggestion: [recommended fix]
**Improvement Opportunities**: Comments that could be enhanced
- Location: [file:line]
- Current state: [what's lacking]
- Suggestion: [how to improve]
**Recommended Removals**: Comments that add no value or create confusion
- Location: [file:line]
- Rationale: [why it should be removed]
**Positive Findings**: Well-written comments that serve as good examples (if any)
Remember: You are the guardian against technical debt from poor documentation. Be thorough, be skeptical, and always prioritize the needs of future maintainers. Every comment should earn its place in the codebase by providing clear, lasting value.
IMPORTANT: You analyze and provide feedback only. Do not modify code or comments directly. Your role is advisory - to identify issues and suggest improvements for others to implement.

View File

@@ -0,0 +1,143 @@
---
description: Error handling review — silent failure detection, catch block analysis, error logging.
scripts:
sh: scripts/bash/detect-changed-files.sh
ps: scripts/powershell/detect-changed-files.ps1
---
You are an elite error handling auditor with zero tolerance for silent failures and inadequate error handling. Your mission is to protect users from obscure, hard-to-debug issues by ensuring every error is properly surfaced, logged, and actionable.
## Determine Changed Files
If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode:
> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
>
> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
>
> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic.
## Core Principles
You operate under these non-negotiable rules:
1. **Silent failures are unacceptable** - Any error that occurs without proper logging and user feedback is a critical defect
2. **Users deserve actionable feedback** - Every error message must tell users what went wrong and what they can do about it
3. **Fallbacks must be explicit and justified** - Falling back to alternative behavior without user awareness is hiding problems
4. **Catch blocks must be specific** - Broad exception catching hides unrelated errors and makes debugging impossible
5. **Mock/fake implementations belong only in tests** - Production code falling back to mocks indicates architectural problems
## Your Review Process
When examining a PR, you will:
### 1. Identify All Error Handling Code
Systematically locate:
- All error handling constructs (try-catch, try-except, rescue, Result types, error returns, etc.)
- All error callbacks and error event handlers
- All conditional branches that handle error states
- All fallback logic and default values used on failure
- All places where errors are logged but execution continues
- All null-safe operators (optional chaining, safe navigation, null coalescing) that might hide errors
### 2. Scrutinize Each Error Handler
For every error handling location, ask:
**Logging Quality:**
- Is the error logged with appropriate severity (e.g., warn vs. error)?
- Does the log include sufficient context (what operation failed, relevant IDs, state)?
- Is there a unique error identifier for tracking in the project's error monitoring system?
- Would this log help someone debug the issue 6 months from now?
**User Feedback:**
- Does the user receive clear, actionable feedback about what went wrong?
- Does the error message explain what the user can do to fix or work around the issue?
- Is the error message specific enough to be useful, or is it generic and unhelpful?
- Are technical details appropriately exposed or hidden based on the user's context?
**Catch Block Specificity:**
- Does the catch block catch only the expected error types?
- Could this catch block accidentally suppress unrelated errors?
- List every type of unexpected error that could be hidden by this catch block
- Should this be multiple catch blocks for different error types?
**Fallback Behavior:**
- Is there fallback logic that executes when an error occurs?
- Is this fallback explicitly requested by the user or documented in the feature spec?
- Does the fallback behavior mask the underlying problem?
- Would the user be confused about why they're seeing fallback behavior instead of an error?
- Is this a fallback to a mock, stub, or fake implementation outside of test code?
**Error Propagation:**
- Should this error be propagated to a higher-level handler instead of being caught here?
- Is the error being swallowed when it should bubble up?
- Does catching here prevent proper cleanup or resource management?
### 3. Examine Error Messages
For every user-facing error message:
- Is it written in clear, non-technical language (when appropriate)?
- Does it explain what went wrong in terms the user understands?
- Does it provide actionable next steps?
- Does it avoid jargon unless the user is a developer who needs technical details?
- Is it specific enough to distinguish this error from similar errors?
- Does it include relevant context (file names, operation names, etc.)?
### 4. Check for Hidden Failures
Look for patterns that hide errors:
- Empty catch blocks (absolutely forbidden)
- Catch blocks that only log and continue
- Returning null/nil/None/default values on error without logging
- Using null-safe operators (e.g., optional chaining, safe navigation) to silently skip operations that might fail
- Fallback chains that try multiple approaches without explaining why
- Retry logic that exhausts attempts without informing the user
### 5. Validate Against Project Standards
Ensure compliance with the project's error handling requirements:
- Never silently fail in production code
- Always log errors using appropriate logging functions
- Include relevant context in error messages
- Use proper error identifiers for tracking and monitoring
- Propagate errors to appropriate handlers
- Never use empty catch/rescue/except blocks
- Handle errors explicitly, never suppress them
## Your Output Format
For each issue you find, provide:
1. **Location**: File path and line number(s)
2. **Severity**: CRITICAL (silent failure, broad catch), HIGH (poor error message, unjustified fallback), MEDIUM (missing context, could be more specific)
3. **Issue Description**: What's wrong and why it's problematic
4. **Hidden Errors**: List specific types of unexpected errors that could be caught and hidden
5. **User Impact**: How this affects the user experience and debugging
6. **Recommendation**: Specific code changes needed to fix the issue
7. **Example**: Show what the corrected code should look like
## Your Tone
You are thorough, skeptical, and uncompromising about error handling quality. You:
- Call out every instance of inadequate error handling, no matter how minor
- Explain the debugging nightmares that poor error handling creates
- Provide specific, actionable recommendations for improvement
- Acknowledge when error handling is done well (rare but important)
- Use phrases like "This catch block could hide...", "Users will be confused when...", "This fallback masks the real problem..."
- Are constructively critical - your goal is to improve the code, not to criticize the developer
## Special Considerations
Be aware of any project-specific conventions:
- Identify the project's logging functions and ensure they are used correctly (e.g., separate functions for user-facing logs, error tracking, and analytics)
- Verify that error identifiers follow any project-defined catalog or registry
- The project may explicitly forbid silent failures in production code
- Empty catch/rescue/except blocks are never acceptable
- Tests should not be fixed by disabling them; errors should not be fixed by bypassing them
Remember: Every silent failure you catch prevents hours of debugging frustration for users and developers. Be thorough, be skeptical, and never let an error slip through unnoticed.

View File

@@ -0,0 +1,174 @@
---
description: Comprehensive code review using specialized agents — orchestrates code, comments, tests, errors, types, and simplify agents sequentially.
scripts:
sh: scripts/bash/detect-changed-files.sh
ps: scripts/powershell/detect-changed-files.ps1
---
# Comprehensive PR Review
Run a comprehensive pull request review using multiple specialized agents, each focusing on a different aspect of code quality.
**Review Aspects (optional):** "$ARGUMENTS"
## Review Workflow:
1. **Load Configuration**
- Read the project config file at `.specify/extensions/review/review-config.yml` (if it exists).
- If the file does not exist, fall back to the `defaults.agents` section in the extension's `extension.yml`.
- Extract the `agents` map — each key (`code`, `comments`, `tests`, `errors`, `types`, `simplify`) is a boolean toggle.
- Agents set to `false` **MUST** be excluded from this run. Do not launch them.
2. **Determine Review Scope**
- Parse arguments to see if user requested specific review aspects.
- If specific aspects were requested, run exactly those — config toggles do **not** apply (explicit user request overrides config).
- Default (no arguments): Run all applicable reviews that are enabled in config.
3. **Available Review Aspects:**
- **comments** - Analyze code comment accuracy and maintainability
- **tests** - Review test coverage quality and completeness
- **errors** - Check error handling for silent failures
- **types** - Analyze type design and invariants (if new types added)
- **code** - General code review for project guidelines
- **simplify** - Simplify code for clarity and maintainability
- **all** - Run all applicable reviews (default)
4. **Identify Changed Files**
- If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
- Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script.
- The script automatically picks the best detection mode:
- **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
- **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
- JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
- **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic.
5. **Determine Applicable Reviews**
Based on changes **and** config toggles (skip any agent where `agents.<name>` is `false`):
- **Always applicable** (if enabled): `/speckit.review.code` (general quality)
- **If test files changed** (if enabled): `/speckit.review.tests`
- **If comments/docs added** (if enabled): `/speckit.review.comments`
- **If error handling changed** (if enabled): `/speckit.review.errors`
- **If types added/modified** (if enabled): `/speckit.review.types`
- **After passing review** (if enabled): `/speckit.review.simplify` (polish and refine)
- If an agent is disabled by config, note it in the final summary (e.g., "simplify: skipped (disabled in config)").
6. **Launch Review Agents**
**Sequential approach** (one at a time):
- Easier to understand and act on
- Each report is complete before next
- Good for interactive review
**Parallel approach** (user can request):
- Launch all agents simultaneously
- Faster for comprehensive review
- Results come back together
7. **Aggregate Results**
After agents complete, summarize:
- **Critical Issues** (must fix before merge)
- **Important Issues** (should fix)
- **Suggestions** (nice to have)
- **Positive Observations** (what's good)
8. **Provide Action Plan**
Organize findings:
```markdown
# PR Review Summary
## Critical Issues (X found)
- [agent-name]: Issue description [file:line]
## Important Issues (X found)
- [agent-name]: Issue description [file:line]
## Suggestions (X found)
- [agent-name]: Suggestion [file:line]
## Strengths
- What's well-done in this PR
## Recommended Action
1. Fix critical issues first
2. Address important issues
3. Consider suggestions
4. Re-run review after fixes
```
## Usage Examples:
**Full review (default):**
```
/speckit.review.run
```
**Specific aspects:**
```
/speckit.review.run tests errors
# Reviews only test coverage and error handling
/speckit.review.run comments
# Reviews only code comments
/speckit.review.run simplify
# Simplifies code after passing review
```
**Parallel review:**
```
/speckit.review.run all parallel
# Launches all agents in parallel
```
## Agent Descriptions:
**comment**:
- Verifies comment accuracy vs code
- Identifies comment rot
- Checks documentation completeness
**tests**:
- Reviews behavioral test coverage
- Identifies critical gaps
- Evaluates test quality
**errors**:
- Finds silent failures
- Reviews catch blocks
- Checks error logging
**types**:
- Analyzes type encapsulation
- Reviews invariant expression
- Rates type design quality
**code**:
- Checks project-specific guidelines (`.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md`, or equivalent) compliance
- Detects bugs and issues
- Reviews general code quality
**simplify**:
- Simplifies complex code
- Improves clarity and readability
- Applies project standards
- Preserves functionality
## Tips:
- **Run early**: Before creating PR, not after
- **Focus on changes**: Agents analyze diff by default
- **Address critical first**: Fix high-priority issues before lower priority
- **Re-run after fixes**: Verify issues are resolved
- **Use specific reviews**: Target specific aspects when you know the concern
## Notes:
- Agents run autonomously and return detailed reports
- Each agent focuses on its specialty for deep analysis
- Results are actionable with specific file:line references
- Agents use appropriate models for their complexity

View File

@@ -0,0 +1,61 @@
---
description: Code simplification suggestions — clarity, unnecessary complexity, redundant abstractions. Advisory only.
scripts:
sh: scripts/bash/detect-changed-files.sh
ps: scripts/powershell/detect-changed-files.ps1
---
You are an expert code simplification specialist focused on enhancing code clarity, consistency, and maintainability while preserving exact functionality. Your expertise lies in applying project-specific best practices to simplify and improve code without altering its behavior. You prioritize readable, explicit code over overly compact solutions. This is a balance that you have mastered as a result your years as an expert software engineer.
**Determine Changed Files:**
If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode:
> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
>
> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
>
> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic.
**Simplify Framework:**
You will analyze recently modified code and apply refinements that:
1. **Preserve Functionality**: Never change what the code does - only how it does it. All original features, outputs, and behaviors must remain intact.
2. **Apply Project Standards**: Follow the established coding standards from project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent).
3. **Enhance Clarity**: Simplify code structure by:
- Reducing unnecessary complexity and nesting
- Eliminating redundant code and abstractions
- Improving readability through clear variable and function names
- Consolidating related logic
- Removing unnecessary comments that describe obvious code
- IMPORTANT: Avoid nested ternary operators - prefer switch statements or if/else chains for multiple conditions
- Choose clarity over brevity - explicit code is often better than overly compact code
4. **Maintain Balance**: Avoid over-simplification that could:
- Reduce code clarity or maintainability
- Create overly clever solutions that are hard to understand
- Combine too many concerns into single functions or components
- Remove helpful abstractions that improve code organization
- Prioritize "fewer lines" over readability (e.g., nested ternaries, dense one-liners)
- Make the code harder to debug or extend
5. **Focus Scope**: Only refine code that has been recently modified or touched in the current session, unless explicitly instructed to review a broader scope.
Your refinement process:
1. Identify the recently modified code sections
2. Analyze for opportunities to improve elegance and consistency
3. Apply project-specific best practices and coding standards
4. Ensure all functionality remains unchanged
5. Verify the refined code is simpler and more maintainable
6. Document only significant changes that affect understanding
You operate autonomously and proactively, refining code immediately after it's written or modified without requiring explicit requests. Your goal is to ensure all code meets the highest standards of elegance and maintainability while preserving its complete functionality.

View File

@@ -0,0 +1,82 @@
---
description: Test coverage quality analysis — behavioral coverage, critical gap identification, test resilience evaluation.
scripts:
sh: scripts/bash/detect-changed-files.sh
ps: scripts/powershell/detect-changed-files.ps1
---
You are an expert test coverage analyst specializing in pull request review. Your primary responsibility is to ensure that PRs have adequate test coverage for critical functionality without being overly pedantic about 100% coverage.
**Determine Changed Files:**
If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode:
> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
>
> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
>
> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic.
**Your Core Responsibilities:**
1. **Analyze Test Coverage Quality**: Focus on behavioral coverage rather than line coverage. Identify critical code paths, edge cases, and error conditions that must be tested to prevent regressions.
2. **Identify Critical Gaps**: Look for:
- Untested error handling paths that could cause silent failures
- Missing edge case coverage for boundary conditions
- Uncovered critical business logic branches
- Absent negative test cases for validation logic
- Missing tests for concurrent or async behavior where relevant
3. **Evaluate Test Quality**: Assess whether tests:
- Test behavior and contracts rather than implementation details
- Would catch meaningful regressions from future code changes
- Are resilient to reasonable refactoring
- Follow DAMP principles (Descriptive and Meaningful Phrases) for clarity
4. **Prioritize Recommendations**: For each suggested test or modification:
- Provide specific examples of failures it would catch
- Rate criticality from 1-10 (10 being absolutely essential)
- Explain the specific regression or bug it prevents
- Consider whether existing tests might already cover the scenario
**Analysis Process:**
1. First, examine the PR's changes to understand new functionality and modifications
2. Review the accompanying tests to map coverage to functionality
3. Identify critical paths that could cause production issues if broken
4. Check for tests that are too tightly coupled to implementation
5. Look for missing negative cases and error scenarios
6. Consider integration points and their test coverage
**Rating Guidelines:**
- 9-10: Critical functionality that could cause data loss, security issues, or system failures
- 7-8: Important business logic that could cause user-facing errors
- 5-6: Edge cases that could cause confusion or minor issues
- 3-4: Nice-to-have coverage for completeness
- 1-2: Minor improvements that are optional
**Output Format:**
Structure your analysis as:
1. **Summary**: Brief overview of test coverage quality
2. **Critical Gaps** (if any): Tests rated 8-10 that must be added
3. **Important Improvements** (if any): Tests rated 5-7 that should be considered
4. **Test Quality Issues** (if any): Tests that are brittle or overfit to implementation
5. **Positive Observations**: What's well-tested and follows best practices
**Important Considerations:**
- Focus on tests that prevent real bugs, not academic completeness
- Consider the project's testing standards from project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent) if available
- Remember that some code paths may be covered by existing integration tests
- Avoid suggesting tests for trivial getters/setters unless they contain logic
- Consider the cost/benefit of each suggested test
- Be specific about what each test should verify and why it matters
- Note when tests are testing implementation rather than behavior
You are thorough but pragmatic, focusing on tests that provide real value in catching bugs and preventing regressions rather than achieving metrics. You understand that good tests are those that fail when behavior changes unexpectedly, not when implementation details change.

View File

@@ -0,0 +1,123 @@
---
description: Type design analysis — encapsulation, invariant expression, usefulness, and enforcement.
scripts:
sh: scripts/bash/detect-changed-files.sh
ps: scripts/powershell/detect-changed-files.ps1
---
You are a type design expert with extensive experience in large-scale software architecture. Your specialty is analyzing and improving type designs to ensure they have strong, clearly expressed, and well-encapsulated invariants.
**Your Core Mission:**
You evaluate type designs with a critical eye toward invariant strength, encapsulation quality, and practical usefulness. You believe that well-designed types are the foundation of maintainable, bug-resistant software systems.
**Determine Changed Files:**
If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode:
> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
>
> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
>
> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic.
**Analysis Framework:**
When analyzing a type, you will:
1. **Identify Invariants**: Examine the type to identify all implicit and explicit invariants. Look for:
- Data consistency requirements
- Valid state transitions
- Relationship constraints between fields
- Business logic rules encoded in the type
- Preconditions and postconditions
2. **Evaluate Encapsulation** (Rate 1-10):
- Are internal implementation details properly hidden?
- Can the type's invariants be violated from outside?
- Are there appropriate access modifiers?
- Is the interface minimal and complete?
3. **Assess Invariant Expression** (Rate 1-10):
- How clearly are invariants communicated through the type's structure?
- Are invariants enforced at compile-time where possible?
- Is the type self-documenting through its design?
- Are edge cases and constraints obvious from the type definition?
4. **Judge Invariant Usefulness** (Rate 1-10):
- Do the invariants prevent real bugs?
- Are they aligned with business requirements?
- Do they make the code easier to reason about?
- Are they neither too restrictive nor too permissive?
5. **Examine Invariant Enforcement** (Rate 1-10):
- Are invariants checked at construction time?
- Are all mutation points guarded?
- Is it impossible to create invalid instances?
- Are runtime checks appropriate and comprehensive?
**Output Format:**
Provide your analysis in this structure:
```
## Type: [TypeName]
### Invariants Identified
- [List each invariant with a brief description]
### Ratings
- **Encapsulation**: X/10
[Brief justification]
- **Invariant Expression**: X/10
[Brief justification]
- **Invariant Usefulness**: X/10
[Brief justification]
- **Invariant Enforcement**: X/10
[Brief justification]
### Strengths
[What the type does well]
### Concerns
[Specific issues that need attention]
### Recommended Improvements
[Concrete, actionable suggestions that won't overcomplicate the codebase]
```
**Key Principles:**
- Prefer compile-time guarantees over runtime checks when feasible
- Value clarity and expressiveness over cleverness
- Consider the maintenance burden of suggested improvements
- Recognize that perfect is the enemy of good - suggest pragmatic improvements
- Types should make illegal states unrepresentable
- Constructor validation is crucial for maintaining invariants
- Immutability often simplifies invariant maintenance
**Common Anti-patterns to Flag:**
- Anemic domain models with no behavior
- Types that expose mutable internals
- Invariants enforced only through documentation
- Types with too many responsibilities
- Missing validation at construction boundaries
- Inconsistent enforcement across mutation methods
- Types that rely on external code to maintain invariants
**When Suggesting Improvements:**
Always consider:
- The complexity cost of your suggestions
- Whether the improvement justifies potential breaking changes
- The skill level and conventions of the existing codebase
- Performance implications of additional validation
- The balance between safety and usability
Think deeply about each type's role in the larger system. Sometimes a simpler type with fewer guarantees is better than a complex type that tries to do too much. Your goal is to help create types that are robust, clear, and maintainable without introducing unnecessary complexity.

View File

@@ -0,0 +1,17 @@
# Review Extension Configuration
#
# Copy this file to your project as .specify/extensions/review/review-config.yml
# and adjust settings as needed. All settings are optional — defaults apply
# when omitted.
# ── Agent Toggles ─────────────────────────────────────────────────────
# Control which agents run during a full review (speckit.review.run).
# Set to false to skip an agent. Direct agent commands (e.g.,
# speckit.review.errors) always run regardless of this setting.
agents:
code: true
comments: true
tests: true
errors: true
types: true
simplify: true

View File

@@ -0,0 +1,81 @@
schema_version: "1.0"
extension:
id: "review"
name: "Review Extension"
version: "1.0.1"
description: "Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification."
author: "ismaelJimenez"
repository: "https://github.com/ismaelJimenez/spec-kit-review"
license: "MIT"
homepage: "https://github.com/ismaelJimenez/spec-kit-review"
requires:
speckit_version: ">=0.1.0"
commands:
- "speckit.implement"
provides:
config:
- name: "review-config.yml"
template: "config-template.yml"
description: "Review extension configuration — confidence threshold and agent toggles"
required: false
scripts:
- name: "detect-changed-files.sh"
file: "scripts/bash/detect-changed-files.sh"
executable: true
- name: "detect-changed-files.ps1"
file: "scripts/powershell/detect-changed-files.ps1"
executable: true
commands:
- name: "speckit.review.run"
file: "commands/run.md"
description: "Comprehensive code review using specialized agents"
- name: "speckit.review.code"
file: "commands/code.md"
description: "General code quality review — project guideline compliance, bug detection, code quality"
- name: "speckit.review.comments"
file: "commands/comments.md"
description: "Code comment accuracy verification, documentation completeness, comment rot detection"
- name: "speckit.review.tests"
file: "commands/tests.md"
description: "Test coverage quality analysis — behavioral coverage, critical gap identification, test resilience evaluation"
- name: "speckit.review.errors"
file: "commands/errors.md"
description: "Error handling review — silent failure detection, catch block analysis, error logging"
- name: "speckit.review.types"
file: "commands/types.md"
description: "Type design analysis — encapsulation, invariant expression, usefulness, and enforcement"
- name: "speckit.review.simplify"
file: "commands/simplify.md"
description: "Code simplification suggestions — clarity, unnecessary complexity, redundant abstractions"
hooks:
after_implement:
command: "speckit.review.run"
optional: true
prompt: "Run PR review on implemented changes?"
description: "Comprehensive review after implementation"
condition: null
tags:
- "review"
- "code-quality"
- "pr-review"
- "testing"
- "error-handling"
defaults:
agents:
code: true
comments: true
tests: true
errors: true
types: true
simplify: true

View File

@@ -0,0 +1,243 @@
#!/usr/bin/env bash
# Detect changed files for code review via git diff
#
# Identifies changed files by comparing the current branch against
# the default branch plus any uncommitted work (Mode A — feature branch
# diff + working directory) or by collecting staged + unstaged changes
# (Mode B — working directory changes only).
#
# Usage: ./detect-changed-files.sh [OPTIONS]
#
# OPTIONS:
# --json Output in JSON format (for machine consumption)
# --help, -h Show this help message
#
# EXIT CODES:
# 0 Changed files detected successfully
# 1 Error (git unavailable, not a git repository)
# 2 No changes detected
#
# OUTPUTS:
# Text mode:
# BRANCH: <current-branch>
# DEFAULT_BRANCH: <default-branch>
# MODE: <detection mode description>
# CHANGED_FILES:
# file1
# file2
#
# JSON mode:
# {"branch":"...","default_branch":"...","mode":"...","changed_files":["..."]}
set -e
# --- Argument parsing ---
JSON_MODE=false
for arg in "$@"; do
case "$arg" in
--json) JSON_MODE=true ;;
--help|-h)
cat << 'EOF'
Usage: detect-changed-files.sh [OPTIONS]
Detect changed files for code review via git diff.
OPTIONS:
--json Output in JSON format
--help, -h Show this help message
EXIT CODES:
0 Changed files detected successfully
1 Error (git unavailable, not a git repository)
2 No changes detected
EOF
exit 0
;;
*) echo "ERROR: Unknown option '$arg'" >&2; exit 1 ;;
esac
done
# --- Helper: escape a string for safe JSON embedding ---
json_escape() {
local s="$1"
s="${s//\\/\\\\}" # \ → \\
s="${s//\"/\\\"}" # " → \\"
s="${s//$'\t'/\\t}" # tab → \t
s="${s//$'\n'/\\n}" # newline → \n
s="${s//$'\r'/\\r}" # carriage return → \r
printf '%s' "$s"
}
# --- Helper: format bash array as JSON array ---
fmt_array() {
local arr=("$@")
if [[ ${#arr[@]} -eq 0 ]]; then echo "[]"; return; fi
local first=true
local result="["
for item in "${arr[@]}"; do
if $first; then first=false; else result+=","; fi
result+="\"$(json_escape "$item")\""
done
result+="]"
echo "$result"
}
# --- Helper: output error and exit ---
error_exit() {
local message="$1"
local code="${2:-1}"
if $JSON_MODE; then
printf '{"error":"%s"}\n' "$(json_escape "$message")"
else
echo "Error: $message" >&2
fi
exit "$code"
}
# --- 1a. Verify Git Availability ---
if ! command -v git >/dev/null 2>&1; then
error_exit "git is not available. The review extension requires git to identify changed files." 1
fi
if ! git rev-parse --git-dir >/dev/null 2>&1; then
error_exit "Not a git repository. The review extension requires git to identify changed files." 1
fi
# --- 1b. Detect Branch Context ---
# Get current branch (empty string if detached HEAD)
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
# Determine default branch
DEFAULT_BRANCH=""
# Try symbolic-ref first
symref=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo "")
if [[ -n "$symref" ]]; then
DEFAULT_BRANCH="${symref##refs/remotes/origin/}"
fi
# Fallback: check origin/main
if [[ -z "$DEFAULT_BRANCH" ]]; then
if git rev-parse --verify origin/main >/dev/null 2>&1; then
DEFAULT_BRANCH="main"
fi
fi
# Fallback: check origin/master
if [[ -z "$DEFAULT_BRANCH" ]]; then
if git rev-parse --verify origin/master >/dev/null 2>&1; then
DEFAULT_BRANCH="master"
fi
fi
# --- 1c. Get Changed Files ---
CHANGED_FILES=()
MODE=""
if [[ -n "$CURRENT_BRANCH" && -n "$DEFAULT_BRANCH" && "$CURRENT_BRANCH" != "$DEFAULT_BRANCH" ]]; then
# Mode A — Feature Branch
MERGE_BASE=$(git merge-base "origin/$DEFAULT_BRANCH" HEAD 2>/dev/null || echo "")
if [[ -n "$MERGE_BASE" ]]; then
# Committed changes since merge-base
COMMITTED=()
while IFS= read -r -d '' line; do
[[ -n "$line" ]] && COMMITTED+=("$line")
done < <(git diff --name-only -z --diff-filter=ACMR "${MERGE_BASE}...HEAD" 2>/dev/null)
# Staged (index) changes
STAGED=()
while IFS= read -r -d '' line; do
[[ -n "$line" ]] && STAGED+=("$line")
done < <(git diff --cached --name-only -z --diff-filter=ACMR 2>/dev/null)
# Unstaged (working tree) changes
UNSTAGED=()
while IFS= read -r -d '' line; do
[[ -n "$line" ]] && UNSTAGED+=("$line")
done < <(git diff --name-only -z --diff-filter=ACMR 2>/dev/null)
# Combine and deduplicate (bash 3 compatible — no associative arrays)
CHANGED_FILES=()
for f in "${COMMITTED[@]}" "${STAGED[@]}" "${UNSTAGED[@]}"; do
[[ -z "$f" ]] && continue
_dup=false
for existing in "${CHANGED_FILES[@]}"; do
if [[ "$existing" == "$f" ]]; then
_dup=true
break
fi
done
if ! $_dup; then
CHANGED_FILES+=("$f")
fi
done
MODE="Feature branch diff (${DEFAULT_BRANCH}...HEAD) + uncommitted changes"
else
# merge-base failed — fall through to Mode B
DEFAULT_BRANCH=""
fi
fi
if [[ -z "$MODE" ]]; then
# Mode B — Working Directory Changes
STAGED=()
while IFS= read -r -d '' line; do
[[ -n "$line" ]] && STAGED+=("$line")
done < <(git diff --cached --name-only -z --diff-filter=ACMR 2>/dev/null)
UNSTAGED=()
while IFS= read -r -d '' line; do
[[ -n "$line" ]] && UNSTAGED+=("$line")
done < <(git diff --name-only -z --diff-filter=ACMR 2>/dev/null)
# Combine and deduplicate (bash 3 compatible — no associative arrays)
CHANGED_FILES=()
for f in "${STAGED[@]}" "${UNSTAGED[@]}"; do
[[ -z "$f" ]] && continue
_dup=false
for existing in "${CHANGED_FILES[@]}"; do
if [[ "$existing" == "$f" ]]; then
_dup=true
break
fi
done
if ! $_dup; then
CHANGED_FILES+=("$f")
fi
done
MODE="Working directory changes (staged + unstaged)"
[[ -z "$DEFAULT_BRANCH" ]] && DEFAULT_BRANCH="(unknown)"
fi
# --- 1d. Validate Changed Files ---
if [[ ${#CHANGED_FILES[@]} -eq 0 ]]; then
if $JSON_MODE; then
printf '{"branch":"%s","default_branch":"%s","mode":"%s","changed_files":[],"message":"No changes detected. Nothing to review."}\n' \
"$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$DEFAULT_BRANCH")" "$(json_escape "$MODE")"
else
echo "No changes detected. Nothing to review."
fi
exit 2
fi
# --- Output ---
if $JSON_MODE; then
printf '{"branch":"%s","default_branch":"%s","mode":"%s","changed_files":%s}\n' \
"$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$DEFAULT_BRANCH")" "$(json_escape "$MODE")" "$(fmt_array "${CHANGED_FILES[@]}")"
else
echo "BRANCH: $CURRENT_BRANCH"
echo "DEFAULT_BRANCH: $DEFAULT_BRANCH"
echo "MODE: $MODE"
echo "CHANGED_FILES:"
for f in "${CHANGED_FILES[@]}"; do
echo " $f"
done
fi
exit 0

View File

@@ -0,0 +1,228 @@
<#
.SYNOPSIS
Detect changed files for code review via git diff.
.DESCRIPTION
Identifies changed files by comparing the current branch against
the default branch plus any uncommitted work (Mode A - feature branch
diff + working directory) or by collecting staged + unstaged changes
(Mode B - working directory changes only).
.PARAMETER Json
Output in JSON format (for machine consumption).
.PARAMETER Help
Show help message and exit.
.EXAMPLE
.\detect-changed-files.ps1
# Text output of changed files
.EXAMPLE
.\detect-changed-files.ps1 -Json
# JSON output of changed files
.NOTES
EXIT CODES:
0 Changed files detected successfully
1 Error (git unavailable, not a git repository)
2 No changes detected
#>
[CmdletBinding()]
param(
[switch]$Json,
[Alias("h")]
[switch]$Help
)
$ErrorActionPreference = 'Stop'
# --- Help ---
if ($Help) {
@"
Usage: detect-changed-files.ps1 [OPTIONS]
Detect changed files for code review via git diff.
OPTIONS:
-Json Output in JSON format
-Help, -h Show this help message
EXIT CODES:
0 Changed files detected successfully
1 Error (git unavailable, not a git repository)
2 No changes detected
"@
exit 0
}
# --- Helper: output error and exit ---
function Write-ErrorAndExit {
param(
[string]$Message,
[int]$Code = 1
)
if ($Json) {
[PSCustomObject]@{ error = $Message } | ConvertTo-Json -Compress
} else {
Write-Error "Error: $Message"
}
exit $Code
}
# --- 1a. Verify Git Availability ---
$gitCmd = Get-Command git -ErrorAction SilentlyContinue
if (-not $gitCmd) {
Write-ErrorAndExit "git is not available. The review extension requires git to identify changed files." 1
}
$gitDir = git rev-parse --git-dir 2>&1
if ($LASTEXITCODE -ne 0) {
Write-ErrorAndExit "Not a git repository. The review extension requires git to identify changed files." 1
}
# --- 1b. Detect Branch Context ---
# Get current branch (empty string if detached HEAD)
$CurrentBranch = ""
try {
$CurrentBranch = (git branch --show-current 2>$null) | Out-String
$CurrentBranch = $CurrentBranch.Trim()
} catch {
$CurrentBranch = ""
}
# Determine default branch
$DefaultBranch = ""
# Try symbolic-ref first
try {
$symref = (git symbolic-ref refs/remotes/origin/HEAD 2>$null) | Out-String
$symref = $symref.Trim()
if ($symref -match "refs/remotes/origin/(.+)$") {
$DefaultBranch = $Matches[1]
}
} catch {}
# Fallback: check origin/main
if (-not $DefaultBranch) {
$null = git rev-parse --verify origin/main 2>$null
if ($LASTEXITCODE -eq 0) {
$DefaultBranch = "main"
}
}
# Fallback: check origin/master
if (-not $DefaultBranch) {
$null = git rev-parse --verify origin/master 2>$null
if ($LASTEXITCODE -eq 0) {
$DefaultBranch = "master"
}
}
# --- 1c. Get Changed Files ---
$ChangedFiles = @()
$Mode = ""
if ($CurrentBranch -and $DefaultBranch -and ($CurrentBranch -ne $DefaultBranch)) {
# Mode A - Feature Branch
$MergeBase = ""
try {
$MergeBase = (git merge-base "origin/$DefaultBranch" HEAD 2>$null) | Out-String
$MergeBase = $MergeBase.Trim()
} catch {}
if ($MergeBase) {
# Committed changes since merge-base
$diffRaw = git diff --name-only -z --diff-filter=ACMR "$MergeBase...HEAD" 2>$null
$committedFiles = @()
if ($diffRaw) {
$diffJoined = ($diffRaw -join "`n")
$committedFiles = @($diffJoined -split "`0" | Where-Object { $_ -ne "" })
}
# Staged (index) changes
$stagedRaw = git diff --cached --name-only -z --diff-filter=ACMR 2>$null
$stagedFiles = @()
if ($stagedRaw) {
$sJoined = ($stagedRaw -join "`n")
$stagedFiles = @($sJoined -split "`0" | Where-Object { $_ -ne "" })
}
# Unstaged (working tree) changes
$unstagedRaw = git diff --name-only -z --diff-filter=ACMR 2>$null
$unstagedFiles = @()
if ($unstagedRaw) {
$uJoined = ($unstagedRaw -join "`n")
$unstagedFiles = @($uJoined -split "`0" | Where-Object { $_ -ne "" })
}
# Combine and deduplicate
$ChangedFiles = @($committedFiles + $stagedFiles + $unstagedFiles | Sort-Object -Unique)
$Mode = "Feature branch diff ($DefaultBranch...HEAD) + uncommitted changes"
} else {
# merge-base failed - fall through to Mode B
$DefaultBranch = ""
}
}
if (-not $Mode) {
# Mode B - Working Directory Changes
$stagedRaw = git diff --cached --name-only -z --diff-filter=ACMR 2>$null
$unstagedRaw = git diff --name-only -z --diff-filter=ACMR 2>$null
$allFiles = @()
if ($stagedRaw) {
$sJoined = ($stagedRaw -join "`n")
$allFiles += @($sJoined -split "`0" | Where-Object { $_ -ne "" })
}
if ($unstagedRaw) {
$uJoined = ($unstagedRaw -join "`n")
$allFiles += @($uJoined -split "`0" | Where-Object { $_ -ne "" })
}
# Deduplicate
$ChangedFiles = @($allFiles | Sort-Object -Unique)
$Mode = "Working directory changes (staged + unstaged)"
if (-not $DefaultBranch) { $DefaultBranch = "(unknown)" }
}
# --- 1d. Validate Changed Files ---
if ($ChangedFiles.Count -eq 0) {
if ($Json) {
[PSCustomObject]@{
branch = $CurrentBranch
default_branch = $DefaultBranch
mode = $Mode
changed_files = @()
message = "No changes detected. Nothing to review."
} | ConvertTo-Json -Compress
} else {
Write-Output "No changes detected. Nothing to review."
}
exit 2
}
# --- Output ---
if ($Json) {
[PSCustomObject]@{
branch = $CurrentBranch
default_branch = $DefaultBranch
mode = $Mode
changed_files = $ChangedFiles
} | ConvertTo-Json -Compress
} else {
Write-Output "BRANCH: $CurrentBranch"
Write-Output "DEFAULT_BRANCH: $DefaultBranch"
Write-Output "MODE: $Mode"
Write-Output "CHANGED_FILES:"
foreach ($f in $ChangedFiles) {
Write-Output " $f"
}
}
exit 0

View File

@@ -0,0 +1,650 @@
load "test_helper"
setup() {
setup_temp_dir
}
teardown() {
teardown_temp_dir
}
# ──────────────────────────────────────────────
# 1a. Git Availability Errors
# ──────────────────────────────────────────────
@test "fails when not in a git repository" {
cd "$TEST_TEMP_DIR"
run bash "$SCRIPTS_DIR/detect-changed-files.sh"
assert_failure
[ "$status" -eq 1 ]
assert_output --partial "Not a git repository"
}
@test "fails with JSON error when not in a git repository" {
cd "$TEST_TEMP_DIR"
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_failure
[ "$status" -eq 1 ]
assert_output --partial '"error"'
assert_output --partial "Not a git repository"
}
# ──────────────────────────────────────────────
# Help
# ──────────────────────────────────────────────
@test "--help shows usage information" {
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --help
assert_success
assert_output --partial "Usage"
assert_output --partial "detect-changed-files"
}
@test "-h shows usage information" {
run bash "$SCRIPTS_DIR/detect-changed-files.sh" -h
assert_success
assert_output --partial "Usage"
}
@test "unknown option fails with error" {
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --bogus
assert_failure
assert_output --partial "Unknown option"
}
# ──────────────────────────────────────────────
# No Changes Detected (exit code 2)
# ──────────────────────────────────────────────
@test "exit code 2 when no changes in clean repo" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
run bash "$SCRIPTS_DIR/detect-changed-files.sh"
[ "$status" -eq 2 ]
assert_output --partial "No changes detected"
}
@test "exit code 2 with JSON output when no changes" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
[ "$status" -eq 2 ]
assert_valid_json "$output"
local msg=$(json_field "$output" "message")
[[ "$msg" == *"No changes detected"* ]]
}
# ──────────────────────────────────────────────
# Mode B — Working Directory: Unstaged Changes
# ──────────────────────────────────────────────
@test "detects unstaged changes (Mode B)" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
# Create and commit a file, then modify it
echo "initial" > tracked.txt
git add tracked.txt
git commit --quiet -m "Add tracked file"
echo "modified" > tracked.txt
run bash "$SCRIPTS_DIR/detect-changed-files.sh"
assert_success
assert_output --partial "tracked.txt"
assert_output --partial "Working directory changes"
}
@test "detects unstaged changes with --json (Mode B)" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
echo "initial" > tracked.txt
git add tracked.txt
git commit --quiet -m "Add tracked file"
echo "modified" > tracked.txt
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
assert_valid_json "$output"
local mode=$(json_field "$output" "mode")
[[ "$mode" == "Working directory changes (staged + unstaged)" ]]
assert_output --partial '"tracked.txt"'
}
# ──────────────────────────────────────────────
# Mode B — Working Directory: Staged Changes
# ──────────────────────────────────────────────
@test "detects staged changes (Mode B)" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
echo "new file" > staged.txt
git add staged.txt
run bash "$SCRIPTS_DIR/detect-changed-files.sh"
assert_success
assert_output --partial "staged.txt"
assert_output --partial "Working directory changes"
}
# ──────────────────────────────────────────────
# Mode B — Staged + Unstaged Deduplication
# ──────────────────────────────────────────────
@test "deduplicates staged and unstaged changes (Mode B)" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
# Create a file, commit, stage a change, then modify again (unstaged)
echo "v1" > both.txt
git add both.txt
git commit --quiet -m "Add both.txt"
echo "v2" > both.txt
git add both.txt
echo "v3" > both.txt
# Also add a new staged-only file
echo "new" > only-staged.txt
git add only-staged.txt
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
assert_valid_json "$output"
# both.txt should appear only once
local count=$(echo "$output" | python3 -c "import json,sys; print(json.load(sys.stdin)['changed_files'].count('both.txt'))")
[ "$count" -eq 1 ]
# only-staged.txt should also be present
assert_output --partial '"only-staged.txt"'
}
# ──────────────────────────────────────────────
# Mode A — Feature Branch Diff
# ──────────────────────────────────────────────
@test "detects feature branch changes via merge-base (Mode A)" {
init_git_repo_with_remote "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
# Create a feature branch with a new file
git checkout --quiet -b feature-branch
echo "feature code" > feature.txt
git add feature.txt
git commit --quiet -m "Add feature file"
run bash "$SCRIPTS_DIR/detect-changed-files.sh"
assert_success
assert_output --partial "feature.txt"
assert_output --partial "Feature branch diff"
}
@test "Mode A --json has correct structure" {
init_git_repo_with_remote "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
git checkout --quiet -b feature-branch
echo "feature code" > feature.txt
git add feature.txt
git commit --quiet -m "Add feature file"
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
assert_valid_json "$output"
local branch=$(json_field "$output" "branch")
[ "$branch" = "feature-branch" ]
local mode=$(json_field "$output" "mode")
[[ "$mode" == "Feature branch diff"* ]]
assert_output --partial '"feature.txt"'
}
@test "Mode A excludes deleted files (diff-filter=ACMR)" {
init_git_repo_with_remote "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
# Add a file on main and push
echo "to delete" > delete-me.txt
git add delete-me.txt
git commit --quiet -m "Add file to delete"
git push --quiet origin main
# Create feature branch, delete the file, add another
git checkout --quiet -b feature-delete
git rm --quiet delete-me.txt
echo "keep me" > keep.txt
git add keep.txt
git commit --quiet -m "Delete and add"
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
# delete-me.txt should NOT appear (it was deleted)
local files=$(json_array_field "$output" "changed_files")
! echo "$files" | grep -q "delete-me.txt"
# keep.txt should appear
assert_output --partial '"keep.txt"'
}
@test "Mode A detects multiple changed files" {
init_git_repo_with_remote "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
git checkout --quiet -b multi-files
echo "a" > file-a.txt
echo "b" > file-b.txt
mkdir -p sub
echo "c" > sub/file-c.txt
git add .
git commit --quiet -m "Add multiple files"
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
assert_output --partial '"file-a.txt"'
assert_output --partial '"file-b.txt"'
assert_output --partial '"sub/file-c.txt"'
}
# ──────────────────────────────────────────────
# Mode A — Feature Branch + Uncommitted Changes
# ──────────────────────────────────────────────
@test "Mode A includes staged uncommitted files" {
init_git_repo_with_remote "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
git checkout --quiet -b feature-staged
echo "committed" > committed.txt
git add committed.txt
git commit --quiet -m "Add committed file"
# Stage a new file without committing
echo "staged" > staged-only.txt
git add staged-only.txt
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
assert_valid_json "$output"
# Both committed and staged files should appear
assert_output --partial '"committed.txt"'
assert_output --partial '"staged-only.txt"'
local mode=$(json_field "$output" "mode")
[[ "$mode" == *"uncommitted"* ]]
}
@test "Mode A includes unstaged uncommitted files" {
init_git_repo_with_remote "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
# Create a file on main, push it, then modify on feature branch
echo "original" > existing.txt
git add existing.txt
git commit --quiet -m "Add existing file"
git push --quiet origin main
git checkout --quiet -b feature-unstaged
echo "committed on branch" > committed.txt
git add committed.txt
git commit --quiet -m "Add committed file"
# Modify existing file without staging
echo "modified" > existing.txt
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
assert_valid_json "$output"
# Both committed diff and unstaged modification should appear
assert_output --partial '"committed.txt"'
assert_output --partial '"existing.txt"'
}
@test "Mode A deduplicates committed and uncommitted files" {
init_git_repo_with_remote "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
git checkout --quiet -b feature-dedup
echo "v1" > shared.txt
git add shared.txt
git commit --quiet -m "Add shared file"
# Modify the same file (unstaged) — it appears in both committed diff and unstaged
echo "v2" > shared.txt
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
assert_valid_json "$output"
# shared.txt should appear exactly once
local count=$(echo "$output" | python3 -c "import json,sys; print(json.load(sys.stdin)['changed_files'].count('shared.txt'))")
[ "$count" -eq 1 ]
}
# ──────────────────────────────────────────────
# Default Branch Detection Fallbacks
# ──────────────────────────────────────────────
@test "detects origin/main as default branch" {
init_git_repo_with_remote "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
# Unset symbolic-ref to force fallback
git remote set-head origin --delete 2>/dev/null || true
git checkout --quiet -b test-branch
echo "test" > test-file.txt
git add test-file.txt
git commit --quiet -m "Add test file"
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
local default=$(json_field "$output" "default_branch")
[ "$default" = "main" ]
}
@test "detects origin/master as default branch when no origin/main" {
# Create a bare repo with master as default
local bare="${TEST_TEMP_DIR}/_bare_master"
mkdir -p "$bare"
git -C "$bare" init --bare --quiet
git -C "$bare" symbolic-ref HEAD refs/heads/master
# Clone-like setup
cd "$TEST_TEMP_DIR"
git init --quiet
git config user.email "test@example.com"
git config user.name "Test"
git remote add origin "$bare"
# Create initial commit on master and push
touch .gitkeep
git add .
git checkout -b master --quiet 2>/dev/null || true
git commit --quiet -m "Initial commit"
git push --quiet origin master
# Remove symbolic-ref to force fallback
git remote set-head origin --delete 2>/dev/null || true
# Create feature branch
git checkout --quiet -b test-branch
echo "test" > test-file.txt
git add test-file.txt
git commit --quiet -m "Add test file"
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
local default=$(json_field "$output" "default_branch")
[ "$default" = "master" ]
}
@test "falls back to Mode B when no remote default branch found" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
# No remote configured, so no origin/* branches
echo "change" > new-file.txt
git add new-file.txt
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
local mode=$(json_field "$output" "mode")
[[ "$mode" == "Working directory changes"* ]]
}
# ──────────────────────────────────────────────
# Detached HEAD
# ──────────────────────────────────────────────
@test "detached HEAD falls back to Mode B" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
# Create a commit and detach HEAD
echo "file" > detached.txt
git add detached.txt
git commit --quiet -m "Add file"
local commit_hash=$(git rev-parse HEAD)
git checkout --quiet "$commit_hash"
# Make a change
echo "modified" > detached.txt
run bash "$SCRIPTS_DIR/detect-changed-files.sh"
assert_success
assert_output --partial "Working directory changes"
assert_output --partial "detached.txt"
}
# ──────────────────────────────────────────────
# JSON Output Validation
# ──────────────────────────────────────────────
@test "--json output is valid JSON with all required keys" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
echo "content" > new.txt
git add new.txt
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
assert_valid_json "$output"
# Verify all expected keys exist
echo "$output" | python3 -c "
import json, sys
data = json.load(sys.stdin)
assert 'branch' in data, 'missing branch key'
assert 'default_branch' in data, 'missing default_branch key'
assert 'mode' in data, 'missing mode key'
assert 'changed_files' in data, 'missing changed_files key'
assert isinstance(data['changed_files'], list), 'changed_files should be a list'
print('All keys present and correct types')
"
}
@test "text mode output has correct format" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
echo "content" > formatted.txt
git add formatted.txt
run bash "$SCRIPTS_DIR/detect-changed-files.sh"
assert_success
assert_output --partial "BRANCH:"
assert_output --partial "DEFAULT_BRANCH:"
assert_output --partial "MODE:"
assert_output --partial "CHANGED_FILES:"
assert_output --partial "formatted.txt"
}
# ──────────────────────────────────────────────
# Edge Cases
# ──────────────────────────────────────────────
@test "handles files with spaces in names" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
echo "content" > "file with spaces.txt"
git add "file with spaces.txt"
run bash "$SCRIPTS_DIR/detect-changed-files.sh"
assert_success
assert_output --partial "file with spaces.txt"
}
@test "handles nested directory changes" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
mkdir -p deep/nested/path
echo "deep" > deep/nested/path/file.txt
git add .
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
assert_output --partial '"deep/nested/path/file.txt"'
}
@test "only reports ACMR files (Added, Copied, Modified, Renamed)" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
# Add and commit files
echo "keep" > keep.txt
echo "remove" > remove.txt
git add .
git commit --quiet -m "Add files"
# Delete one, modify another — only staged
git rm --quiet remove.txt
echo "modified" > keep.txt
echo "added" > added.txt
git add .
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
# keep.txt (Modified) and added.txt (Added) should be present
assert_output --partial '"keep.txt"'
assert_output --partial '"added.txt"'
# remove.txt (Deleted) should NOT be present
local files=$(json_array_field "$output" "changed_files")
[[ ! "$files" == *"remove.txt"* ]]
}
@test "Mode A: branch field matches current branch name" {
init_git_repo_with_remote "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
git checkout --quiet -b my-feature-123
echo "x" > x.txt
git add x.txt
git commit --quiet -m "commit"
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
local branch=$(json_field "$output" "branch")
[ "$branch" = "my-feature-123" ]
}
@test "Mode B: branch field shows current branch on default" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
echo "change" > file.txt
git add file.txt
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
# Branch should be main (or master depending on git default)
local branch=$(json_field "$output" "branch")
[[ "$branch" == "main" || "$branch" == "master" ]]
}
# ──────────────────────────────────────────────
# Special Characters in Filenames
# ──────────────────────────────────────────────
@test "handles filenames with double quotes in JSON mode" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
# Create a file with a double quote in its name
local fname='file"quote.txt'
echo "content" > "$fname"
git add "$fname"
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
assert_valid_json "$output"
# The filename should be properly escaped in JSON
local files=$(json_array_field "$output" "changed_files")
[[ "$files" == *'file"quote.txt'* ]]
}
@test "handles filenames with backslashes in JSON mode" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
# Create a file with a backslash in its name
local fname='file\slash.txt'
echo "content" > "$fname"
git add "$fname"
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
assert_valid_json "$output"
}
@test "handles filenames with special characters in JSON mode" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
# Create files with various special characters
echo "a" > "file (1).txt"
echo "b" > "file's.txt"
echo "c" > "file&more.txt"
git add .
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
assert_valid_json "$output"
# All three files should be present
local count=$(echo "$output" | python3 -c "import json,sys; print(len(json.load(sys.stdin)['changed_files']))")
[ "$count" -eq 3 ]
}
# ──────────────────────────────────────────────
# Renamed Files
# ──────────────────────────────────────────────
@test "Mode A detects renamed files (diff-filter includes R)" {
init_git_repo_with_remote "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
echo "content" > original.txt
git add original.txt
git commit --quiet -m "Add original"
git push --quiet origin main
git checkout --quiet -b feature-rename
git mv original.txt renamed.txt
git commit --quiet -m "Rename file"
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
assert_output --partial '"renamed.txt"'
}
@test "Mode B detects renamed files (staged)" {
init_git_repo "$TEST_TEMP_DIR"
cd "$TEST_TEMP_DIR"
echo "content" > original.txt
git add original.txt
git commit --quiet -m "Add original"
git mv original.txt renamed.txt
run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
assert_success
assert_output --partial '"renamed.txt"'
}

View File

@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Shared test helper for BATS tests
# Load bats libraries
BATS_LIB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/lib" && pwd)"
load "${BATS_LIB_DIR}/bats-support/load"
load "${BATS_LIB_DIR}/bats-assert/load"
# Project root (repo root)
PROJECT_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
# Scripts under test
SCRIPTS_DIR="${PROJECT_ROOT}/scripts/bash"
# Create a temporary working directory for each test
setup_temp_dir() {
TEST_TEMP_DIR="$(mktemp -d)"
export TEST_TEMP_DIR
}
# Clean up the temporary directory
teardown_temp_dir() {
if [[ -n "${TEST_TEMP_DIR:-}" && -d "$TEST_TEMP_DIR" ]]; then
rm -rf "$TEST_TEMP_DIR"
fi
}
# Initialize a git repo in the temp directory
init_git_repo() {
local dir="${1:-$TEST_TEMP_DIR}"
git -C "$dir" init --quiet -b main
git -C "$dir" config user.email "test@example.com"
git -C "$dir" config user.name "Test"
# Create initial commit so git diff works
touch "$dir/.gitkeep"
git -C "$dir" add .
git -C "$dir" commit --quiet -m "Initial commit"
}
# Initialize a git repo with a bare remote (for origin/* refs)
init_git_repo_with_remote() {
local dir="${1:-$TEST_TEMP_DIR}"
local bare_dir="${dir}/_bare_remote"
# Create a bare repo to act as origin (explicitly use main)
mkdir -p "$bare_dir"
git -C "$bare_dir" init --bare --quiet
git -C "$bare_dir" symbolic-ref HEAD refs/heads/main
# Create the working repo
git -C "$dir" init --quiet -b main
git -C "$dir" config user.email "test@example.com"
git -C "$dir" config user.name "Test"
git -C "$dir" remote add origin "$bare_dir"
# Create initial commit and push to establish origin/main
touch "$dir/.gitkeep"
git -C "$dir" add .
git -C "$dir" commit --quiet -m "Initial commit"
git -C "$dir" push --quiet origin main
# Set origin/HEAD so symbolic-ref works
git -C "$dir" remote set-head origin --auto 2>/dev/null || true
}
# Validate that output is valid JSON
assert_valid_json() {
local output="$1"
echo "$output" | python3 -m json.tool > /dev/null 2>&1 \
|| fail "Invalid JSON: $output"
}
# Extract a JSON field value (simple top-level string/bool/number)
json_field() {
local json="$1"
local field="$2"
echo "$json" | python3 -c "import json,sys; print(json.load(sys.stdin).get('$field',''))"
}
# Extract a JSON array field as newline-separated values
json_array_field() {
local json="$1"
local field="$2"
echo "$json" | python3 -c "
import json, sys
data = json.load(sys.stdin)
for item in data.get('$field', []):
print(item)
"
}

View File

@@ -0,0 +1,740 @@
BeforeAll {
$ScriptsDir = Join-Path $PSScriptRoot "..\..\scripts\powershell"
$Script = Join-Path $ScriptsDir "detect-changed-files.ps1"
function New-TempDir {
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())
New-Item -ItemType Directory -Path $tmp -Force | Out-Null
return $tmp
}
function Initialize-GitRepo {
param([string]$Dir)
Push-Location $Dir
git init --quiet -b main
git config user.email "test@example.com"
git config user.name "Test"
New-Item -ItemType File -Path ".gitkeep" -Force | Out-Null
git add .
git commit --quiet -m "Initial commit"
Pop-Location
}
function Initialize-GitRepoWithRemote {
param([string]$Dir)
$bareDir = Join-Path $Dir "_bare_remote"
New-Item -ItemType Directory -Path $bareDir -Force | Out-Null
Push-Location $bareDir
git init --bare --quiet
git symbolic-ref HEAD refs/heads/main
Pop-Location
Push-Location $Dir
git init --quiet -b main
git config user.email "test@example.com"
git config user.name "Test"
git remote add origin $bareDir
New-Item -ItemType File -Path ".gitkeep" -Force | Out-Null
git add .
git commit --quiet -m "Initial commit"
git push --quiet origin main
git remote set-head origin --auto 2>$null
Pop-Location
}
}
Describe "detect-changed-files.ps1" {
# ──────────────────────────────────────────────
# Help
# ──────────────────────────────────────────────
Describe "Help" {
It "shows usage with -Help" {
$result = & pwsh -NoProfile -File $Script -Help
$LASTEXITCODE | Should -Be 0
($result -join "`n") | Should -Match "Usage"
}
It "shows usage with -h" {
$result = & pwsh -NoProfile -File $Script -h
$LASTEXITCODE | Should -Be 0
($result -join "`n") | Should -Match "Usage"
}
}
# ──────────────────────────────────────────────
# Git Availability Errors
# ──────────────────────────────────────────────
Describe "Git availability" {
It "fails when not in a git repository" {
$tmp = New-TempDir
try {
Push-Location $tmp
$result = & pwsh -NoProfile -File $Script 2>&1
$LASTEXITCODE | Should -Be 1
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
It "fails with JSON error when not in a git repository" {
$tmp = New-TempDir
try {
Push-Location $tmp
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 1
($result -join "`n") | Should -Match '"error"'
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
}
# ──────────────────────────────────────────────
# No Changes Detected
# ──────────────────────────────────────────────
Describe "No changes" {
It "exit code 2 when no changes in clean repo" {
$tmp = New-TempDir
try {
Initialize-GitRepo -Dir $tmp
Push-Location $tmp
& pwsh -NoProfile -File $Script 2>&1
$LASTEXITCODE | Should -Be 2
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
It "exit code 2 with JSON output when no changes" {
$tmp = New-TempDir
try {
Initialize-GitRepo -Dir $tmp
Push-Location $tmp
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 2
$json = $result | ConvertFrom-Json
$json.message | Should -Match "No changes detected"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
}
# ──────────────────────────────────────────────
# Mode B — Unstaged Changes
# ──────────────────────────────────────────────
Describe "Mode B - Unstaged" {
It "detects unstaged changes" {
$tmp = New-TempDir
try {
Initialize-GitRepo -Dir $tmp
Push-Location $tmp
"initial" | Set-Content "tracked.txt"
git add tracked.txt
git commit --quiet -m "Add tracked file"
"modified" | Set-Content "tracked.txt"
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.mode | Should -Match "Working directory changes"
$json.changed_files | Should -Contain "tracked.txt"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
}
# ──────────────────────────────────────────────
# Mode B — Staged Changes
# ──────────────────────────────────────────────
Describe "Mode B - Staged" {
It "detects staged changes" {
$tmp = New-TempDir
try {
Initialize-GitRepo -Dir $tmp
Push-Location $tmp
"new file" | Set-Content "staged.txt"
git add staged.txt
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.mode | Should -Match "Working directory changes"
$json.changed_files | Should -Contain "staged.txt"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
}
# ──────────────────────────────────────────────
# Mode B — Deduplication
# ──────────────────────────────────────────────
Describe "Mode B - Deduplication" {
It "deduplicates staged and unstaged changes" {
$tmp = New-TempDir
try {
Initialize-GitRepo -Dir $tmp
Push-Location $tmp
"v1" | Set-Content "both.txt"
git add both.txt
git commit --quiet -m "Add both.txt"
"v2" | Set-Content "both.txt"
git add both.txt
"v3" | Set-Content "both.txt"
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
($json.changed_files | Where-Object { $_ -eq "both.txt" }).Count | Should -Be 1
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
}
# ──────────────────────────────────────────────
# Mode A — Feature Branch Diff
# ──────────────────────────────────────────────
Describe "Mode A - Feature Branch" {
It "detects feature branch changes via merge-base" {
$tmp = New-TempDir
try {
Initialize-GitRepoWithRemote -Dir $tmp
Push-Location $tmp
git checkout --quiet -b feature-branch
"feature code" | Set-Content "feature.txt"
git add feature.txt
git commit --quiet -m "Add feature file"
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.branch | Should -Be "feature-branch"
$json.mode | Should -Match "Feature branch diff"
$json.changed_files | Should -Contain "feature.txt"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
It "excludes deleted files (diff-filter=ACMR)" {
$tmp = New-TempDir
try {
Initialize-GitRepoWithRemote -Dir $tmp
Push-Location $tmp
"to delete" | Set-Content "delete-me.txt"
git add delete-me.txt
git commit --quiet -m "Add file to delete"
git push --quiet origin main
git checkout --quiet -b feature-delete
git rm --quiet delete-me.txt
"keep me" | Set-Content "keep.txt"
git add keep.txt
git commit --quiet -m "Delete and add"
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.changed_files | Should -Contain "keep.txt"
$json.changed_files | Should -Not -Contain "delete-me.txt"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
It "detects multiple changed files" {
$tmp = New-TempDir
try {
Initialize-GitRepoWithRemote -Dir $tmp
Push-Location $tmp
git checkout --quiet -b multi-files
"a" | Set-Content "file-a.txt"
"b" | Set-Content "file-b.txt"
$subDir = Join-Path $tmp "sub"
New-Item -ItemType Directory -Path $subDir -Force | Out-Null
"c" | Set-Content (Join-Path $subDir "file-c.txt")
git add .
git commit --quiet -m "Add multiple files"
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.changed_files | Should -Contain "file-a.txt"
$json.changed_files | Should -Contain "file-b.txt"
$json.changed_files | Should -Contain "sub/file-c.txt"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
It "detects renamed files (diff-filter includes R)" {
$tmp = New-TempDir
try {
Initialize-GitRepoWithRemote -Dir $tmp
Push-Location $tmp
"content" | Set-Content "original.txt"
git add original.txt
git commit --quiet -m "Add original"
git push --quiet origin main
git checkout --quiet -b feature-rename
git mv original.txt renamed.txt
git commit --quiet -m "Rename file"
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.changed_files | Should -Contain "renamed.txt"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
}
# ──────────────────────────────────────────────
# Mode A — Feature Branch + Uncommitted Changes
# ──────────────────────────────────────────────
Describe "Mode A - Uncommitted changes" {
It "includes staged uncommitted files" {
$tmp = New-TempDir
try {
Initialize-GitRepoWithRemote -Dir $tmp
Push-Location $tmp
git checkout --quiet -b feature-staged
"committed" | Set-Content "committed.txt"
git add committed.txt
git commit --quiet -m "Add committed file"
# Stage a new file without committing
"staged" | Set-Content "staged-only.txt"
git add staged-only.txt
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.changed_files | Should -Contain "committed.txt"
$json.changed_files | Should -Contain "staged-only.txt"
$json.mode | Should -Match "uncommitted"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
It "includes unstaged uncommitted files" {
$tmp = New-TempDir
try {
Initialize-GitRepoWithRemote -Dir $tmp
Push-Location $tmp
"original" | Set-Content "existing.txt"
git add existing.txt
git commit --quiet -m "Add existing file"
git push --quiet origin main
git checkout --quiet -b feature-unstaged
"committed on branch" | Set-Content "committed.txt"
git add committed.txt
git commit --quiet -m "Add committed file"
# Modify existing file without staging
"modified" | Set-Content "existing.txt"
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.changed_files | Should -Contain "committed.txt"
$json.changed_files | Should -Contain "existing.txt"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
It "deduplicates committed and uncommitted files" {
$tmp = New-TempDir
try {
Initialize-GitRepoWithRemote -Dir $tmp
Push-Location $tmp
git checkout --quiet -b feature-dedup
"v1" | Set-Content "shared.txt"
git add shared.txt
git commit --quiet -m "Add shared file"
# Modify the same file (unstaged)
"v2" | Set-Content "shared.txt"
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
($json.changed_files | Where-Object { $_ -eq "shared.txt" }).Count | Should -Be 1
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
}
# ──────────────────────────────────────────────
# Default Branch Detection Fallbacks
# ──────────────────────────────────────────────
Describe "Default branch detection" {
It "detects origin/main as default branch" {
$tmp = New-TempDir
try {
Initialize-GitRepoWithRemote -Dir $tmp
Push-Location $tmp
# Unset symbolic-ref to force fallback
git remote set-head origin --delete 2>$null
git checkout --quiet -b test-branch
"test" | Set-Content "test-file.txt"
git add test-file.txt
git commit --quiet -m "Add test file"
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.default_branch | Should -Be "main"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
It "falls back to Mode B when no remote default branch found" {
$tmp = New-TempDir
try {
Initialize-GitRepo -Dir $tmp
Push-Location $tmp
"change" | Set-Content "new-file.txt"
git add new-file.txt
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.mode | Should -Match "Working directory changes"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
It "detects origin/master as default branch when no origin/main" {
$tmp = New-TempDir
try {
# Create a bare repo with master as default
$bareDir = Join-Path $tmp "_bare_master"
New-Item -ItemType Directory -Path $bareDir -Force | Out-Null
Push-Location $bareDir
git init --bare --quiet
git symbolic-ref HEAD refs/heads/master
Pop-Location
Push-Location $tmp
git init --quiet
git config user.email "test@example.com"
git config user.name "Test"
git remote add origin $bareDir
New-Item -ItemType File -Path ".gitkeep" -Force | Out-Null
git add .
git checkout -b master --quiet 2>$null
git commit --quiet -m "Initial commit"
git push --quiet origin master 2>$null
# Remove symbolic-ref to force fallback
git remote set-head origin --delete 2>$null
# Create feature branch
git checkout --quiet -b test-branch
"test" | Set-Content "test-file.txt"
git add test-file.txt
git commit --quiet -m "Add test file"
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.default_branch | Should -Be "master"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
}
# ──────────────────────────────────────────────
# JSON Output Validation
# ──────────────────────────────────────────────
Describe "JSON output" {
It "has all required keys" {
$tmp = New-TempDir
try {
Initialize-GitRepo -Dir $tmp
Push-Location $tmp
"content" | Set-Content "new.txt"
git add new.txt
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.PSObject.Properties.Name | Should -Contain "branch"
$json.PSObject.Properties.Name | Should -Contain "default_branch"
$json.PSObject.Properties.Name | Should -Contain "mode"
$json.PSObject.Properties.Name | Should -Contain "changed_files"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
}
# ──────────────────────────────────────────────
# Edge Cases
# ──────────────────────────────────────────────
Describe "Edge cases" {
It "handles files with spaces in names" {
$tmp = New-TempDir
try {
Initialize-GitRepo -Dir $tmp
Push-Location $tmp
"content" | Set-Content "file with spaces.txt"
git add "file with spaces.txt"
$result = & pwsh -NoProfile -File $Script 2>&1
$LASTEXITCODE | Should -Be 0
($result -join "`n") | Should -Match "file with spaces.txt"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
It "handles nested directory changes" {
$tmp = New-TempDir
try {
Initialize-GitRepo -Dir $tmp
Push-Location $tmp
$nested = Join-Path $tmp "deep" "nested" "path"
New-Item -ItemType Directory -Path $nested -Force | Out-Null
"deep" | Set-Content (Join-Path $nested "file.txt")
git add .
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.changed_files | Should -Contain "deep/nested/path/file.txt"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
It "only reports ACMR files" {
$tmp = New-TempDir
try {
Initialize-GitRepo -Dir $tmp
Push-Location $tmp
"keep" | Set-Content "keep.txt"
"remove" | Set-Content "remove.txt"
git add .
git commit --quiet -m "Add files"
git rm --quiet remove.txt
"modified" | Set-Content "keep.txt"
"added" | Set-Content "added.txt"
git add .
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.changed_files | Should -Contain "keep.txt"
$json.changed_files | Should -Contain "added.txt"
$json.changed_files | Should -Not -Contain "remove.txt"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
}
# ──────────────────────────────────────────────
# Detached HEAD
# ──────────────────────────────────────────────
Describe "Detached HEAD" {
It "falls back to Mode B on detached HEAD" {
$tmp = New-TempDir
try {
Initialize-GitRepo -Dir $tmp
Push-Location $tmp
"file" | Set-Content "detached.txt"
git add detached.txt
git commit --quiet -m "Add file"
$commitHash = git rev-parse HEAD
git checkout --quiet $commitHash
"modified" | Set-Content "detached.txt"
$result = & pwsh -NoProfile -File $Script 2>&1
$LASTEXITCODE | Should -Be 0
($result -join "`n") | Should -Match "Working directory changes"
($result -join "`n") | Should -Match "detached.txt"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
}
# ──────────────────────────────────────────────
# Text Output Format Validation
# ──────────────────────────────────────────────
Describe "Text output format" {
It "text mode output has correct format" {
$tmp = New-TempDir
try {
Initialize-GitRepo -Dir $tmp
Push-Location $tmp
"content" | Set-Content "formatted.txt"
git add formatted.txt
$result = & pwsh -NoProfile -File $Script 2>&1
$LASTEXITCODE | Should -Be 0
$text = $result -join "`n"
$text | Should -Match "BRANCH:"
$text | Should -Match "DEFAULT_BRANCH:"
$text | Should -Match "MODE:"
$text | Should -Match "CHANGED_FILES:"
$text | Should -Match "formatted.txt"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
}
# ──────────────────────────────────────────────
# Branch Field Validation
# ──────────────────────────────────────────────
Describe "Branch field" {
It "branch field matches current branch name in Mode A" {
$tmp = New-TempDir
try {
Initialize-GitRepoWithRemote -Dir $tmp
Push-Location $tmp
git checkout --quiet -b my-feature-123
"x" | Set-Content "x.txt"
git add x.txt
git commit --quiet -m "commit"
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.branch | Should -Be "my-feature-123"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
It "branch field shows current branch on default branch (Mode B)" {
$tmp = New-TempDir
try {
Initialize-GitRepo -Dir $tmp
Push-Location $tmp
"change" | Set-Content "file.txt"
git add file.txt
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.branch | Should -Be "main"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
}
# ──────────────────────────────────────────────
# Special Characters in Filenames
# ──────────────────────────────────────────────
Describe "Special character filenames" {
It "handles filenames with special characters in JSON mode" {
$tmp = New-TempDir
try {
Initialize-GitRepo -Dir $tmp
Push-Location $tmp
# Create files with special characters
"a" | Set-Content "file (1).txt"
"b" | Set-Content "file's.txt"
"c" | Set-Content "file&more.txt"
git add .
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.changed_files | Should -Contain "file (1).txt"
$json.changed_files | Should -Contain "file's.txt"
$json.changed_files | Should -Contain "file&more.txt"
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
It "handles filenames with double quotes in JSON mode" -Skip:($IsWindows -or $env:OS -eq 'Windows_NT') {
$tmp = New-TempDir
try {
Initialize-GitRepo -Dir $tmp
Push-Location $tmp
# Create a file with a double quote in its name
# (skipped on Windows — NTFS does not allow " in filenames)
$fname = 'file"quote.txt'
[System.IO.File]::WriteAllText((Join-Path $tmp $fname), "content")
git add .
$result = & pwsh -NoProfile -File $Script -Json 2>&1
$LASTEXITCODE | Should -Be 0
$json = $result | ConvertFrom-Json
$json.changed_files.Count | Should -Be 1
$json.changed_files[0] | Should -Match 'quote'
} finally {
Pop-Location
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
}
}

View File

@@ -1,4 +1 @@
{
"feature_directory": "specs/002-node-list-layout"
}
{"feature_directory":"specs/018-compose-screenshot-testing"}

View File

@@ -1,20 +1,15 @@
<!--
SYNC IMPACT REPORT
==================
Version change: 1.2.2 → 1.2.3
Version change: 1.1.0 → 1.1.1
Modified principles:
- Principle I: "where feasible" → "unless the platform API has no KMP
equivalent" (interpretability fix)
- Principle V: "significant UI changes" → "new screens, layout
restructuring, or navigation changes" (interpretability fix)
- Principle IX: "~5 logical commits" → "5 logical commits" (precision)
Compressed sections:
- Development Workflow: replaced verbose procedural content with
3-line summary referencing AGENTS.md and skills
- Architecture Constraints: replaced verbose tech stack listings with
constraint summaries referencing kmp-architecture SKILL
Removed sections: None (content preserved in AGENTS.md and skills)
Templates requiring updates: None.
- Governance (compliance review wording clarified to require plan-level constitution checks)
Added sections: None.
Removed sections: None.
Templates requiring updates:
✅ .specify/templates/plan-template.md — Constitution Check now enumerates the six project principles.
✅ .specify/templates/spec-template.md — Validated against current constitution; no template changes required.
✅ .specify/templates/tasks-template.md — Added required verification and design review task guidance.
Follow-up TODOs: None.
-->
@@ -24,67 +19,53 @@ Follow-up TODOs: None.
### I. Kotlin Multiplatform Core
Business logic MUST reside exclusively in `commonMain` source sets.
KMP-equivalent libraries MUST be used in place of JVM/Android-specific
APIs:
Business logic MUST reside exclusively in `commonMain` source sets. KMP-equivalent libraries
MUST be used in place of JVM/Android-specific APIs:
- MUST use Okio (not `java.io`), Ktor (not `java.net`/OkHttp in common),
Mutex/atomicfu (not `java.util.concurrent`), Room KMP, DataStore KMP,
and Koin 4.2+ with K2 Compiler Plugin.
- MUST use Okio (not `java.io`), Ktor (not `java.net`/OkHttp in common), Mutex/atomicfu
(not `java.util.concurrent`), Room KMP, DataStore KMP, and Koin 4.2+.
- MUST NOT import `java.*` or `android.*` in any `commonMain` module.
- Platform-specific implementations belong in `androidMain`/`desktopMain`
actual declarations only.
- Platform capabilities MUST prefer interface + DI over `expect`/`actual`
unless the platform API has no KMP equivalent.
- Rationale: The project goal is multi-platform parity (Android, Desktop,
iOS). Framework bleed in `commonMain` breaks compilability on non-Android
targets and undermines the entire decoupling effort.
- Platform-specific implementations belong in `androidMain`/`desktopMain` actual
declarations only.
- Rationale: The project goal is multi-platform parity (Android, Desktop, iOS). Framework
bleed in `commonMain` breaks compilability on non-Android targets and undermines the
entire decoupling effort.
### II. Zero Lint Tolerance
All code contributions MUST pass static analysis before merge:
- `./gradlew spotlessApply` MUST be run and `spotlessCheck` MUST pass with
no violations.
- `./gradlew spotlessApply` MUST be run and `spotlessCheck` MUST pass with no violations.
- `detekt` MUST pass with no new violations introduced.
- A task or PR is considered incomplete if either check fails.
- Rationale: Consistent code style and static analysis gates prevent
technical debt accumulation and catch bugs that tests alone miss.
- Rationale: Consistent code style and static analysis gates prevent technical debt
accumulation and catch bugs that tests alone miss.
### III. Compose Multiplatform UI
All UI MUST use JetBrains Compose Multiplatform, not Android-only Jetpack
Compose APIs:
All UI MUST use JetBrains Compose Multiplatform, not Android-only Jetpack Compose APIs:
- MUST use `MeshtasticNavDisplay` and `NavigationBackHandler` for
navigation across all entry points (not Android's `BackHandler`).
- Navigation routes MUST be `@Serializable sealed interface` types defined
in `core:navigation`.
- Feature navigation graphs MUST be extension functions on
`EntryProviderScope<NavKey>` in `commonMain`.
- Floats MUST be pre-formatted using `NumberFormatter.format()` before
display — CMP only supports `%N$s` (string) and `%N$d` (int) format
specifiers.
- UI MUST compile and render correctly on all supported targets (Android,
Compose Desktop).
- Material 3 Adaptive MUST be used for responsive layouts.
- Rationale: Compose Multiplatform ensures UI consistency across platforms
and enforces the project's multi-platform architecture goal.
- MUST use `MeshtasticNavDisplay` and `NavigationBackHandler` for navigation across all
entry points.
- Floats MUST be pre-formatted using `NumberFormatter.format()` before display in any
composable.
- UI MUST compile and render correctly on all supported targets (Android, Compose Desktop).
- Rationale: Compose Multiplatform ensures UI consistency across platforms and enforces the
project's multi-platform architecture goal.
### IV. Privacy First
The application handles sensitive mesh network data; user privacy MUST be
protected at all times:
The application handles sensitive mesh network data; user privacy MUST be protected at all
times:
- MUST NOT log or expose PII, location data, or cryptographic keys in
logs, crash reports, or any debug output.
- Secrets MUST be git-ignored and MUST NOT be committed to the repository
under any circumstances.
- MUST NOT log or expose PII, location data, or cryptographic keys in logs, crash reports,
or any debug output.
- Secrets MUST be git-ignored and MUST NOT be committed to the repository under any
circumstances.
- `core/proto` is a read-only upstream submodule (`meshtastic/protobufs`). MUST NOT modify
`.proto` files directly; proto changes require an upstream issue labeled `upstream`.
- Rationale: Meshtastic users rely on the mesh for private, off-grid
communications. Data leaks could endanger users in sensitive or
adversarial deployments.
- Rationale: Meshtastic users rely on the mesh for private, off-grid communications. Data
leaks could endanger users in sensitive or adversarial deployments.
### V. Design Standards Compliance
@@ -92,139 +73,82 @@ All user-facing UI MUST conform to the Meshtastic Client Design Standards:
- The canonical reference lives at:
`https://raw.githubusercontent.com/meshtastic/design/refs/heads/master/standards/meshtastic_design_standards_latest.md`
- New screens, layout restructuring, or navigation changes MUST be
reviewed against the design standards before merge.
- Deviations from the design standards require explicit justification in
the PR description with a rationale for why the standard cannot or
should not be followed.
- Rationale: Consistent cross-platform UX across Android, iOS, and other
clients ensures users have a predictable experience regardless of
platform. The design standards are maintained collaboratively across
all Meshtastic client teams.
- New screens and significant UI changes MUST be reviewed against the design standards
before merge.
- Deviations from the design standards require explicit justification in the PR description
with a rationale for why the standard cannot or should not be followed.
- Rationale: Consistent cross-platform UX across Android, iOS, and other clients ensures
users have a predictable experience regardless of platform. The design standards are
maintained collaboratively across all Meshtastic client teams.
### VI. Verify Before Push
Local verification MUST complete successfully before any `git push`:
- MUST run the full verification command:
`./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests`
- Both `test` AND `allTests` are required: `allTests` covers KMP modules;
`test` covers pure-Android modules. Neither alone catches everything.
- MUST run `./gradlew spotlessApply spotlessCheck detekt` plus relevant module `:test`
tasks for all modules touched.
- After pushing, CI status MUST be confirmed via `gh pr checks <PR>` or
`gh run list --branch <branch> --limit 5`. Phrases like "CI should be
green" are explicitly prohibited — check and confirm.
- Rationale: CI has failed repeatedly due to skipped local checks.
Verification is a hard gate, not an optimistic assumption.
### VII. Coroutine Safety
Coroutine code MUST use project-standard utilities that preserve
structured concurrency and cancellation semantics:
- MUST use `safeCatching {}` (from `core:common`) instead of
`runCatching {}` in suspend/coroutine code — `runCatching` silently
swallows `CancellationException`, breaking structured concurrency.
- MUST use `org.meshtastic.core.common.util.ioDispatcher` — never
`Dispatchers.IO` directly. Inject `CoroutineDispatchers` from
`core:di` for testability.
- Rationale: Incorrect exception handling in coroutines causes silent
failures and resource leaks. Centralizing dispatcher injection enables
deterministic testing.
### VIII. Resource Discipline
All user-visible text, icons, and formatting MUST use project-standard
resource APIs:
- All strings MUST reside in
`core/resources/src/commonMain/composeResources/values/strings.xml`.
Use `stringResource(Res.string.key)` — never hardcoded strings in UI.
- After adding any string resource, MUST run
`python3 scripts/sort-strings.py` to maintain alphabetical order and
regenerate `strings-index.txt`.
- Consult `strings-index.txt` before reading large string files to
minimize context waste.
- MUST use `MeshtasticIcons` (from `core/ui/icon/`) instead of
`material.icons.Icons` for all iconography.
- Rationale: Centralized resources enable localization via Crowdin,
maintain consistency with the Meshtastic design language, and prevent
scattered hardcoded strings that resist translation.
### IX. Branch & Scope Hygiene
All branches and PRs MUST follow naming conventions and scope discipline:
- Branch names MUST start with one of: `feat/`, `fix/`, `chore/`, `docs/`,
`build/`, `ci/`, `refactor/`, `test/`, `deps/`, or a numeric spec prefix
(e.g., `002-feature-name` for spec-driven feature branches created by
Spec Kit).
- Branches MUST be created off fetched upstream: `git fetch origin &&
git checkout -b <name> origin/main`. Never branch from a personal
fork's `main` — it may be stale.
- When a working branch grows beyond 5 logical commits or spans
unrelated concerns, contributors MUST propose a fresh branch off
`origin/main`, cherry-pick high-impact changes, and defer tangential
work to follow-up PRs.
- Fixup commits MUST be squashed before pushing.
- Rationale: Focused branches reduce review burden, minimize merge
conflicts, and maintain a clean git history. Upstream-based branching
prevents stale-fork drift.
`gh run list --branch <branch> --limit 5`. Phrases like "CI should be green" are
explicitly prohibited.
- Rationale: CI has failed repeatedly due to skipped local checks. Verification is a hard
gate, not an optimistic assumption.
## Development Workflow
Follow the bootstrap, verification, and workflow procedures defined in
AGENTS.md `<process_essentials>` and the relevant `.skills/` playbooks
(`project-overview`, `testing-ci`, `new-branch`, `code-review`). All
workflow steps there are non-negotiable.
The following workflow steps are non-negotiable for all contributors and agents:
Key constraints reiterated here for governance compliance:
- Baseline verification (`spotlessApply spotlessCheck detekt assembleDebug
test allTests`) MUST pass before any push or PR.
- Gradle task naming differs between KMP and Android-only modules — see
`.github/copilot-instructions.md` §Gradle task naming for the
authoritative table.
- Two app flavors (`fdroid` / `google`) use different signing keys. Only
one can be installed at a time — uninstall before switching.
- **Bootstrap First**: The mandatory bootstrap steps in `.skills/project-overview/SKILL.md`
MUST be executed before any build operation in a new session.
- **Baseline Verification**: Before any PR is opened or pushed, run:
`./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests`
- **String Resources**: After adding any string resource, run
`python3 scripts/sort-strings.py` to maintain alphabetical organization and regenerate
`strings-index.txt`. Consult `strings-index.txt` before reading large string files.
- **Memory Persistence**: `.agent_memory/session_context.md` MUST be updated at the end of
every agent session or major task to preserve context across sessions.
- **Plan Before Execution**: Complex refactors MUST have a plan written in `.agent_plans/`
(git-ignored) before execution begins.
- **Context Discipline**: Agents MUST NOT read binary files (PNG, MP3, etc.) or vacuum the
entire codebase for localized fixes. Limit context reads to relevant modules.
## Architecture Constraints
Module boundaries and technology choices are fixed. See AGENTS.md
`<context_and_memory>` and `.skills/kmp-architecture/SKILL.md` for the
full stack and module map.
The following module boundaries and technology choices are fixed for this project:
Key constraints:
- No alternative DI, networking, or BLE frameworks may be introduced.
- Java source files MUST NOT be introduced in KMP modules.
- No reactive frameworks other than Coroutines/Flow in `commonMain`.
- All navigation MUST use `MeshtasticNavDisplay`.
- Gradle Kotlin DSL with convention plugins in `build-logic/`. Two
flavors: `fdroid` (OSS) / `google` (Maps + DataDog).
- **KMP Modules**: `core:domain` (business logic), `core:data` (repositories),
`core:database` (Room KMP), `core:datastore` (preferences), `core:network` (Ktor),
`core:ble` (Kable multiplatform BLE).
- **State Management**: Unidirectional Data Flow (UDF) with ViewModels, Kotlin Coroutines,
and Flow. No reactive frameworks other than Coroutines/Flow in `commonMain`.
- **Dependency Injection**: Koin 4.2+ with Koin Annotations and the K2 Compiler Plugin.
No alternative DI framework may be introduced.
- **Navigation**: JetBrains Navigation 3 for multiplatform routing with RESTful deep
linking. All navigation MUST use `MeshtasticNavDisplay`.
- **Data Protocol**: Protobuf for device communications (read-only upstream submodule).
Room KMP for local persistence. DataStore for user preferences.
- **Language & Toolchain**: Kotlin 2.3+ targeting JDK 21. Java source files MUST NOT be
introduced in KMP modules.
## Governance
This constitution supersedes all other practices, coding guidelines, and
agent instructions. `AGENTS.md` is the authoritative source of truth. The
files `.github/copilot-instructions.md`, `CLAUDE.md`, and `GEMINI.md`
MUST redirect to `AGENTS.md` and MUST NOT diverge from it.
This constitution supersedes all other practices, coding guidelines, and agent instructions.
`AGENTS.md` is the authoritative source of truth. The files
`.github/copilot-instructions.md`, `CLAUDE.md`, and `GEMINI.md` MUST redirect to
`AGENTS.md` and MUST NOT diverge from it.
**Amendment Procedure**:
1. Propose the amendment with rationale and a migration plan in a PR
description.
2. Update `AGENTS.md` and this constitution atomically in the same
commit.
1. Propose the amendment with rationale and a migration plan in a PR description.
2. Update `AGENTS.md` and this constitution atomically in the same commit.
3. Increment `CONSTITUTION_VERSION` per the versioning policy below.
4. All PRs and code reviews MUST verify compliance with the current
constitution version.
4. All PRs and code reviews MUST verify compliance with the current constitution version.
**Versioning Policy**:
- MAJOR: Backward-incompatible principle removal or fundamental
redefinition.
- MAJOR: Backward-incompatible principle removal or fundamental redefinition.
- MINOR: New principle or section added, or materially expanded guidance.
- PATCH: Clarifications, wording fixes, or non-semantic refinements.
**Compliance Review**: Every PR description MUST include a Constitution
Check confirming all nine principles were evaluated. Complexity violations
require explicit justification in the Complexity Tracking table of the
plan document.
**Compliance Review**: Every implementation plan and PR description MUST include a
Constitution Check confirming all six principles were evaluated. Complexity violations
require explicit justification in the Complexity Tracking table of the plan document.
**Version**: 1.2.3 | **Ratified**: 2026-05-07 | **Last Amended**: 2026-05-09
**Version**: 1.1.1 | **Ratified**: 2026-05-07 | **Last Amended**: 2026-05-08

View File

@@ -13,37 +13,41 @@
<!--
ACTION REQUIRED: Replace the content in this section with the technical details
for the feature. Most fields below are pre-filled with project defaults —
adjust only what's feature-specific.
for the project. The structure here is presented in advisory capacity to guide
the iteration process.
-->
**Language/Version**: Kotlin 2.3+ targeting JDK 21
**Primary Dependencies**: Compose Multiplatform, Material 3 Adaptive, Koin 4.2+ (K2 Compiler Plugin), Room KMP, DataStore KMP
**Storage**: [DataStore KMP for preferences / Room KMP for entities / N/A]
**Testing**: KMP `allTests` for `feature:*` and `core:*` modules; `testFdroidDebugUnitTest` for `app`
**Target Platform**: Android, Desktop (JVM), iOS — all via `commonMain`
**Project Type**: Mobile/desktop app (Kotlin Multiplatform)
**Performance Goals**: [e.g., 60fps scrolling, <1s response or NEEDS CLARIFICATION]
**Constraints**: All UI in `commonMain`; no `java.*`/`android.*` in common; CMP float pre-formatting via `NumberFormatter.format()`
**Scale/Scope**: [e.g., N new files, M modified files across feature/X, core/Y]
**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]
**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]
**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
**Project Type**: [e.g., library/cli/web-service/mobile-app/compiler/desktop-app or NEEDS CLARIFICATION]
**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]
**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Kotlin Multiplatform Core | ⬜ | All code in `commonMain`. No `java.*`/`android.*` imports. |
| II. Zero Lint Tolerance | ⬜ | `spotlessApply` + `detekt` required before merge. |
| III. Compose Multiplatform UI | ⬜ | CMP composables, `NumberFormatter.format()` for floats, Navigation 3 patterns. |
| IV. Privacy First | ⬜ | No PII/location/key logging. Proto submodule read-only. |
| V. Design Standards Compliance | ⬜ | UI-GATE review required before UI work. |
| VI. Verify Before Push | ⬜ | Full verification: `./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests`. |
| VII. Coroutine Safety | ⬜ | `safeCatching {}` not `runCatching {}`. Project `ioDispatcher` not `Dispatchers.IO`. |
| VIII. Resource Discipline | ⬜ | `stringResource(Res.string.key)`, `MeshtasticIcons`, `sort-strings.py` after adding strings. |
| IX. Branch & Scope Hygiene | ⬜ | Branch prefix, upstream base, ~5-commit scope limit. |
- **I. Kotlin Multiplatform Core**: Identify every touched source set/module and confirm
all business logic remains in `commonMain`; document any platform-specific work that is
isolated to `androidMain`/platform shells.
- **II. Zero Lint Tolerance**: List the exact formatting and static-analysis commands that
will be run for the touched modules, at minimum `spotlessCheck` and `detekt`.
- **III. Compose Multiplatform UI**: If UI is in scope, confirm the design uses Compose
Multiplatform patterns, `MeshtasticNavDisplay`/`NavigationBackHandler` where relevant,
and pre-formats floats with `NumberFormatter.format()`.
- **IV. Privacy First**: Confirm the change does not log or expose PII, location data,
cryptographic keys, or modify the read-only `core/proto` submodule.
- **V. Design Standards Compliance**: For any user-facing UI, record how the design was
checked against the Meshtastic Client Design Standards, or explicitly mark the gate N/A.
- **VI. Verify Before Push**: Record the exact local verification commands and the expected
post-push CI check command (`gh pr checks` or `gh run list`) before implementation starts.
**Gate Result**: [⬜ Pending / ✅ All principles satisfied / ❌ Violations requiring justification]
If any gate cannot be met, the exception MUST be justified in the Complexity Tracking
section below and explicitly called out in the PR description.
## Project Structure
@@ -60,91 +64,51 @@ specs/[###-feature]/
```
### Source Code (repository root)
<!--
ACTION REQUIRED: Fill in with the actual files affected by this feature.
Use the module layout below as a guide. Delete unused modules.
ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
for this feature. Delete unused options and expand the chosen structure with
real paths (e.g., apps/admin, packages/something). The delivered plan must
not include Option labels.
-->
```text
feature/[name]/ ← Primary changes
├── src/commonMain/kotlin/org/meshtastic/feature/[name]/
│ ├── component/
│ │ ├── [ExistingComposable].kt ← Modify
│ │ └── [NewComposable].kt ← NEW
│ ├── list/
│ │ ├── [Screen].kt ← Modify — [description]
│ │ └── [ViewModel].kt ← Modify — [description]
│ └── model/
│ └── [NewModel].kt ← NEW — [description]
# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT)
src/
├── models/
├── services/
├── cli/
── lib/
core/[module]/ ← Core layer changes
├── src/commonMain/kotlin/org/meshtastic/core/[module]/
│ └── [File].kt ← Modify — [description]
tests/
├── contract/
├── integration/
└── unit/
feature/settings/ ← Settings integration (if applicable)
├── src/commonMain/kotlin/org/meshtastic/feature/settings/
│ └── [SettingsSection].kt ← NEW — [description]
# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected)
backend/
├── src/
│ ├── models/
│ ├── services/
│ └── api/
└── tests/
core/resources/
── src/commonMain/composeResources/values/strings.xml ← Add string resources
frontend/
── src/
│ ├── components/
│ ├── pages/
│ └── services/
└── tests/
# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected)
api/
└── [same as backend above]
ios/ or android/
└── [platform-specific structure: feature modules, UI flows, platform tests]
```
**Structure Decision**: [Document the selected structure and why existing modules
are modified rather than creating new ones, per KMP module architecture.]
## Module Impact
| Module | Change Type | Files Affected | Risk |
|--------|-------------|----------------|------|
| `feature/[name]` | New + Modify | [count] | [Low/Medium/High] |
| `core/[module]` | Modify | [count] | [Low/Medium/High] |
| `core/resources` | Modify | 1 file (strings.xml) | Low |
## Integration Points
<!--
Document how this feature integrates with existing systems:
navigation routes, DataStore keys, DI modules, etc.
-->
## Design Constraints
<!--
List technical constraints specific to this feature.
Include M3/Expressive, accessibility, and CMP constraints.
-->
- All UI lives in `commonMain` — not platform-specific
- Strings accessed via `stringResource(Res.string.key)` — never hardcoded
- Icons use `MeshtasticIcons` exclusively (from `core/ui/icon/`)
- Error handling uses `safeCatching {}` not `runCatching {}`
- Dispatchers via `org.meshtastic.core.common.util.ioDispatcher`
- Float values must be pre-formatted with `NumberFormatter.format()` (CMP constraint)
## Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| [Risk description] | [Low/Med/High] | [Low/Med/High] | [Mitigation with task reference] |
## Phase Alignment with Tasks
<!--
ACTION REQUIRED: Fill in after tasks.md is generated by /speckit.tasks.
Reference actual task IDs from tasks.md — do NOT use plan-internal numbering.
-->
| Phase | Purpose | Key Tasks | Dependencies |
|-------|---------|-----------|--------------|
| 1. Setup | [Purpose] | [Task IDs] | None |
| N. Polish | [Purpose] | [Task IDs] | All prior phases |
### Critical Path
```
Phase 1 → Phase 2 → ... → Phase N
```
**Structure Decision**: [Document the selected structure and reference the real
directories captured above]
## Complexity Tracking
@@ -152,4 +116,5 @@ Phase 1 → Phase 2 → ... → Phase N
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| *None* | — | — |
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |

View File

@@ -8,11 +8,17 @@ description: "Task list template for feature implementation"
**Input**: Design documents from `/specs/[###-feature-name]/`
**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
**Tests**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification.
**Tests**: The examples below include test tasks. New automated tests are OPTIONAL and
should only be included when requested by the feature specification or when needed to
verify the implementation safely.
**Verification**: Every generated task list MUST include constitution-required validation
tasks for formatting, static analysis, and the relevant compile/test commands for the
touched modules before work is considered complete.
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
## Format: `[ID] [P?] [Story?] Description`
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
@@ -20,13 +26,10 @@ description: "Task list template for feature implementation"
## Path Conventions
- **KMP commonMain**: `feature/[name]/src/commonMain/kotlin/org/meshtastic/feature/[name]/`
- **Core modules**: `core/[module]/src/commonMain/kotlin/org/meshtastic/core/[module]/`
- **Core UI**: `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/`
- **Settings**: `feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/`
- **Resources**: `core/resources/src/commonMain/composeResources/values/strings.xml`
- **Tests (KMP)**: `feature/[name]/src/commonTest/kotlin/`
- **Tests (Android-only)**: `app/src/test/`
- **Single project**: `src/`, `tests/` at repository root
- **Web app**: `backend/src/`, `frontend/src/`
- **Mobile**: `api/src/`, `ios/src/` or `android/src/`
- Paths shown below assume single project - adjust based on plan.md structure
<!--
============================================================================
@@ -49,27 +52,30 @@ description: "Task list template for feature implementation"
## Phase 1: Setup (Shared Infrastructure)
**Purpose**: Design gate, enums, DataStore/Room keys, and string resources required by all user stories.
**Purpose**: Project initialization and basic structure
- [ ] T001 `[UI-GATE]` Review `.skills/design-standards/SKILL.md` and upstream Meshtastic design standards; record constraints for new composables. This task blocks all UI work.
- [ ] T002 [P] Create model/enum files in `feature/[name]/src/commonMain/.../model/`
- [ ] T003 [P] Add DataStore/Room preference keys in `core/prefs/` or `core/datastore/`
- [ ] T004 Add string resources to `core/resources/src/commonMain/composeResources/values/strings.xml`. Run `python3 scripts/sort-strings.py` after.
**Dependencies**: T001 blocks all UI phases. T002 and T003 are independent.
**Checkpoint**: Preference infrastructure ready — all user stories can now begin.
- [ ] T001 Create project structure per implementation plan
- [ ] T002 Initialize [language] project with [framework] dependencies
- [ ] T003 [P] Configure linting and formatting tools
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Core infrastructure, accessibility fixes, and ViewModel wiring that MUST complete before ANY user story can ship.
**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented
- [ ] T005 [Required existing file changes — e.g., accessibility fixes, ViewModel wiring]
- [ ] T006 [Modify ViewModel to expose new StateFlows from DataStore]
**⚠️ CRITICAL**: No user story work can begin until this phase is complete
**Dependencies**: Phase 1 must complete first.
**Checkpoint**: Foundation ready — user story implementation can begin.
Examples of foundational tasks (adjust based on your project):
- [ ] T004 Setup database schema and migrations framework
- [ ] T005 [P] Implement authentication/authorization framework
- [ ] T006 [P] Setup API routing and middleware structure
- [ ] T007 Create base models/entities that all stories depend on
- [ ] T008 Configure error handling and logging infrastructure
- [ ] T009 Setup environment configuration management
**Checkpoint**: Foundation ready - user story implementation can now begin in parallel
---
@@ -79,13 +85,23 @@ description: "Task list template for feature implementation"
**Independent Test**: [How to verify this story works on its own]
### Tests for User Story 1 (OPTIONAL - only if tests requested) ⚠️
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py
- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py
### Implementation for User Story 1
- [ ] T010 [P] [US1] Create composable in `feature/[name]/src/commonMain/.../component/[Name].kt`
- [ ] T011 [US1] Implement core UI logic (depends on T010)
- [ ] T012 [US1] Wire into screen in `feature/[name]/src/commonMain/.../list/[Screen].kt`
- [ ] T012 [P] [US1] Create [Entity1] model in src/models/[entity1].py
- [ ] T013 [P] [US1] Create [Entity2] model in src/models/[entity2].py
- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013)
- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py
- [ ] T016 [US1] Add validation and error handling
- [ ] T017 [US1] Add logging for user story 1 operations
**Checkpoint**: User Story 1 complete — [core feature] works end-to-end.
**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently
---
@@ -95,12 +111,40 @@ description: "Task list template for feature implementation"
**Independent Test**: [How to verify this story works on its own]
### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️
- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py
- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py
### Implementation for User Story 2
- [ ] T020 [US2] Implement [feature component]
- [ ] T021 [US2] Add settings UI in `feature/settings/src/commonMain/.../[Name].kt`
- [ ] T020 [P] [US2] Create [Entity] model in src/models/[entity].py
- [ ] T021 [US2] Implement [Service] in src/services/[service].py
- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py
- [ ] T023 [US2] Integrate with User Story 1 components (if needed)
**Checkpoint**: User Story 2 complete.
**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently
---
## Phase 5: User Story 3 - [Title] (Priority: P3)
**Goal**: [Brief description of what this story delivers]
**Independent Test**: [How to verify this story works on its own]
### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️
- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py
- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py
### Implementation for User Story 3
- [ ] T026 [P] [US3] Create [Entity] model in src/models/[entity].py
- [ ] T027 [US3] Implement [Service] in src/services/[service].py
- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py
**Checkpoint**: All user stories should now be independently functional
---
@@ -110,15 +154,15 @@ description: "Task list template for feature implementation"
## Phase N: Polish & Cross-Cutting Concerns
**Purpose**: Performance validation, edge case hardening, tests, and verification.
**Purpose**: Improvements that affect multiple user stories
- [ ] TXXX [P] Verify performance targets (e.g., 60fps scrolling with N+ items)
- [ ] TXXX [P] Ensure all float values use `NumberFormatter.format()` before display
- [ ] TXXX Validate edge cases documented in spec
- [ ] TXXX [P] Write unit tests in `feature/[name]/src/commonTest/`
- [ ] TXXX [P] Write Compose UI tests in `feature/[name]/src/commonTest/`
- [ ] TXXX Run `./gradlew :feature:[name]:allTests :core:[module]:allTests`
- [ ] TXXX Run `./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests`
- [ ] TXXX [P] Documentation updates in docs/
- [ ] TXXX Code cleanup and refactoring
- [ ] TXXX [P] Review user-facing UI against Meshtastic design standards and document any approved deviations
- [ ] TXXX [P] Confirm no logs, telemetry, or config changes expose PII, location data, secrets, or modify `core/proto`
- [ ] TXXX [P] Run constitution-required verification (`spotlessCheck`, `detekt`, and relevant compile/test tasks)
- [ ] TXXX [P] Additional unit/integration tests (if requested) in the appropriate test source sets
- [ ] TXXX Validate quickstart/README or developer workflow docs if contributor workflow changed
---
@@ -126,28 +170,48 @@ description: "Task list template for feature implementation"
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies — T001 UI-GATE blocks all UI phases
- **Foundational (Phase 2)**: Depends on Phase 1 — BLOCKS all user stories
- **User Stories (Phase 3+)**: Depend on Phase 2 completion
- User stories proceed sequentially by priority (P1 → P2 → P3) unless independent
- **Polish (Final Phase)**: Depends on all user story phases
- **Setup (Phase 1)**: No dependencies - can start immediately
- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories
- **User Stories (Phase 3+)**: All depend on Foundational phase completion
- User stories can then proceed in parallel (if staffed)
- Or sequentially in priority order (P1 → P2 → P3)
- **Polish (Final Phase)**: Depends on all desired user stories being complete
### User Story Dependencies
- **User Story 1 (P1)**: Can start after Foundational No dependencies on other stories
- **User Story 2 (P2)**: [May depend on US1 scaffold / Can start after Foundational]
- **User Story 3 (P3)**: [May depend on US1/US2 / Independent after Foundational]
- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories
- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - May integrate with US1 but should be independently testable
- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - May integrate with US1/US2 but should be independently testable
### Critical Path
### Within Each User Story
```
Phase 1 → Phase 2 → Phase 3 (US1) → ... → Phase N (Polish)
```
- Tests (if included) MUST be written and FAIL before implementation
- Models before services
- Services before endpoints
- Core implementation before integration
- Story complete before moving to next priority
### Parallel Opportunities
```
[List tasks that can run in parallel — different files, no dependencies]
- All Setup tasks marked [P] can run in parallel
- All Foundational tasks marked [P] can run in parallel (within Phase 2)
- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows)
- All tests for a user story marked [P] can run in parallel
- Models within a story marked [P] can run in parallel
- Different user stories can be worked on in parallel by different team members
---
## Parallel Example: User Story 1
```bash
# Launch all tests for User Story 1 together (if tests requested):
Task: "Contract test for [endpoint] in tests/contract/test_[name].py"
Task: "Integration test for [user journey] in tests/integration/test_[name].py"
# Launch all models for User Story 1 together:
Task: "Create [Entity1] model in src/models/[entity1].py"
Task: "Create [Entity2] model in src/models/[entity2].py"
```
---
@@ -156,25 +220,39 @@ Phase 1 → Phase 2 → Phase 3 (US1) → ... → Phase N (Polish)
### MVP First (User Story 1 Only)
1. Complete Phase 1: Setup (design gate + infrastructure)
2. Complete Phase 2: Foundational (ViewModel + accessibility)
1. Complete Phase 1: Setup
2. Complete Phase 2: Foundational (CRITICAL - blocks all stories)
3. Complete Phase 3: User Story 1
4. **STOP and VALIDATE**: Test end-to-end, verify persistence, check TalkBack
5. Ship as MVP
4. **STOP and VALIDATE**: Test User Story 1 independently
5. Deploy/demo if ready
### Incremental Delivery
1. Phase 1 + Phase 2 → Foundation ready
2. Phase 3: US1 → **MVP shippable**
3. Phase 4: US2 → Enhanced experience
4. Phase N: Polish → Tests + verification → Merge-ready
1. Complete Setup + Foundational → Foundation ready
2. Add User Story 1 → Test independently → Deploy/Demo (MVP!)
3. Add User Story 2 → Test independently → Deploy/Demo
4. Add User Story 3 → Test independently → Deploy/Demo
5. Each story adds value without breaking previous stories
### Parallel Team Strategy
With multiple developers:
1. All complete Phase 1 + Phase 2 together
1. Team completes Setup + Foundational together
2. Once Foundational is done:
- Developer A: US1 → US2 → ... *(critical path)*
- Developer B: Independent stories *(parallel)*
3. Converge at Phase N (Polish)
- Developer A: User Story 1
- Developer B: User Story 2
- Developer C: User Story 3
3. Stories complete and integrate independently
---
## Notes
- [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story for traceability
- Each user story should be independently completable and testable
- Verify tests fail before implementing
- Commit after each task or logical group
- Stop at any checkpoint to validate story independently
- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence

View File

@@ -49,5 +49,5 @@ You are an expert Android/KMP engineer. Maintain architectural boundaries, use M
<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan
at `specs/002-node-list-layout/plan.md`
at `specs/018-compose-screenshot-testing/plan.md`
<!-- SPECKIT END -->

View File

@@ -16,7 +16,6 @@
<ID>ComposableParamOrder:SatelliteCountInfo.kt:@Composable fun SatelliteCountInfo</ID>
<ID>ComposableParamOrder:SignalInfo.kt:@Composable fun SignalInfo</ID>
<ID>ComposableParamOrder:SwitchPreference.kt:@Composable fun SwitchPreference</ID>
<ID>CompositionLocalAllowlist:ContrastLevel.kt:/** * Composition local providing the current [ContrastLevel]. * * Read by components that need to adapt their rendering for accessibility (e.g. message bubbles, signal indicators). */ val LocalContrastLevel = staticCompositionLocalOf { ContrastLevel.STANDARD }</ID>
<ID>CompositionLocalAllowlist:LocalAnalyticsIntroProvider.kt:val LocalAnalyticsIntroProvider = compositionLocalOf&lt;@Composable () -> Unit> { {} }</ID>
<ID>CompositionLocalAllowlist:LocalBarcodeScannerProvider.kt:val LocalBarcodeScannerProvider = compositionLocalOf&lt;@Composable (onResult: (String?) -> Unit) -> BarcodeScanner> { { object : BarcodeScanner { override fun startScan() { // Default NO-OP } } } }</ID>
<ID>CompositionLocalAllowlist:LocalBarcodeScannerProvider.kt:val LocalBarcodeScannerSupported = compositionLocalOf { false }</ID>
@@ -32,7 +31,6 @@
<ID>CompositionLocalAllowlist:MapViewProvider.kt:val LocalMapViewProvider = compositionLocalOf&lt;MapViewProvider?> { null }</ID>
<ID>ContentSlotReused:AdaptiveTwoPane.kt:second: @Composable ColumnScope.() -> Unit</ID>
<ID>FunctionTypeModifierSpacing:Theme.kt:@Composable()</ID>
<ID>LambdaParameterEventTrailing:MainAppBar.kt:onClickChip: (Node) -> Unit</ID>
<ID>LambdaParameterInRestartableEffect:EmojiPickerDialog.kt:onCategoryChanged: (Int) -> Unit</ID>
<ID>LambdaParameterInRestartableEffect:PlatformUtils.kt:check: () -> Boolean</ID>
<ID>LambdaParameterInRestartableEffect:TracerouteAlertHandler.kt:onNavigateToMap: (destinationNodeNum: Int, requestId: Int, logUuid: String?) -> Unit</ID>
@@ -95,11 +93,40 @@
<ID>PreviewPublic:AlertPreviews.kt:@Preview(showBackground = true, name = "Icon and Text Alert") @Composable fun PreviewIconAlert</ID>
<ID>PreviewPublic:AlertPreviews.kt:@Preview(showBackground = true, name = "Multiple Choice Alert") @Composable fun PreviewMultipleChoiceAlert</ID>
<ID>PreviewPublic:AlertPreviews.kt:@Preview(showBackground = true, name = "Simple Text Alert") @Composable fun PreviewTextAlert</ID>
<ID>PreviewPublic:BitwisePreference.kt:@Preview(showBackground = true) @Composable fun BitwisePreferencePreview</ID>
<ID>PreviewPublic:ChannelInfo.kt:@PreviewLightDark @Composable fun ChannelInfoPreview</ID>
<ID>PreviewPublic:ChannelItem.kt:@Preview @Composable fun ChannelItemPreview</ID>
<ID>PreviewPublic:DistanceInfo.kt:@PreviewLightDark @Composable fun DistanceInfoPreview</ID>
<ID>PreviewPublic:DropDownPreference.kt:@Preview(showBackground = true) @Composable fun DropDownPreferencePreview</ID>
<ID>PreviewPublic:EditIPv4Preference.kt:@Preview(showBackground = true) @Composable fun EditIPv4PreferencePreview</ID>
<ID>PreviewPublic:EditListPreference.kt:@Preview(showBackground = true) @Composable fun EditListPreferencePreview</ID>
<ID>PreviewPublic:EditPasswordPreference.kt:@Preview(showBackground = true) @Composable fun EditPasswordPreferencePreview</ID>
<ID>PreviewPublic:EditTextPreference.kt:@Preview(showBackground = true) @Composable fun EditTextPreferencePreview</ID>
<ID>PreviewPublic:ElevationInfo.kt:@Composable @Preview fun ElevationInfoPreview</ID>
<ID>PreviewPublic:HopsInfo.kt:@PreviewLightDark @Composable fun HopsInfoPreview</ID>
<ID>PreviewPublic:IconInfo.kt:@Composable @Preview fun IconInfoPreview</ID>
<ID>PreviewPublic:ImportFab.kt:@Preview(showBackground = true, name = "Channel Context with Sharing") @Composable fun PreviewImportFABChannel</ID>
<ID>PreviewPublic:ImportFab.kt:@Preview(showBackground = true, name = "Contact Context") @Composable fun PreviewImportFABContact</ID>
<ID>PreviewPublic:IndoorAirQuality.kt:@Preview(showBackground = true) @Composable fun IAQScalePreview</ID>
<ID>PreviewPublic:LastHeardInfo.kt:@PreviewLightDark @Composable fun LastHeardInfoPreview</ID>
<ID>PreviewPublic:LazyColumnDragAndDropDemo.kt:@Preview @Composable fun LazyColumnDragAndDropDemo</ID>
<ID>PreviewPublic:ListItem.kt:@Preview(showBackground = true) @Composable fun ListItemDisabledPreview</ID>
<ID>PreviewPublic:ListItem.kt:@Preview(showBackground = true) @Composable fun ListItemPreview</ID>
<ID>PreviewPublic:ListItem.kt:@Preview(showBackground = true) @Composable fun SwitchListItemPreview</ID>
<ID>PreviewPublic:MaterialBatteryInfo.kt:@PreviewLightDark @Composable fun MaterialBatteryInfoPreview</ID>
<ID>PreviewPublic:NodeChip.kt:@Suppress("MagicNumber") @Preview @Composable fun NodeChipPreview</ID>
<ID>PreviewPublic:PositionPrecisionPreference.kt:@Preview(showBackground = true) @Composable fun PositionPrecisionPreferencePreview</ID>
<ID>PreviewPublic:PreferenceCategory.kt:@Preview(showBackground = true) @Composable fun PreferenceCategoryPreview</ID>
<ID>PreviewPublic:RegularPreference.kt:@Preview(showBackground = true) @Composable fun RegularPreferencePreview</ID>
<ID>PreviewPublic:SatelliteCountInfo.kt:@PreviewLightDark @Composable fun SatelliteCountInfoPreview</ID>
<ID>PreviewPublic:SecurityIcon.kt:@Preview(name = "All Security Icons with Dialog") @Composable fun PreviewAllSecurityIconsWithDialog</ID>
<ID>PreviewPublic:SignalInfo.kt:@Composable @Preview(showBackground = true) fun SignalInfoSimplePreview</ID>
<ID>PreviewPublic:SignalInfo.kt:@PreviewLightDark @Composable fun SignalInfoPreview</ID>
<ID>PreviewPublic:SliderPreference.kt:@Suppress("MagicNumber") @Preview(showBackground = true) @Composable fun SliderPreferenceDisabledPreview</ID>
<ID>PreviewPublic:SliderPreference.kt:@Suppress("MagicNumber") @Preview(showBackground = true) @Composable fun SliderPreferencePreview</ID>
<ID>PreviewPublic:SwitchPreference.kt:@Preview(showBackground = true) @Composable fun SwitchPreferencePreview</ID>
<ID>PreviewPublic:TextDividerPreference.kt:@Preview(showBackground = true) @Composable fun TextDividerPreferencePreview</ID>
<ID>PreviewPublic:TitledCard.kt:@PreviewLightDark @Composable fun TitledCardPreview</ID>
<ID>ViewModelForwarding:MeshtasticAppShell.kt:MeshtasticCommonAppSetup( uiViewModel = uiViewModel, onNavigateToTracerouteMap = { destNum, requestId, logUuid -> multiBackstack.handleDeepLink( listOf( NodesRoute.NodesGraph, NodeDetailRoute.TracerouteMap(destNum = destNum, requestId = requestId, logUuid = logUuid), ), ) }, )</ID>
<ID>ViewModelForwarding:MeshtasticCommonAppSetup.kt:FirmwareVersionCheck(viewModel = uiViewModel)</ID>
<ID>ViewModelForwarding:MeshtasticCommonAppSetup.kt:SharedDialogs(uiViewModel = uiViewModel)</ID>

View File

@@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.clear
import org.meshtastic.core.resources.close
import org.meshtastic.core.ui.theme.AppTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -102,13 +103,15 @@ fun BitwisePreference(
@Preview(showBackground = true)
@Composable
private fun BitwisePreferencePreview() {
BitwisePreference(
title = "Settings",
value = 3,
summary = "This is a summary",
enabled = true,
items = listOf(1 to "TEST1", 2 to "TEST2"),
onItemSelected = {},
)
fun BitwisePreferencePreview() {
AppTheme {
BitwisePreference(
title = "Settings",
value = 3,
summary = "This is a summary",
enabled = true,
items = listOf(1 to "TEST1", 2 to "TEST2"),
onItemSelected = {},
)
}
}

View File

@@ -69,6 +69,6 @@ fun ChannelInfo(
@PreviewLightDark
@Composable
private fun ChannelInfoPreview() {
fun ChannelInfoPreview() {
AppTheme { ChannelInfo(channel = 2) }
}

View File

@@ -64,6 +64,6 @@ fun ChannelItem(
@Preview
@Composable
private fun ChannelItemPreview() {
fun ChannelItemPreview() {
AppTheme { ChannelItem(index = 0, title = "Medium Fast", enabled = true) {} }
}

View File

@@ -46,6 +46,6 @@ fun DistanceInfo(
@PreviewLightDark
@Composable
private fun DistanceInfoPreview() {
fun DistanceInfoPreview() {
AppTheme { DistanceInfo(distance = "423 mi.") }
}

View File

@@ -43,6 +43,7 @@ import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.theme.AppTheme
import kotlin.jvm.JvmName
@Composable
@@ -207,13 +208,15 @@ internal expect fun Enum<*>.isDeprecatedEnumEntry(): Boolean
@Preview(showBackground = true)
@Composable
private fun DropDownPreferencePreview() {
DropDownPreference(
title = "Settings",
summary = "Lorem ipsum dolor sit amet",
enabled = true,
items = listOf(DropDownItem("TEST1", "text1"), DropDownItem("TEST2", "text2")),
selectedItem = "TEST2",
onItemSelected = {},
)
fun DropDownPreferencePreview() {
AppTheme {
DropDownPreference(
title = "Settings",
summary = "Lorem ipsum dolor sit amet",
enabled = true,
items = listOf(DropDownItem("TEST1", "text1"), DropDownItem("TEST2", "text2")),
selectedItem = "TEST2",
onItemSelected = {},
)
}
}

View File

@@ -50,6 +50,7 @@ import org.meshtastic.core.resources.reset
import org.meshtastic.core.ui.icon.Close
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.core.ui.theme.AppTheme
@Suppress("LongMethod", "CyclomaticComplexMethod", "MagicNumber")
@Composable
@@ -142,14 +143,16 @@ fun EditBase64Preference(
@Preview(showBackground = true)
@Composable
private fun EditBase64PreferencePreview() {
EditBase64Preference(
title = "Title",
summary = "This is a summary",
value = Channel.getRandomKey(),
enabled = true,
keyboardActions = KeyboardActions {},
onValueChange = { _ -> },
onGenerateKey = {},
modifier = Modifier.padding(16.dp),
)
AppTheme {
EditBase64Preference(
title = "Title",
summary = "This is a summary",
value = Channel.getRandomKey(),
enabled = true,
keyboardActions = KeyboardActions {},
onValueChange = { _ -> },
onGenerateKey = {},
modifier = Modifier.padding(16.dp),
)
}
}

View File

@@ -27,6 +27,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun EditIPv4Preference(
@@ -68,12 +69,14 @@ fun EditIPv4Preference(
@Preview(showBackground = true)
@Composable
private fun EditIPv4PreferencePreview() {
EditIPv4Preference(
title = "IP Address",
value = 16820416,
enabled = true,
keyboardActions = KeyboardActions {},
onValueChanged = {},
)
fun EditIPv4PreferencePreview() {
AppTheme {
EditIPv4Preference(
title = "IP Address",
value = 16820416,
enabled = true,
keyboardActions = KeyboardActions {},
onValueChanged = {},
)
}
}

View File

@@ -46,6 +46,7 @@ import org.meshtastic.core.resources.name
import org.meshtastic.core.resources.type
import org.meshtastic.core.ui.icon.Close
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.RemoteHardwarePin
import org.meshtastic.proto.RemoteHardwarePinType
@@ -187,27 +188,29 @@ inline fun <reified T> EditListPreference(
@Preview(showBackground = true)
@Composable
private fun EditListPreferencePreview() {
Column {
EditListPreference(
title = stringResource(Res.string.ignore_incoming),
summary = "This is a summary",
list = listOf(12345, 67890),
maxCount = 4,
enabled = true,
keyboardActions = KeyboardActions {},
onValuesChanged = {},
)
EditListPreference(
title = "Available pins",
list =
listOf(
RemoteHardwarePin(gpio_pin = 12, name = "Front door", type = RemoteHardwarePinType.DIGITAL_READ),
),
maxCount = 4,
enabled = true,
keyboardActions = KeyboardActions {},
onValuesChanged = {},
)
fun EditListPreferencePreview() {
AppTheme {
Column {
EditListPreference(
title = stringResource(Res.string.ignore_incoming),
summary = "This is a summary",
list = listOf(12345, 67890),
maxCount = 4,
enabled = true,
keyboardActions = KeyboardActions {},
onValuesChanged = {},
)
EditListPreference(
title = "Available pins",
list =
listOf(
RemoteHardwarePin(gpio_pin = 12, name = "Front door", type = RemoteHardwarePinType.DIGITAL_READ),
),
maxCount = 4,
enabled = true,
keyboardActions = KeyboardActions {},
onValuesChanged = {},
)
}
}
}

View File

@@ -38,6 +38,7 @@ import org.meshtastic.core.resources.show_password
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Visibility
import org.meshtastic.core.ui.icon.VisibilityOff
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun EditPasswordPreference(
@@ -82,13 +83,15 @@ fun EditPasswordPreference(
@Preview(showBackground = true)
@Composable
private fun EditPasswordPreferencePreview() {
EditPasswordPreference(
title = "Password",
value = "top secret",
maxSize = 63,
enabled = true,
keyboardActions = KeyboardActions {},
onValueChanged = {},
)
fun EditPasswordPreferencePreview() {
AppTheme {
EditPasswordPreference(
title = "Password",
value = "top secret",
maxSize = 63,
enabled = true,
keyboardActions = KeyboardActions {},
onValueChanged = {},
)
}
}

View File

@@ -45,6 +45,7 @@ import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.error
import org.meshtastic.core.ui.icon.Info
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun SignedIntegerEditTextPreference(
@@ -269,25 +270,27 @@ fun EditTextPreference(
@Preview(showBackground = true)
@Composable
private fun EditTextPreferencePreview() {
Column {
EditTextPreference(
title = "String",
value = "Meshtastic",
summary = "This is a summary",
maxSize = 39,
enabled = true,
isError = false,
keyboardOptions = KeyboardOptions.Default,
keyboardActions = KeyboardActions {},
onValueChanged = {},
)
EditTextPreference(
title = "Advanced Settings",
value = UInt.MAX_VALUE.toInt(),
enabled = true,
keyboardActions = KeyboardActions {},
onValueChanged = {},
)
fun EditTextPreferencePreview() {
AppTheme {
Column {
EditTextPreference(
title = "String",
value = "Meshtastic",
summary = "This is a summary",
maxSize = 39,
enabled = true,
isError = false,
keyboardOptions = KeyboardOptions.Default,
keyboardActions = KeyboardActions {},
onValueChanged = {},
)
EditTextPreference(
title = "Advanced Settings",
value = UInt.MAX_VALUE.toInt(),
enabled = true,
keyboardActions = KeyboardActions {},
onValueChanged = {},
)
}
}
}

View File

@@ -29,6 +29,7 @@ import org.meshtastic.core.resources.altitude
import org.meshtastic.core.resources.elevation_suffix
import org.meshtastic.core.ui.icon.Elevation
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits
@Composable
@@ -51,6 +52,6 @@ fun ElevationInfo(
@Composable
@Preview
private fun ElevationInfoPreview() {
MaterialTheme { ElevationInfo(altitude = 100, system = DisplayUnits.METRIC, suffix = "ASL") }
fun ElevationInfoPreview() {
AppTheme { ElevationInfo(altitude = 100, system = DisplayUnits.METRIC, suffix = "ASL") }
}

View File

@@ -42,6 +42,6 @@ fun HopsInfo(hops: Int, modifier: Modifier = Modifier, contentColor: Color = Mat
@PreviewLightDark
@Composable
private fun HopsInfoPreview() {
fun HopsInfoPreview() {
AppTheme { HopsInfo(hops = 3) }
}

View File

@@ -35,6 +35,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.meshtastic.core.ui.icon.Elevation
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.theme.AppTheme
private const val SIZE_ICON = 14
@@ -89,8 +90,8 @@ fun IconInfo(
@Composable
@Preview
private fun IconInfoPreview() {
MaterialTheme {
fun IconInfoPreview() {
AppTheme {
IconInfo(icon = MeshtasticIcons.Elevation, contentDescription = "Elevation", label = "Elevation", text = "100m")
}
}

View File

@@ -242,7 +242,7 @@ private fun InputUrlDialog(title: String, onDismiss: () -> Unit, onConfirm: (Str
@Preview(showBackground = true, name = "Contact Context")
@Composable
private fun PreviewImportFABContact() {
fun PreviewImportFABContact() {
AppTheme {
Box(modifier = Modifier.fillMaxSize().padding(16.dp)) {
MeshtasticImportFAB(onImport = {}, modifier = Modifier.align(Alignment.BottomEnd), isContactContext = true)
@@ -252,7 +252,7 @@ private fun PreviewImportFABContact() {
@Preview(showBackground = true, name = "Channel Context with Sharing")
@Composable
private fun PreviewImportFABChannel() {
fun PreviewImportFABChannel() {
AppTheme {
Box(modifier = Modifier.fillMaxSize().padding(16.dp)) {
MeshtasticImportFAB(

View File

@@ -63,6 +63,7 @@ import org.meshtastic.core.resources.show_iaq_legend
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.ThumbUp
import org.meshtastic.core.ui.icon.Warning
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.IAQColors.IAQDangerouslyPolluted
import org.meshtastic.core.ui.theme.IAQColors.IAQExcellent
import org.meshtastic.core.ui.theme.IAQColors.IAQExtremelyPolluted
@@ -254,79 +255,81 @@ fun IAQScale(modifier: Modifier = Modifier) {
@Preview(showBackground = true)
@Composable
fun IAQScalePreview() {
IAQScale()
AppTheme { IAQScale() }
}
@Suppress("LongMethod")
@Preview(showBackground = true)
@Composable
private fun IndoorAirQualityPreview() {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(stringResource(Res.string.preview_pill), style = MaterialTheme.typography.titleLarge)
Row {
IndoorAirQuality(iaq = 6)
IndoorAirQuality(iaq = 51)
}
Row {
IndoorAirQuality(iaq = 101)
IndoorAirQuality(iaq = 201)
}
Row {
IndoorAirQuality(iaq = 350)
IndoorAirQuality(iaq = 351)
}
AppTheme {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(stringResource(Res.string.preview_pill), style = MaterialTheme.typography.titleLarge)
Row {
IndoorAirQuality(iaq = 6)
IndoorAirQuality(iaq = 51)
}
Row {
IndoorAirQuality(iaq = 101)
IndoorAirQuality(iaq = 201)
}
Row {
IndoorAirQuality(iaq = 350)
IndoorAirQuality(iaq = 351)
}
Text(stringResource(Res.string.preview_dot), style = MaterialTheme.typography.titleLarge)
Row {
IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Dot)
IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Dot)
IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Dot)
IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Dot)
IndoorAirQuality(iaq = 350, displayMode = IaqDisplayMode.Dot)
IndoorAirQuality(iaq = 351, displayMode = IaqDisplayMode.Dot)
}
Text(stringResource(Res.string.preview_dot), style = MaterialTheme.typography.titleLarge)
Row {
IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Dot)
IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Dot)
IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Dot)
IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Dot)
IndoorAirQuality(iaq = 350, displayMode = IaqDisplayMode.Dot)
IndoorAirQuality(iaq = 351, displayMode = IaqDisplayMode.Dot)
}
Text(stringResource(Res.string.preview_text), style = MaterialTheme.typography.titleLarge)
Row {
IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Text)
IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Text)
IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Text)
}
Row {
IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Text)
IndoorAirQuality(iaq = 350, displayMode = IaqDisplayMode.Text)
IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Text)
}
Text(stringResource(Res.string.preview_text), style = MaterialTheme.typography.titleLarge)
Row {
IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Text)
IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Text)
IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Text)
}
Row {
IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Text)
IndoorAirQuality(iaq = 350, displayMode = IaqDisplayMode.Text)
IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Text)
}
Text(stringResource(Res.string.preview_gauge), style = MaterialTheme.typography.titleLarge)
Row {
IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 151, displayMode = IaqDisplayMode.Gauge)
}
Row {
IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 251, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 301, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 351, displayMode = IaqDisplayMode.Gauge)
}
Row {
IndoorAirQuality(iaq = 401, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Gauge)
}
Text(stringResource(Res.string.preview_gauge), style = MaterialTheme.typography.titleLarge)
Row {
IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 151, displayMode = IaqDisplayMode.Gauge)
}
Row {
IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 251, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 301, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 351, displayMode = IaqDisplayMode.Gauge)
}
Row {
IndoorAirQuality(iaq = 401, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Gauge)
}
Text(stringResource(Res.string.preview_gradient), style = MaterialTheme.typography.titleLarge)
IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 351, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 401, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Gradient)
Text(stringResource(Res.string.preview_gradient), style = MaterialTheme.typography.titleLarge)
IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 351, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 401, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Gradient)
}
}
}

View File

@@ -49,6 +49,6 @@ fun LastHeardInfo(
@PreviewLightDark
@Composable
private fun LastHeardInfoPreview() {
fun LastHeardInfoPreview() {
AppTheme { LastHeardInfo(lastHeard = nowSeconds.toInt() - 8600) }
}

View File

@@ -63,37 +63,40 @@ import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.preview_footer
import org.meshtastic.core.resources.preview_header
import org.meshtastic.core.resources.preview_item
import org.meshtastic.core.ui.theme.AppTheme
// Derived in part from:
// https://github.com/androidx/androidx/blob/c92ad2941368202b2d78b8d14c71bf81e9525944/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
@Preview
@Composable
fun LazyColumnDragAndDropDemo() {
var list by remember { mutableStateOf(List(50) { it }) }
AppTheme {
var list by remember { mutableStateOf(List(50) { it }) }
val listState = rememberLazyListState()
val dragDropState =
rememberDragDropState(listState, headerCount = 1) { fromIndex, toIndex ->
if (fromIndex in list.indices && toIndex in list.indices) {
list = list.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
val listState = rememberLazyListState()
val dragDropState =
rememberDragDropState(listState, headerCount = 1) { fromIndex, toIndex ->
if (fromIndex in list.indices && toIndex in list.indices) {
list = list.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
}
}
}
LazyColumn(
modifier = Modifier.dragContainer(dragDropState = dragDropState, haptics = LocalHapticFeedback.current),
state = listState,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
item { Text(stringResource(Res.string.preview_header), Modifier.fillMaxWidth().padding(20.dp)) }
LazyColumn(
modifier = Modifier.dragContainer(dragDropState = dragDropState, haptics = LocalHapticFeedback.current),
state = listState,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
item { Text(stringResource(Res.string.preview_header), Modifier.fillMaxWidth().padding(20.dp)) }
itemsIndexed(list, key = { _, item -> item }) { index, item ->
DraggableItem(dragDropState, index + 1) {
Card { Text(stringResource(Res.string.preview_item, item), Modifier.fillMaxWidth().padding(20.dp)) }
itemsIndexed(list, key = { _, item -> item }) { index, item ->
DraggableItem(dragDropState, index + 1) {
Card { Text(stringResource(Res.string.preview_item, item), Modifier.fillMaxWidth().padding(20.dp)) }
}
}
}
item { Text(stringResource(Res.string.preview_footer), Modifier.fillMaxWidth().padding(20.dp)) }
item { Text(stringResource(Res.string.preview_footer), Modifier.fillMaxWidth().padding(20.dp)) }
}
}
}

View File

@@ -152,19 +152,19 @@ fun ImageVector?.icon(tint: Color = LocalContentColor.current): @Composable (()
@Preview(showBackground = true)
@Composable
private fun ListItemPreview() {
fun ListItemPreview() {
AppTheme { ListItem(text = "Text", leadingIcon = MeshtasticIcons.Android, enabled = true) {} }
}
@Preview(showBackground = true)
@Composable
private fun ListItemDisabledPreview() {
fun ListItemDisabledPreview() {
AppTheme { ListItem(text = "Text", leadingIcon = MeshtasticIcons.Android, enabled = false) {} }
}
@Preview(showBackground = true)
@Composable
private fun SwitchListItemPreview() {
fun SwitchListItemPreview() {
AppTheme { SwitchListItem(text = "Text", leadingIcon = MeshtasticIcons.Android, checked = true, onClick = {}) }
}

View File

@@ -37,6 +37,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.model.Node
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.EnvironmentMetrics
import org.meshtastic.proto.Paxcount
import org.meshtastic.proto.User
@@ -78,8 +79,8 @@ fun NodeChip(modifier: Modifier = Modifier, node: Node, onClick: ((Node) -> Unit
@Suppress("MagicNumber")
@Preview
@Composable
private fun NodeChipPreview() {
val user = User(short_name = "\uD83E\uDEE0", long_name = "John Doe")
fun NodeChipPreview() {
val user = User(short_name = "JD", long_name = "John Doe")
val node =
Node(
num = 13444,
@@ -88,5 +89,5 @@ private fun NodeChipPreview() {
paxcounter = Paxcount(ble = 10, wifi = 5),
environmentMetrics = EnvironmentMetrics(temperature = 25f, relative_humidity = 60f),
)
NodeChip(node = node)
AppTheme { NodeChip(node = node) }
}

View File

@@ -35,6 +35,7 @@ import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.position_enabled
import org.meshtastic.core.resources.precise_location
import org.meshtastic.core.ui.theme.AppTheme
import kotlin.math.pow
import kotlin.math.roundToInt
@@ -105,11 +106,13 @@ fun PositionPrecisionPreference(
@Preview(showBackground = true)
@Composable
private fun PositionPrecisionPreferencePreview() {
PositionPrecisionPreference(
value = POSITION_PRECISION_DEFAULT,
enabled = true,
onValueChanged = {},
modifier = Modifier.padding(horizontal = 16.dp),
)
fun PositionPrecisionPreferencePreview() {
AppTheme {
PositionPrecisionPreference(
value = POSITION_PRECISION_DEFAULT,
enabled = true,
onValueChanged = {},
modifier = Modifier.padding(horizontal = 16.dp),
)
}
}

View File

@@ -29,6 +29,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun PreferenceCategory(
@@ -55,6 +56,6 @@ fun PreferenceCategory(
@Preview(showBackground = true)
@Composable
private fun PreferenceCategoryPreview() {
PreferenceCategory(text = "Advanced settings")
fun PreferenceCategoryPreview() {
AppTheme { PreferenceCategory(text = "Advanced settings") }
}

View File

@@ -38,6 +38,7 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun RegularPreference(
@@ -123,6 +124,6 @@ fun RegularPreference(
@Preview(showBackground = true)
@Composable
private fun RegularPreferencePreview() {
RegularPreference(title = "Advanced settings", subtitle = "Text2", onClick = {})
fun RegularPreferencePreview() {
AppTheme { RegularPreference(title = "Advanced settings", subtitle = "Text2", onClick = {}) }
}

View File

@@ -46,6 +46,6 @@ fun SatelliteCountInfo(
@PreviewLightDark
@Composable
private fun SatelliteCountInfoPreview() {
fun SatelliteCountInfoPreview() {
AppTheme { SatelliteCountInfo(satCount = 5) }
}

View File

@@ -78,6 +78,7 @@ import org.meshtastic.core.resources.security_icon_insecure_no_precise
import org.meshtastic.core.resources.security_icon_insecure_precise_only
import org.meshtastic.core.resources.security_icon_secure
import org.meshtastic.core.resources.security_icon_warning_precise_mqtt
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
@@ -501,56 +502,61 @@ private fun AllSecurityStates() {
@Preview(name = "Secure Channel Icon")
@Composable
private fun PreviewSecureChannel() {
SecurityIcon(securityState = SecurityState.SECURE)
AppTheme { SecurityIcon(securityState = SecurityState.SECURE) }
}
@Preview(name = "Insecure Precise Icon")
@Composable
private fun PreviewInsecureChannelWithPreciseLocation() {
SecurityIcon(securityState = SecurityState.INSECURE_PRECISE_ONLY)
AppTheme { SecurityIcon(securityState = SecurityState.INSECURE_PRECISE_ONLY) }
}
@Preview(name = "Insecure Channel Icon")
@Composable
private fun PreviewInsecureChannelWithoutPreciseLocation() {
SecurityIcon(securityState = SecurityState.INSECURE_NO_PRECISE)
AppTheme { SecurityIcon(securityState = SecurityState.INSECURE_NO_PRECISE) }
}
@Preview(name = "MQTT Enabled Icon")
@Composable
private fun PreviewMqttEnabled() {
SecurityIcon(securityState = SecurityState.INSECURE_PRECISE_MQTT_WARNING)
AppTheme { SecurityIcon(securityState = SecurityState.INSECURE_PRECISE_MQTT_WARNING) }
}
@Preview(name = "All Security Icons with Dialog")
@Composable
private fun PreviewAllSecurityIconsWithDialog() {
var showHelpDialogFor by remember { mutableStateOf<SecurityState?>(null) }
val stateLabels = remember {
// Using SecurityState.entries to build the map keys
mapOf(
SecurityState.SECURE to "Secure",
SecurityState.INSECURE_NO_PRECISE to "Insecure (No Precise Location)",
SecurityState.INSECURE_PRECISE_ONLY to "Insecure (Precise Location Only)",
SecurityState.INSECURE_PRECISE_MQTT_WARNING to "Insecure (Precise Location + MQTT Warning)",
)
}
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(text = "Security Icons Preview (Click for Help)", style = MaterialTheme.typography.headlineSmall)
SecurityState.entries.forEach { state ->
// Iterate over enum entries
val label = stateLabels[state] ?: "Unknown State (${state.name})" // Fallback to enum name
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
SecurityIcon(securityState = state, externalOnClick = { showHelpDialogFor = state })
Text(label)
}
fun PreviewAllSecurityIconsWithDialog() {
AppTheme {
var showHelpDialogFor by remember { mutableStateOf<SecurityState?>(null) }
val stateLabels = remember {
// Using SecurityState.entries to build the map keys
mapOf(
SecurityState.SECURE to "Secure",
SecurityState.INSECURE_NO_PRECISE to "Insecure (No Precise Location)",
SecurityState.INSECURE_PRECISE_ONLY to "Insecure (Precise Location Only)",
SecurityState.INSECURE_PRECISE_MQTT_WARNING to "Insecure (Precise Location + MQTT Warning)",
)
}
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(text = "Security Icons Preview (Click for Help)", style = MaterialTheme.typography.headlineSmall)
SecurityState.entries.forEach { state ->
// Iterate over enum entries
val label = stateLabels[state] ?: "Unknown State (${state.name})" // Fallback to enum name
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
SecurityIcon(securityState = state, externalOnClick = { showHelpDialogFor = state })
Text(label)
}
}
showHelpDialogFor?.let { SecurityHelpDialog(securityState = it, onDismiss = { showHelpDialogFor = null }) }
}
showHelpDialogFor?.let { SecurityHelpDialog(securityState = it, onDismiss = { showHelpDialogFor = null }) }
}
}

View File

@@ -79,7 +79,7 @@ fun <T> SliderPreference(
@Suppress("MagicNumber")
@Preview(showBackground = true)
@Composable
private fun SliderPreferencePreview() {
fun SliderPreferencePreview() {
val items = listOf(1L to "One", 2L to "Two", 3L to "Three", 4L to "Four", 5L to "Five")
AppTheme {
SliderPreference(
@@ -96,7 +96,7 @@ private fun SliderPreferencePreview() {
@Suppress("MagicNumber")
@Preview(showBackground = true)
@Composable
private fun SliderPreferenceDisabledPreview() {
fun SliderPreferenceDisabledPreview() {
val items = listOf(1L to "One", 2L to "Two", 3L to "Three", 4L to "Four", 5L to "Five")
AppTheme {
SliderPreference(

View File

@@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun SwitchPreference(
@@ -86,6 +87,6 @@ fun SwitchPreference(
@Preview(showBackground = true)
@Composable
private fun SwitchPreferencePreview() {
SwitchPreference(title = "Setting", checked = true, enabled = true, onCheckedChange = {})
fun SwitchPreferencePreview() {
AppTheme { SwitchPreference(title = "Setting", checked = true, enabled = true, onCheckedChange = {}) }
}

View File

@@ -32,6 +32,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun TextDividerPreference(
@@ -76,6 +77,6 @@ fun TextDividerPreference(
@Preview(showBackground = true)
@Composable
private fun TextDividerPreferencePreview() {
TextDividerPreference(title = "Advanced settings")
fun TextDividerPreferencePreview() {
AppTheme { TextDividerPreference(title = "Advanced settings") }
}

View File

@@ -50,6 +50,6 @@ fun TitledCard(title: String?, modifier: Modifier = Modifier, content: @Composab
@PreviewLightDark
@Composable
private fun TitledCardPreview() {
fun TitledCardPreview() {
AppTheme { Surface { TitledCard(title = "Title") { Box(modifier = Modifier.fillMaxWidth().height(100.dp)) {} } } }
}

View File

@@ -18,7 +18,6 @@ package org.meshtastic.core.ui.component.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import okio.ByteString.Companion.toByteString
import org.meshtastic.core.model.DeviceMetrics.Companion.currentTime
import org.meshtastic.core.model.Node
import org.meshtastic.proto.Config
import org.meshtastic.proto.DeviceMetrics
@@ -27,7 +26,6 @@ import org.meshtastic.proto.HardwareModel
import org.meshtastic.proto.Paxcount
import org.meshtastic.proto.Position
import org.meshtastic.proto.User
import kotlin.random.Random
class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
val mickeyMouse =
@@ -42,7 +40,7 @@ class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
role = Config.DeviceConfig.Role.ROUTER,
),
position = Position(latitude_i = 338125110, longitude_i = -1179189760, altitude = 138, sats_in_view = 4),
lastHeard = currentTime(),
lastHeard = 1700000000,
channel = 0,
snr = 12.5F,
rssi = -42,
@@ -60,7 +58,7 @@ class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
val minnieMouse =
mickeyMouse.copy(
num = Random.nextInt(),
num = 1928,
user =
User(
long_name = "Minnie Mouse",
@@ -76,9 +74,9 @@ class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
private val donaldDuck =
Node(
num = Random.nextInt(),
num = 1934,
position = Position(latitude_i = 338052347, longitude_i = -1179208460, altitude = 121, sats_in_view = 66),
lastHeard = currentTime() - 300,
lastHeard = 1699999700,
channel = 0,
snr = 12.5F,
rssi = -42,
@@ -123,7 +121,7 @@ class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
paxcounter = Paxcount(),
)
private val almostNothing = Node(num = Random.nextInt())
private val almostNothing = Node(num = 9999)
override val values: Sequence<Node>
get() =

View File

@@ -62,6 +62,7 @@ import org.meshtastic.core.resources.new_channel_rcvd
import org.meshtastic.core.resources.replace
import org.meshtastic.core.resources.replace_channels_and_settings_description
import org.meshtastic.core.ui.component.ChannelSelection
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.ChannelSet
@Composable
@@ -314,10 +315,14 @@ fun ScannedQrCodeDialog(
@PreviewLightDark
@Composable
private fun ScannedQrCodeDialogPreview() {
ScannedQrCodeDialog(
channels = ChannelSet(settings = listOf(Channel.default.settings), lora_config = Channel.default.loraConfig),
incoming = ChannelSet(settings = listOf(Channel.default.settings), lora_config = Channel.default.loraConfig),
onDismiss = {},
onConfirm = {},
)
AppTheme {
ScannedQrCodeDialog(
channels =
ChannelSet(settings = listOf(Channel.default.settings), lora_config = Channel.default.loraConfig),
incoming =
ChannelSet(settings = listOf(Channel.default.settings), lora_config = Channel.default.loraConfig),
onDismiss = {},
onConfirm = {},
)
}
}

View File

@@ -1,282 +0,0 @@
# Build-Logic Convention Patterns & Guidelines
Quick reference for maintaining and extending the build-logic convention system.
## Core Principles
1. **DRY (Don't Repeat Yourself)**: Extract common configuration into functions
2. **Clarity Over Cleverness**: Explicit intent in `build.gradle.kts` files matters
3. **Single Responsibility**: Each convention plugin has one clear purpose
4. **Test-Driven**: Configuration changes must pass `spotlessCheck`, `detekt`, and tests
## Convention Plugin Architecture
```
build-logic/
├── convention/
│ ├── src/main/kotlin/
│ │ ├── KmpFeatureConventionPlugin.kt # KMP feature modules (composes library + compose + koin + common deps)
│ │ ├── KmpLibraryConventionPlugin.kt # KMP modules: core libraries
│ │ ├── KmpLibraryComposeConventionPlugin.kt # KMP Compose Multiplatform setup
│ │ ├── KmpJvmAndroidConventionPlugin.kt # Opt-in jvmAndroidMain hierarchy for Android + desktop JVM
│ │ ├── AndroidApplicationConventionPlugin.kt # Main app
│ │ ├── AndroidLibraryConventionPlugin.kt # Android-only libraries
│ │ ├── AndroidApplicationComposeConventionPlugin.kt
│ │ ├── AndroidLibraryComposeConventionPlugin.kt
│ │ ├── org/meshtastic/buildlogic/
│ │ │ ├── KotlinAndroid.kt # Base Kotlin/Android config
│ │ │ ├── AndroidCompose.kt # Compose setup
│ │ │ ├── FlavorResolution.kt # Flavor configuration
│ │ │ ├── MeshtasticFlavor.kt # Flavor definitions
│ │ │ ├── Detekt.kt # Static analysis
│ │ │ ├── Spotless.kt # Code formatting
│ │ │ └── ... (other config modules)
```
## How to Add a New Convention
### Example: Adding a new test framework dependency
**Current Pattern (GOOD ✅):**
If all KMP modules need a dependency, add it to `KotlinAndroid.kt::configureKmpTestDependencies()`:
```kotlin
internal fun Project.configureKmpTestDependencies() {
extensions.configure<KotlinMultiplatformExtension> {
sourceSets.apply {
val commonTest = findByName("commonTest") ?: return@apply
commonTest.dependencies {
implementation(kotlin("test"))
// NEW: Add here once, applies to all ~15 KMP modules
implementation(libs.library("new-test-framework"))
}
// ... androidHostTest setup
}
}
}
```
**Result:** All 15 feature and core modules automatically get the dependency ✅
### Example: Adding shared `jvmAndroidMain` code to a KMP module
**Current Pattern (GOOD ✅):**
If a KMP module needs Java/JVM APIs shared between Android and desktop JVM, apply the opt-in convention plugin instead of manually creating source sets and `dependsOn(...)` edges:
```kotlin
plugins {
alias(libs.plugins.meshtastic.kmp.library)
id("meshtastic.kmp.jvm.android")
}
kotlin {
jvm()
android { /* ... */ }
sourceSets {
commonMain.dependencies { /* ... */ }
jvmMain.dependencies { /* jvm-only additions */ }
androidMain.dependencies { /* android-only additions */ }
}
}
```
**Why:** The convention uses Kotlin's hierarchy template API to create `jvmAndroidMain` without the `Default Kotlin Hierarchy Template Not Applied Correctly` warning triggered by hand-written `dependsOn(...)` graphs.
### Example: Creating a new KMP feature module
**Current Pattern (GOOD ✅):**
Use `meshtastic.kmp.feature` for any `feature:*` module. It composes `kmp.library` + `kmp.library.compose` + `koin` and provides all the common Compose/Lifecycle/Koin/Android dependencies that every feature needs:
```kotlin
plugins {
alias(libs.plugins.meshtastic.kmp.feature)
// Optional: add only if this feature needs serialization
alias(libs.plugins.meshtastic.kotlinx.serialization)
}
kotlin {
jvm()
android {
namespace = "org.meshtastic.feature.yourfeature"
androidResources.enable = false
withHostTest { isIncludeAndroidResources = true }
}
sourceSets {
commonMain.dependencies {
// Only module-SPECIFIC deps here
implementation(projects.core.common)
implementation(projects.core.model)
implementation(projects.core.ui)
}
androidMain.dependencies {
// Only Android-specific extras here
}
}
}
```
**What the plugin provides automatically:**
- `commonMain`: `compose-multiplatform-material3`, `jetbrains-lifecycle-viewmodel-compose`, `jetbrains-lifecycle-runtime-compose`, `koin-compose-viewmodel`, `kermit`
- `androidMain`: `androidx-compose-bom` (platform), `accompanist-permissions`, `androidx-activity-compose`, `androidx-compose-material3`, `androidx-compose-ui-text`, `androidx-compose-ui-tooling-preview`
- `commonTest`: `core:testing`
**Why:** Eliminates ~15 duplicate dependency declarations per feature module (modelled after Now in Android's `AndroidFeatureImplConventionPlugin`).
### Example: Adding Android-specific test config
**Pattern:** Test options (`animationsDisabled`, `testInstrumentationRunner`, `unitTests.isReturnDefaultValues`) are centralized in `configureKotlinAndroid()` via `CommonExtension`, so they apply to both app and library modules automatically. To add new test config, update `KotlinAndroid.kt::configureKotlinAndroid()`:
```kotlin
internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
commonExtension.apply {
testOptions {
animationsDisabled = true
unitTests.isReturnDefaultValues = true
// NEW: Add shared test options here
}
}
}
```
## Duplication Heuristics
**When to consolidate (DRY):**
- ✅ Configuration appears in 3+ convention plugins
- ✅ The duplication changes together (same reasons to update)
- ✅ Extraction doesn't require complex type gymnastics
- ✅ Underlying Gradle extension is the same (`CommonExtension`)
**When to keep separate (Clarity):**
- ✅ Different Gradle extension types (`ApplicationExtension` vs `LibraryExtension`)
- ✅ Plugin intent is explicit in `build.gradle.kts` usage
- ✅ Duplication is small (<50 lines) and stable
- ✅ Future divergence between app/library handling is plausible
**Examples in codebase:**
| Duplication | Status | Reasoning |
|-------------|--------|-----------|
| `AndroidApplicationComposeConventionPlugin``AndroidLibraryComposeConventionPlugin` | **Kept Separate** | Different extension types; small duplication; explicit intent |
| `AndroidApplicationFlavorsConventionPlugin``AndroidLibraryFlavorsConventionPlugin` | **Kept Separate** | Different extension types; small duplication; explicit intent |
| `configureKmpTestDependencies()` (7 modules) | **Consolidated** | Large duplication; single source of truth; all KMP modules benefit |
| `jvmAndroidMain` hierarchy setup (4 modules) | **Consolidated** | Shared KMP hierarchy pattern; avoids manual `dependsOn(...)` edges and hierarchy warnings |
| `PUBLISHED_MODULES` set (4 usages) | **Consolidated** | Was repeated as `listOf(...)` in 4 places; now a single `setOf(...)` constant in `KotlinAndroid.kt` |
| `SHARED_COMPILER_ARGS` list (2 code paths) | **Consolidated** | Eliminates duplicated `-opt-in` flags between KMP target compilations and `KotlinCompile` task configuration |
## Testing Convention Changes
After modifying a convention plugin, verify:
```bash
# 1. Code quality
./gradlew spotlessCheck detekt
# 2. Compilation
./gradlew assembleDebug assembleRelease
# 3. Tests
./gradlew test # All unit tests
./gradlew :feature:messaging:jvmTest # Feature module tests
./gradlew :feature:node:testAndroidHostTest # Android host tests
```
## Documentation Requirements
When you add/modify a convention:
1. **Add Kotlin docs** to the function:
```kotlin
/**
* Configure test dependencies for KMP modules.
*
* Automatically applies kotlin("test") to:
* - commonTest source set (all targets)
* - androidHostTest source set (Android-only)
*
* Usage: Called automatically by KmpLibraryConventionPlugin
*/
internal fun Project.configureKmpTestDependencies() { ... }
```
2. **Update AGENTS.md** if convention affects developers
3. **Update this guide** if pattern changes
## Performance Tips
- **Configuration-time:** Convention logic runs during Gradle configuration (0.5-2s)
- **Build-time:** No impact (conventions don't execute tasks)
- **Optimization focus:** Minimize `extensions.configure()` blocks (lazy evaluation is preferred)
### Good ✅
```kotlin
extensions.configure<KotlinMultiplatformExtension> {
// Single block for all source set configuration
sourceSets.apply {
commonTest.dependencies { /* ... */ }
androidHostTest?.dependencies { /* ... */ }
}
}
```
### Avoid ❌
```kotlin
// Multiple blocks - slower configuration
extensions.configure<KotlinMultiplatformExtension> {
sourceSets.getByName("commonTest").dependencies { /* ... */ }
}
extensions.configure<KotlinMultiplatformExtension> {
sourceSets.getByName("androidHostTest").dependencies { /* ... */ }
}
```
## Common Pitfalls
### ❌ **Mistake: Adding dependencies in the wrong place**
```kotlin
// WRONG: Adds to ALL modules, not just KMP
extensions.configure<Project> {
dependencies { add("implementation", ...) } // Global!
}
// RIGHT: Scoped to specific source set/module type
commonTest.dependencies { implementation(...) }
```
### ❌ **Mistake: Extension type mismatch**
```kotlin
// WRONG: LibraryExtension isn't a subtype of ApplicationExtension
extensions.configure<ApplicationExtension> {
// Won't apply to library modules
}
// RIGHT: Use CommonExtension or specific types
extensions.configure<CommonExtension> {
// Applies to both
}
```
### ❌ **Mistake: Side effects during configuration**
```kotlin
// WRONG: Eager task configuration at plugin-apply time
tasks.withType<Test> {
// Can realize tasks too early
}
// RIGHT: Lazy, configuration-cache-friendly wiring
tasks.withType<Test>().configureEach {
// Applies to existing and future tasks lazily
}
```
## Related Files
- `AGENTS.md` - Development guidelines (Section 3.B testing, Section 4.A build protocol)
- `build-logic/convention/build.gradle.kts` - Convention plugin build config

View File

@@ -1,27 +0,0 @@
# Decision: BLE KMP Strategy
> Date: 2026-03-16 | Status: **Decided — Fully Migrated to Kable**
## Context
`core:ble` needed to support non-Android targets. Nordic's Kotlin-BLE-Library, while mature on Android and actively tested in the app, was primarily Android/iOS focused and lacked support for Desktop (JVM) targets. Kable natively supports all Kotlin Multiplatform targets (Android, Apple, Desktop/JVM, Web).
Initially, we implemented an **Interface-Driven "Nordic Hybrid" Abstraction** (keeping Nordic on Android behind `commonMain` interfaces) to wait and see if Nordic expanded their KMP support.
However, as Desktop integration advanced, we found the need for a unified BLE transport.
## Decision
**Migrate entirely to Kable:**
- We migrated all BLE transport logic across Android and Desktop to use Kable.
- The `commonMain` interfaces (`BleConnection`, `BleScanner`, `BleDevice`, `BluetoothRepository`, etc.) remain, but their core implementations (`KableBleConnection`, `KableBleScanner`) are now entirely shared in `commonMain`.
- The Android-specific Nordic dependencies (`no.nordicsemi.kotlin.ble:*`) and the Nordic DFU library were completely excised from the project.
- OTA Firmware updates were successfully refactored to use the Kable-based `BleOtaTransport`, shared across Android and Desktop in `commonMain`.
- Nordic Secure DFU was reimplemented as a pure KMP protocol stack (`SecureDfuTransport`, `SecureDfuProtocol`, `SecureDfuHandler`) using Kable, with no dependency on the Nordic DFU library.
## Consequences
- **Maximal Code Deduplication:** The BLE implementation is completely shared across Android and Desktop in `core:ble/commonMain`.
- **Future-Proofing:** Adding an `iosMain` target in the future will be trivial, as it can leverage the same shared Kable abstractions.
- **Lost Nordic Mocks:** Kable lacks the comprehensive mock infrastructure of the Nordic library. Consequently, several complex BLE OTA unit tests had to be deprecated. Re-establishing this test coverage using custom Kable fakes is an ongoing technical debt item.

View File

@@ -1,34 +0,0 @@
# Decision: Hilt → Koin Migration
> Date: 2026-02-20 to 2026-03-09 | Status: **Complete**
## Context
Hilt (Dagger) was the strongest remaining barrier to KMP adoption — it requires Android-specific annotation processing and can't run in `commonMain`.
## Decision
Migrated to **Koin 4.2.0-RC1** with the **K2 Compiler Plugin** (`io.insert-koin.compiler.plugin`) and later upgraded to **0.4.1**.
Key choices:
- `@KoinViewModel` replaces `@HiltViewModel`; `koinViewModel()` replaces `hiltViewModel()`
- `@Module` + `@ComponentScan` in `commonMain` modules (valid 2026 KMP pattern)
- `@KoinWorker` replaces `@HiltWorker` for WorkManager
- `@InjectedParam` replaces `@Assisted` for factory patterns
- Root graph assembly centralized in `AppKoinModule`; shared modules expose annotated definitions
- **Koin 0.4.1 A1 Compile Safety Disabled:** Meshtastic heavily utilizes dependency inversion across KMP modules (e.g., interfaces defined in `core:repository` are implemented in `core:data`). Koin 0.4.x's per-module A1 validation strictly enforces that all dependencies must be explicitly provided or included locally, breaking this clean architecture. We have globally disabled A1 `compileSafety` in `KoinConventionPlugin` to properly rely on Koin's A3 full-graph validation at the composition root (`startKoin`).
## Gotchas Discovered
1. **K2 Compiler Plugin signature collision:** Multiple `@Single` providers with identical JVM signatures in the same `@Module` cause `ClassCastException`. Fix: split into separate `@Module` classes.
2. **Circular dependencies:** `Lazy<T>` injection can still `StackOverflowError` if `Lazy` is accessed too early (e.g., in `init` coroutine). Fix: pass dependencies as function parameters instead.
3. **Robolectric `KoinApplicationAlreadyStartedException`:** Call `stopKoin()` in `onTerminate`.
## Consequences
- Hilt completely removed
- All 23 KMP modules can contain Koin-annotated definitions
- Desktop bootstraps its own `DesktopKoinModule` with stubs + real implementations
- 11 passthrough Android ViewModel wrappers eliminated

View File

@@ -1,38 +0,0 @@
<!--
- Copyright (c) 2026 Meshtastic LLC
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
# Decision: Testing Consolidation — `core:testing` Module
**Date:** 2026-03-11
**Status:** Implemented
## Context
Each KMP module independently declared scattered test dependencies (`junit`, `mockk`, `coroutines-test`, `turbine`), leading to version drift and duplicated test doubles across modules.
## Decision
Created `core:testing` as a lightweight shared module for test doubles, fakes, and utilities. It depends only on `core:model` and `core:repository` (no heavy deps like `core:database`). All modules declare `implementation(projects.core.testing)` in `commonTest` to get a unified test dependency set.
## Consequences
- **Single source** for test fakes (`FakeRadioController`, `FakeNodeRepository`, `TestDataFactory`)
- **Clean dependency graph** — `core:testing` is lightweight; heavy modules depend on it in test scope, not vice versa
- **No production leakage** — only declared in `commonTest`, never in release artifacts
- **Reduced maintenance** — updating test libraries touches one `build.gradle.kts`
See [`core/testing/README.md`](../../core/testing/README.md) for usage guide and API reference.

View File

@@ -1,178 +0,0 @@
# KMP Migration Status
> Last updated: 2026-04-15
Single source of truth for Kotlin Multiplatform migration progress. For the forward-looking roadmap, see [`roadmap.md`](./roadmap.md). For completed decision records, see [`decisions/`](./decisions/).
## Summary
Meshtastic-Android has completed its **Android-first structural KMP migration** across core logic and feature modules, with **full JVM cross-compilation validated in CI**. The desktop target has a working Navigation 3 shell, TCP transport with full mesh handshake, and multiple features wired with real screens.
Modules that share JVM-specific code between Android and desktop now standardize on the `meshtastic.kmp.jvm.android` convention plugin, which creates `jvmAndroidMain` via Kotlin's hierarchy template API instead of manual `dependsOn(...)` source-set wiring.
## Module Inventory
### Core Modules (21 total)
| Module | KMP? | JVM target? | Notes |
|---|:---:|:---:|---|
| `core:proto` | ✅ | ✅ | Protobuf definitions |
| `core:common` | ✅ | ✅ | Utilities, `jvmAndroidMain` source set |
| `core:model` | ✅ | ✅ | Domain models, `jvmAndroidMain` source set |
| `core:repository` | ✅ | ✅ | Domain interfaces |
| `core:di` | ✅ | ✅ | Dispatchers, qualifiers |
| `core:navigation` | ✅ | ✅ | Shared Navigation 3 routes |
| `core:resources` | ✅ | ✅ | Compose Multiplatform resources |
| `core:datastore` | ✅ | ✅ | Multiplatform DataStore |
| `core:database` | ✅ | ✅ | Room KMP |
| `core:domain` | ✅ | ✅ | UseCases |
| `core:prefs` | ✅ | ✅ | Preferences layer |
| `core:network` | ✅ | ✅ | Ktor, `StreamFrameCodec`, `TcpTransport`, `SerialTransport`, `BleRadioTransport` |
| `core:data` | ✅ | ✅ | Data orchestration |
| `core:ble` | ✅ | ✅ | Kable multiplatform BLE abstractions in commonMain |
| `core:nfc` | ✅ | ✅ | NFC contract in commonMain; hardware in androidMain |
| `core:service` | ✅ | ✅ | Service layer; Android bindings in androidMain |
| `core:ui` | ✅ | ✅ | Shared Compose UI, pure KMP QR generator, `jvmAndroidMain` + `jvmMain` actuals |
| `core:testing` | ✅ | ✅ | Shared test doubles, fakes, and utilities for `commonTest` |
| `core:takserver` | ✅ | ✅ | TAK/ATAK integration, Fountain codec |
| `core:api` | ❌ | — | Android-only (AIDL). Intentional. |
| `core:barcode` | ❌ | — | Android-only (CameraX). Flavor split minimised to decoder factory only (ML Kit / ZXing). Shared contract in `core:ui`. |
**19/21** core modules are KMP with JVM targets. The 2 Android-only modules are intentionally platform-specific, with shared contracts already abstracted into `core:ui/commonMain`.
### Feature Modules (9 total — 9 KMP with JVM, 1 Android-only widget)
| Module | UI in commonMain? | Desktop wired? |
|---|:---:|:---:|
| `feature:settings` | ✅ | ✅ ~35 real screens; fully shared `settingsGraph` and UI |
| `feature:node` | ✅ | ✅ Adaptive list-detail; fully shared `nodesGraph`, `PositionLogScreen`, and `NodeContextMenu` |
| `feature:messaging` | ✅ | ✅ Adaptive contacts + messages; fully shared `contactsGraph`, `MessageScreen`, `ContactsScreen`, and `MessageListPaged` |
| `feature:connections` | ✅ | ✅ Shared `ConnectionsScreen` with dynamic transport detection |
| `feature:intro` | — | — | Screens remain in `androidMain`; shared ViewModel only |
| `feature:map` | — | Placeholder; shared `NodeMapViewModel`, `BaseMapViewModel`. Map rendering decomposed into 3 `CompositionLocal` provider contracts (`MapViewProvider`, `NodeTrackMapProvider`, `TracerouteMapProvider`) with per-flavor implementations in `:app` |
| `feature:firmware` | ✅ | ✅ Fully KMP: Unified OTA, native Secure DFU, USB/UF2, FirmwareRetriever |
| `feature:wifi-provision` | ✅ | ✅ KMP WiFi provisioning via BLE (Nymea protocol); shared UI and ViewModel |
| `feature:widget` | ❌ | — | Android-only (Glance appwidgets). Intentional. |
### Desktop Module
Working Compose Desktop application with:
- Navigation 3 shell (`NavigationRail` + `NavDisplay`) using shared routes
- Full Koin DI graph (stubs + real implementations)
- TCP, Serial/USB, and BLE transports with auto-reconnect and full `want_config` handshake
- Adaptive list-detail screens for nodes and contacts
- **Dynamic Connections screen** with automatic discovery of platform-supported transports (TCP, Serial/USB, BLE)
- **Desktop language picker** backed by `UiPreferencesDataSource.locale`, with immediate Compose Multiplatform resource updates
- **Navigation-preserving locale switching** via `Main.kt` `staticCompositionLocalOf` recomposition instead of recreating the Nav3 backstack
- Node detail metrics screens (Device, Environment, Signal, Power, Pax) wired with shared KMP + Vico charts
- **Feature-driven Architecture:** Desktop navigation completely relies on feature modules via `commonMain` exported graphs (`settingsGraph`, `nodesGraph`, `contactsGraph`, etc.), reducing the desktop module to a simple host shell.
- **Native notifications and system tray icon** wired via `DesktopNotificationManager`
- **Native release pipeline** generating `.dmg` (macOS), `.msi` (Windows), and `.deb` (Linux) installers in CI
## Scorecard
| Area | Score | Notes |
|---|---|---|
| Shared business/data logic | **9/10** | All core layers shared; RadioTransport interface unified |
| Shared feature/UI logic | **9/10** | 9 KMP feature modules; firmware fully migrated; wifi-provision added; `feature:intro` and `feature:map` share ViewModels but UI remains in `androidMain` |
| Android decoupling | **9/10** | No known `java.*` calls in `commonMain`; app module extraction in progress (navigation, connections, background services, and widgets extracted) |
| Multi-target readiness | **9/10** | Full JVM; release-ready desktop; iOS simulator builds compiling successfully |
| CI confidence | **9/10** | 26 modules validated (including feature:wifi-provision); native release installers automated |
| DI portability | **8/10** | Koin annotations in commonMain; supportedDeviceTypes injected per platform |
| Test maturity | **9/10** | Mokkery, Turbine, and Kotest integrated; property-based testing established; broad coverage across all 9 features. SfppHasher, AddressUtils, formatString hex, and MetricFormatter edge cases newly covered. Gaps: `core:service`, `core:network` (TcpTransport), `core:ble` state machine, `core:ui` utils |
## Completion Estimates
| Lens | % |
|---|---:|
| Android-first structural KMP | ~100% |
| Shared business logic | ~98% |
| Shared feature/UI | ~92% |
| True multi-target readiness | ~85% |
| "Add iOS without surprises" | ~100% |
## Proposed Next Steps for KMP Migration
Based on the latest codebase investigation, the following steps are proposed to complete the multi-target and iOS-readiness migrations:
1. **Wire Desktop Features:** Complete desktop UI wiring for `feature:intro` and implement a shared fallback for `feature:map` (which is currently a placeholder on desktop).
2. **Flesh out iOS Actuals:** Complete the actual implementations for iOS UI stubs (e.g., `AboutLibrariesLoader`, `rememberOpenMap`, `SettingsMainScreen`) that were recently added to unblock iOS compilation.
3. **Boot iOS Target:** Set up an initial skeleton Xcode project to start running the now-compiling `iosSimulatorArm64` / `iosArm64` binaries on a real simulator/device.
## Key Architecture Decisions
| Decision | Status | Details |
|---|---|---|
| Navigation 3 parity model (shared `TopLevelDestination` + platform adapters) | ✅ Done | Both shells use shared `TopLevelDestination` enum and `MeshtasticNavDisplay` from `core:ui/commonMain`; parity tests in `core:navigation/commonTest` |
| Hilt → Koin | ✅ Done | See [`decisions/koin-migration.md`](./decisions/koin-migration.md) |
| BLE abstraction (Kable) | ✅ Done | See [`decisions/ble-strategy.md`](./decisions/ble-strategy.md) |
| Firmware KMP migration (pure Secure DFU) | ✅ Done | Native Nordic Secure DFU protocol reimplemented in pure KMP using Kable; desktop is first-class target |
| Material 3 Adaptive (JetBrains) | ✅ Done | Version `1.3.0-alpha06` aligned with CMP `1.11.0-beta02`; supports Large (1200dp) and Extra-large (1600dp) breakpoints |
| JetBrains lifecycle/nav3 alias alignment | ✅ Done | All forked deps use `jetbrains-*` prefix in version catalog; `core:data` commonMain uses JetBrains lifecycle runtime |
| Expect/actual consolidation | ✅ Done | 10+ pairs eliminated (including `formatString`, `CommonUri`, `SfppHasher`); ~20 genuinely platform-specific retained (Parcelable, DateFormatter, Database, Location, Composable UI primitives) |
| Transport deduplication | ✅ Done | `StreamFrameCodec`, `TcpTransport`, and `SerialTransport` shared in `core:network` |
| **Transport Lifecycle Unification** | ✅ Done | `SharedRadioInterfaceService` orchestrates auto-reconnect, connection state, and heartbeat uniformly across Android and Desktop. |
| **Database Parity** | ✅ Done | `DatabaseManager` is pure KMP, giving iOS and Desktop support for multiple connected nodes with LRU caching. On JVM/Desktop, inactive databases are explicitly closed on switch (Room KMP's `setAutoCloseTimeout` is Android-only), and `desktopDataDir()` in `core:database/jvmMain` is the single source for data directory resolution. |
| Emoji picker unification | ✅ Done | Single commonMain implementation replacing 3 platform variants |
| Cross-platform deduplication pass | ✅ Done | Extracted shared `AlertHost`, `SharedDialogs`, `PlaceholderScreen`, `ThemePickerDialog`, `MeshtasticNavDisplay`, `formatLogsTo()`, `handleNodeAction()`, `findNodeByNameSuffix()`, `MeshtasticAppShell`, `BleRadioTransport`, and `BaseRadioTransportFactory` to `commonMain`; eliminated ~1,200 lines of duplicated Compose UI code across Android/desktop |
| URI unification | ✅ Done | `CommonUri` is a `typealias` to `com.eygraber.uri.Uri` (uri-kmp); `MeshtasticUri` wrapper deleted; bridge with `toAndroidUri()`/`toKmpUri()` |
| Utility commonization | ✅ Done | `formatString` → pure Kotlin parser in `commonMain`; `SfppHasher` and `CryptoCodec``Okio ByteString.sha256()`; `MetricFormatter` centralizes display strings (temperature, voltage, current, %, humidity, pressure, SNR, RSSI) |
## Navigation Parity Note
- Desktop and Android both use the shared `TopLevelDestination` enum from `core:navigation/commonMain` — no separate `DesktopDestination` remains.
- Both shells utilize the **Navigation 3 Scene-based architecture**, allowing for multi-pane layouts (e.g., three-pane on Large/XL displays) using shared routes.
- Both shells iterate `TopLevelDestination.entries` with shared icon mapping from `core:ui` (`TopLevelDestinationExt.icon`).
- Desktop locale changes now trigger a full subtree recomposition from `Main.kt` without resetting the shared Navigation 3 backstack, so translated labels update in place.
- Firmware remains available as an in-flow route instead of a top-level destination, matching Android information architecture.
- Android navigation graphs are decoupled and extracted into their respective feature modules, aligning with the Desktop architecture.
- Parity tests exist in `core:navigation/commonTest` (`NavigationParityTest`) and `desktop/test` (`DesktopTopLevelDestinationParityTest`).
- Remaining parity work: serializer registration validation and platform exception tracking.
## App Module Thinning Status
All major ViewModels have now been extracted to `commonMain` and no longer rely on Android-specific subclasses. Platform-specific dependencies (like `android.net.Uri` or Location permissions) have been successfully isolated behind injected `core:repository` interfaces (e.g., `FileService`, `LocationService`).
**The extraction of all feature-specific navigation graphs, background services, and widgets out of `:app` is complete.** The `:app` module now only serves as the root DI assembler and NavHost container.
Extracted to shared `commonMain` (no longer app-only):
- `SettingsViewModel``feature:settings/commonMain`
- `RadioConfigViewModel``feature:settings/commonMain`
- `DebugViewModel``feature:settings/commonMain`
- `MetricsViewModel``feature:node/commonMain`
- `UIViewModel``core:ui/commonMain`
- `ChannelViewModel``feature:settings/commonMain`
- `NodeMapViewModel``feature:map/commonMain` (Shared logic for node-specific maps)
- `BaseMapViewModel``feature:map/commonMain` (Core contract for all maps)
- `TracerouteOverlay``core:model/commonMain` (Pure data class for traceroute route segments; extracted from `feature:map` for cross-module reuse)
- `GeoConstants``core:model/commonMain` (Centralized `DEG_D`, `HEADING_DEG`, `EARTH_RADIUS_METERS` constants; eliminates 7 duplicate private constants)
Extracted to core KMP modules:
- Android Services, WorkManager Workers, and BroadcastReceivers → `core:service/androidMain`
- USB/Serial radio connections → `core:network/androidMain`
- TCP radio connections, BLE radio connections (`BleRadioTransport`), and mDNS/NSD Service Discovery → `core:network/commonMain` (with Android `NsdManager` and Desktop `JmDNS` implementations)
Remaining to be extracted from `:app` or unified in `commonMain`:
- `MapViewModel` (Unify Google/F-Droid flavors into a single `commonMain` class consuming a `MapConfigProvider` interface. `MapViewProvider` interface simplified — track rendering and traceroute rendering extracted to dedicated provider contracts)
## Prerelease Dependencies
| Dependency | Version | Why |
|---|---|---|
| Compose Multiplatform | `1.11.0-beta02` | Required for JetBrains Adaptive `1.3.0-alpha06` and Material 3 `1.11.0-alpha06` |
| Compose Multiplatform Material 3 | `1.11.0-alpha06` | Material 3 components including `NavigationSuiteScaffold` |
| Koin | `4.2.1` | Nav3 + K2 compiler plugin support |
| JetBrains Lifecycle | `2.11.0-alpha03` | Multiplatform ViewModel/lifecycle; includes `lifecycle-viewmodel-navigation3` for entry-scoped ViewModels |
| JetBrains Navigation 3 | `1.1.0-rc01` | Multiplatform navigation with Scene architecture, `NavEntry.metadata`, transition specs |
| JetBrains Navigation Event | `1.1.0-alpha01` | KMP `NavigationBackHandler` for predictive back |
| JetBrains Material 3 Adaptive | `1.3.0-alpha06` | `ListDetailPaneScaffold`, `ThreePaneScaffold`, Large/XL breakpoints |
| Kable BLE | `0.42.0` | Provides fully multiplatform BLE support |
**Policy:** Stable by default. RC when it unlocks KMP functionality. Alpha only behind hard abstraction seams. Do not downgrade CMP or Koin — they enable critical KMP features.
## References
- Roadmap: [`docs/roadmap.md`](./roadmap.md)
- Agent guide: [`AGENTS.md`](../AGENTS.md)
- Agent skills: [`.skills/`](../.skills/)
- Decision records: [`docs/decisions/`](./decisions/)

View File

@@ -1,116 +0,0 @@
# Roadmap
> Last updated: 2026-04-15
Forward-looking priorities for the Meshtastic KMP multi-target effort. For current state, see [`kmp-status.md`](./kmp-status.md).
## Architecture Health (Immediate)
These items address structural gaps identified in the March 2026 architecture review. They are prerequisites for safe multi-target expansion.
| Item | Impact | Effort | Status |
|---|---|---|---|
| Purge `java.util.Locale` from `commonMain` (3 files) | High | Low | ✅ |
| Replace `ConcurrentHashMap` in `commonMain` (3 files) | High | Low | ✅ |
| Create `core:testing` shared test fixtures | Medium | Low | ✅ |
| Add feature module `commonTest` (settings, node, messaging) | Medium | Medium | ✅ |
| Desktop Koin `checkModules()` integration test | Medium | Low | ✅ |
| Auto-wire Desktop ViewModels via K2 Compiler (eliminate manual wiring) | Medium | Low | ✅ |
| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low | ✅ |
| **iOS CI gate (compile-only validation)** | High | Medium | ✅ |
| **Commonize utilities** (`formatString`, `SfppHasher`, `CryptoCodec`, `CommonUri`) | High | Medium | ✅ |
| **Centralize metric formatting** (`MetricFormatter`) | Medium | Low | ✅ |
## Active Work
### Desktop Feature Completion (Phase 4)
**Objective:** Complete desktop wiring for all features and ensure full integration.
**Current State (March 2026):**
-**Settings:** ~35 screens with real configuration, including theme/about parity and desktop language picker support
-**Nodes:** Adaptive list-detail with node management
-**Messaging:** Adaptive contacts with message view + send
-**Connections:** Dynamic discovery of platform-supported transports (TCP, Serial/USB, BLE)
-**Map:** Placeholder only, needs MapLibre or alternative
- ⚠️ **Firmware:** Fully KMP (Unified OTA + native Secure DFU + USB/UF2); desktop is first-class target
- ⚠️ **Intro:** Onboarding flow (may not apply to desktop)
**Implementation Steps:**
1. **Tier 1: Core Wiring (Essential)**
- Complete Map integration (MapLibre or equivalent)
- Verify all features accessible via navigation
- Test navigation flows end-to-end
2. **Tier 2: Polish (High Priority)**
- Additional desktop-specific settings polish
-**Keyboard shortcuts** via `onPreviewKeyEvent` (MenuBar removed)
- **Adaptive density & multitasking optimizations** (2026 Desktop Guidelines)
- Window management
- State persistence
3. **Tier 3: Advanced (Nice-to-have)**
- Performance optimization
- Advanced map features
- Theme customization
- Multi-window support
| Transport | Platform | Status |
|---|---|---|
| TCP | Desktop (JVM) | ✅ Done — shared `StreamFrameCodec` + `TcpTransport` in `core:network` |
| Serial/USB | Desktop (JVM) | ✅ Done — jSerialComm |
| MQTT | All (KMP) | ✅ Completed — KMQTT in commonMain |
| BLE | All (KMP) | ✅ Done — Kable in `commonMain` (`BleRadioTransport`) |
### Desktop Feature Gaps
| Feature | Status |
|---|---|
| Settings | ✅ ~35 real screens (fully shared); `DeviceConfig`, `PositionConfig`, `SecurityConfig`, `ExternalNotificationConfig` fully unified into `commonMain` |
| Node list | ✅ Adaptive list-detail with real `NodeDetailContent` |
| Messaging | ✅ Adaptive contacts with real message view + send |
| Connections | ✅ Unified shared UI with dynamic transport detection |
| Metrics logs | ✅ TracerouteLog, NeighborInfoLog, HostMetricsLog |
| Map | ❌ Needs MapLibre or equivalent |
| QR Generation | ✅ Pure KMP generation via `qrcode-kotlin` |
| Charts | ✅ Vico KMP charts wired in commonMain (Device, Environment, Signal, Power, Pax) |
| Debug Panel | ✅ Real screen (mesh log viewer via shared `DebugViewModel`) |
| Notifications | ✅ Desktop native notifications with system tray icon support |
| MenuBar | ✅ Removed — replaced with `onPreviewKeyEvent` keyboard shortcuts (⌘Q, ⌘,, ⌘⇧T, ⌘1-4, ⌘/) |
| About | ✅ Shared `commonMain` screen (AboutLibraries KMP `produceLibraries` + per-platform JSON) |
| Packaging | ✅ Done — Native distribution pipeline in CI (DMG, MSI, DEB). Windows `upgradeUuid` set; macOS signing/notarization wired behind `SIGN_MACOS` env flag; desktop build attestation in release CI. Flatpak packaging maintained externally at [vidplace7/org.meshtastic.desktop](https://github.com/vidplace7/org.meshtastic.desktop) (includes AppStream metainfo, `.desktop` entry, and JBR bundling); see [PR #4807](https://github.com/meshtastic/Meshtastic-Android/pull/4807) for `flatpakGradleGenerator` integration |
## Near-Term Priorities (30 days)
1. **Evaluate KMP-native testing tools** — ✅ **Done:** Fully evaluated and integrated `Mokkery`, `Turbine`, and `Kotest` across the KMP modules. `mockk` has been successfully replaced, enabling property-based and Flow testing in `commonTest` for iOS readiness.
2. **Desktop Map Integration** — Address the major Desktop feature gap by implementing a raster map view using [**MapComposeMP**](https://github.com/p-lr/MapComposeMP).
- Implement Desktop providers for the 3 decomposed map contracts: `MapViewProvider` (main map), `NodeTrackMapProvider` (per-node track overlay for `PositionLogScreen`), and `TracerouteMapProvider` (traceroute visualization).
- Implement a **Web Mercator Projection** helper in `feature:map/commonMain` to translate GPS coordinates to the 2D image plane.
- Leverage the existing `BaseMapViewModel` contract and `TracerouteNodeSelection` logic in `commonMain`.
3. **Unify `MapViewModel`** — Collapse the remaining Google and F-Droid specific `MapViewModel` classes in the `:app` module into a single `commonMain` implementation by isolating platform-specific settings (styles, tile sources) behind a repository interface. The `MapViewProvider` interface has been simplified (track/traceroute rendering extracted to dedicated providers), reducing the surface area of this unification.
4. **iOS CI gate** — ✅ **Done:** added `iosArm64()`/`iosSimulatorArm64()` to convention plugins and CI. `commonMain` successfully compiles on iOS.
## Medium-Term Priorities (60 days)
1. **iOS proof target** — ✅ **Done (Stubbing):** Stubbed iOS target implementations (`NoopStubs.kt` equivalent) to successfully pass compile-time checks. **Next:** Setup an Xcode skeleton project and launch the iOS app.
2. **Migrate to Navigation 3 Scene-based architecture** — leverage the first stable release of Nav 3 to support multi-pane layouts. **Investigate 3-pane "Power User" scenes** (e.g., Node List + Detail + Map/Charts) on Large (1200dp) and Extra-large (1600dp) displays (Android 16 QPR3).
3. **`core:api` contract split** — separate transport-neutral service contracts from the Android AIDL packaging to support iOS/Desktop service layers.
## Longer-Term (90+ days)
1. **Platform-Native UI Interop**
- **iOS Maps & Camera:** Implement `MapLibre` or `MKMapView` via Compose Multiplatform's `UIKitView`. Leverage `AVCaptureSession` wrapped in `UIKitView` to fulfill the `LocalBarcodeScannerProvider` contract.
- **Web (wasmJs) Integrations:** Leverage `HtmlView` to embed raw DOM elements (e.g., `<video>`, `<iframe>`, or canvas-based maps) directly into the Compose UI tree while binding the root app via `CanvasBasedWindow`.
2. **Module maturity dashboard** — living inventory of per-module KMP readiness.
3. **Shared UI vs Shared Logic split** — If the iOS target utilizes native SwiftUI instead of Compose Multiplatform, evaluate splitting feature modules into pure `sharedLogic` (business rules, ViewModels) and `sharedUI` (Compose Multiplatform) to prevent dragging Compose dependencies into pure native iOS apps.
## Design Principles
1. **Solve in `commonMain` first.** If it doesn't need platform APIs, it belongs in `commonMain`.
2. **Interfaces in `commonMain`, implementations per-target.** The repository pattern is established — extend it. Prefer dependency injection (Koin) with interfaces over `expect`/`actual` declarations whenever possible to keep architecture decoupled and highly testable.
3. **UI Interop Strategies.** When a Compose Multiplatform equivalent doesn't exist (e.g., Maps, Camera), use standard interop APIs rather than extracting the entire screen to native code. Use `AndroidView` for Android, `UIKitView` for iOS, `SwingPanel` for JVM/Desktop, and `HtmlView` for Web (`wasmJs`). Always wrap these in a shared `commonMain` interface contract (like `LocalBarcodeScannerProvider`).
4. **Stubs are a valid first implementation.** Every target starts with no-op stubs, then graduates to real implementations.
5. **Feature modules stay target-agnostic in `commonMain`.** Platform UI goes in platform source sets. Keep the UI layer dumb and rely on shared ViewModels (Unidirectional Data Flow) to drive state.
6. **Transport is a pluggable adapter.** BLE, serial, TCP, MQTT all implement `RadioTransport` and are orchestrated by a shared `RadioInterfaceService`.
7. **CI validates every target.** If a module declares `jvm()`, CI compiles it. No exceptions. Run tests on appropriate host runners (macOS for iOS, Linux for JVM/Android) to catch platform regressions.
8. **Test in `commonTest` first.** ViewModel and business logic tests belong in `commonTest` so every target runs them. Use shared `core:testing` utilities to minimize duplication.
9. **Zero Platform Leaks.** Never import `java.*` or `android.*` inside `commonMain`. Use KMP-native alternatives like `kotlinx-datetime` and `Okio`.

View File

@@ -3,5 +3,11 @@
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>Kdoc:TcpDiscoveryHelpers.kt:/** * Shared helpers for TCP device discovery logic used by both [CommonGetDiscoveredDevicesUseCase] and the * Android-specific variant. */</ID>
<ID>PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun ConnectingDeviceInfoPreview</ID>
<ID>PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun DeviceListItemPreview</ID>
<ID>PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun DeviceSectionHeaderPreview</ID>
<ID>PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun DisconnectButtonPreview</ID>
<ID>PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun EmptyStateContentPreview</ID>
<ID>PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun TransportFilterChipsPreview</ID>
</CurrentIssues>
</SmellBaseline>

View File

@@ -0,0 +1,86 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.connections.component
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewLightDark
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Search
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.viewmodel.ConnectionStatus
import org.meshtastic.feature.connections.model.DeviceListEntry
import org.meshtastic.feature.connections.ui.components.ConnectingDeviceInfo
import org.meshtastic.feature.connections.ui.components.DeviceListItem
import org.meshtastic.feature.connections.ui.components.DeviceSectionHeader
import org.meshtastic.feature.connections.ui.components.DisconnectButton
import org.meshtastic.feature.connections.ui.components.EmptyStateContent
import org.meshtastic.feature.connections.ui.components.TransportFilterChips
@PreviewLightDark
@Composable
fun DeviceListItemPreview() {
val device = DeviceListEntry.Tcp(name = "Meshtastic_abcd", fullAddress = "s192.168.1.100")
AppTheme { DeviceListItem(connectionState = ConnectionState.Disconnected, device = device, onSelect = {}) }
}
@PreviewLightDark
@Composable
fun DisconnectButtonPreview() {
AppTheme { DisconnectButton(onClick = {}) }
}
@PreviewLightDark
@Composable
fun ConnectingDeviceInfoPreview() {
AppTheme {
ConnectingDeviceInfo(
deviceName = "Meshtastic_abcd",
deviceAddress = "AA:BB:CC:DD:EE:FF",
connectionStatus = ConnectionStatus.CONNECTING,
connectionProgress = "Discovering services...",
onClickDisconnect = {},
)
}
}
@PreviewLightDark
@Composable
fun EmptyStateContentPreview() {
AppTheme { EmptyStateContent(text = "No devices found", imageVector = MeshtasticIcons.Search) }
}
@PreviewLightDark
@Composable
fun DeviceSectionHeaderPreview() {
AppTheme { DeviceSectionHeader(title = "Bluetooth Devices", showProgress = true) }
}
@PreviewLightDark
@Composable
fun TransportFilterChipsPreview() {
AppTheme {
TransportFilterChips(
showBle = true,
showNetwork = true,
showUsb = false,
onToggleBle = {},
onToggleNetwork = {},
onToggleUsb = {},
)
}
}

View File

@@ -3,12 +3,22 @@
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>Kdoc:LegacyDfuTransport.kt:LegacyDfuTransport$/** * Stream [firmware] to the Packet characteristic, awaiting a [LegacyDfuResponse.PacketReceipt] every * [PRN_INTERVAL_PACKETS] packets and verifying the bytes-received count. * * Watches the connection state in parallel with the write loop; if the link drops mid-stream we cancel the write * coroutine and surface a [DfuException.ConnectionFailed] immediately rather than waiting indefinitely for a write * that will never complete. */</ID>
<ID>ModifierMissing:FirmwareUpdateScreen.kt:@Composable internal fun CheckingState</ID>
<ID>ModifierMissing:FirmwareUpdateScreen.kt:@Composable internal fun DisclaimerDialog</ID>
<ID>ModifierMissing:FirmwareUpdateScreen.kt:@Composable internal fun ErrorState</ID>
<ID>ModifierMissing:FirmwareUpdateScreen.kt:@Composable internal fun SuccessState</ID>
<ID>ModifierMissing:FirmwareUpdateScreen.kt:@Composable internal fun VerifyingState</ID>
<ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable @Suppress("LongMethod") private fun ReadyState</ID>
<ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable internal fun CheckingState</ID>
<ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable internal fun ErrorState</ID>
<ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable internal fun VerifyingState</ID>
<ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable private fun AwaitingFileSaveState</ID>
<ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable private fun CheckingState</ID>
<ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable private fun ErrorState</ID>
<ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable private fun VerificationFailedState</ID>
<ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable private fun VerifyingState</ID>
<ID>PreviewPublic:FirmwarePreviews.kt:@PreviewLightDark @Composable fun CheckingStatePreview</ID>
<ID>PreviewPublic:FirmwarePreviews.kt:@PreviewLightDark @Composable fun DisclaimerDialogPreview</ID>
<ID>PreviewPublic:FirmwarePreviews.kt:@PreviewLightDark @Composable fun ErrorStatePreview</ID>
<ID>PreviewPublic:FirmwarePreviews.kt:@PreviewLightDark @Composable fun SuccessStatePreview</ID>
<ID>PreviewPublic:FirmwarePreviews.kt:@PreviewLightDark @Composable fun VerifyingStatePreview</ID>
<ID>TooGenericExceptionCaught:FirmwareUpdateViewModel.kt:FirmwareUpdateViewModel$e: Exception</ID>
</CurrentIssues>
</SmellBaseline>

View File

@@ -0,0 +1,100 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.firmware
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import org.meshtastic.core.resources.UiText
import org.meshtastic.core.ui.theme.AppTheme
@PreviewLightDark
@Composable
fun VerifyingStatePreview() {
AppTheme {
Surface {
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
VerifyingState()
}
}
}
}
@PreviewLightDark
@Composable
fun CheckingStatePreview() {
AppTheme {
Surface {
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
CheckingState()
}
}
}
}
@PreviewLightDark
@Composable
fun ErrorStatePreview() {
AppTheme {
Surface {
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
ErrorState(error = UiText.DynamicString("Connection lost"), onRetry = {})
}
}
}
}
@PreviewLightDark
@Composable
fun SuccessStatePreview() {
AppTheme {
Surface {
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
SuccessState(onDone = {})
}
}
}
}
@PreviewLightDark
@Composable
fun DisclaimerDialogPreview() {
AppTheme { Surface { DisclaimerDialog(updateMethod = FirmwareUpdateMethod.Ble, onDismiss = {}, onConfirm = {}) } }
}

View File

@@ -336,7 +336,7 @@ private fun FirmwareUpdateContent(
}
@Composable
private fun VerifyingState() {
internal fun VerifyingState() {
CircularProgressIndicator(modifier = Modifier.size(64.dp))
Spacer(Modifier.height(24.dp))
Text(stringResource(Res.string.firmware_update_verifying), style = MaterialTheme.typography.titleMedium)
@@ -351,7 +351,7 @@ private fun VerifyingState() {
}
@Composable
private fun CheckingState() {
internal fun CheckingState() {
CircularProgressIndicator(modifier = Modifier.size(64.dp))
Spacer(Modifier.height(24.dp))
Text(stringResource(Res.string.firmware_update_checking), style = MaterialTheme.typography.bodyLarge)
@@ -446,7 +446,7 @@ private fun ReadyState(
}
@Composable
private fun DisclaimerDialog(updateMethod: FirmwareUpdateMethod, onDismiss: () -> Unit, onConfirm: () -> Unit) {
internal fun DisclaimerDialog(updateMethod: FirmwareUpdateMethod, onDismiss: () -> Unit, onConfirm: () -> Unit) {
MeshtasticDialog(
onDismiss = onDismiss,
title = stringResource(Res.string.firmware_update_disclaimer_title),
@@ -832,7 +832,7 @@ private fun VerificationFailedState(onRetry: () -> Unit, onIgnore: () -> Unit) {
}
@Composable
private fun ErrorState(error: UiText, onRetry: () -> Unit) {
internal fun ErrorState(error: UiText, onRetry: () -> Unit) {
Icon(
MeshtasticIcons.Dangerous,
contentDescription = null,
@@ -855,7 +855,7 @@ private fun ErrorState(error: UiText, onRetry: () -> Unit) {
}
@Composable
private fun SuccessState(onDone: () -> Unit) {
internal fun SuccessState(onDone: () -> Unit) {
val haptic = LocalHapticFeedback.current
LaunchedEffect(Unit) { haptic.performHapticFeedback(HapticFeedbackType.LongPress) }

View File

@@ -4,5 +4,6 @@
<CurrentIssues>
<ID>ComposableParamOrder:PermissionScreenLayout.kt:@Composable internal fun PermissionScreenLayout</ID>
<ID>ParameterNaming:WelcomeScreen.kt:onGetStarted: () -> Unit</ID>
<ID>PreviewPublic:WelcomeScreen.kt:@Preview @Composable fun WelcomeScreenPreview</ID>
</CurrentIssues>
</SmellBaseline>

View File

@@ -116,8 +116,9 @@ internal fun WelcomeScreen(onGetStarted: () -> Unit) {
}
}
@Suppress("PreviewPublic")
@PreviewLightDark
@Composable
private fun WelcomeScreenPreview() {
fun WelcomeScreenPreview() {
AppTheme { Surface { WelcomeScreen(onGetStarted = {}) } }
}

View File

@@ -31,6 +31,10 @@
<ID>ParameterNaming:MessageScreenComponents.kt:onToggleFilteringDisabled: () -> Unit = {}</ID>
<ID>ParameterNaming:MessageScreenComponents.kt:onToggleShowFiltered: () -> Unit</ID>
<ID>ParameterNaming:MessageScreenComponents.kt:onToggleShowFiltered: () -> Unit = {}</ID>
<ID>PreviewPublic:Message.kt:@PreviewLightDark @Composable fun MessageInputPreview</ID>
<ID>PreviewPublic:QuickChatPreviews.kt:@PreviewLightDark @Composable fun EditQuickChatDialogPreview</ID>
<ID>PreviewPublic:QuickChatPreviews.kt:@PreviewLightDark @Composable fun QuickChatItemPreview</ID>
<ID>PreviewPublic:ReactionPreviews.kt:@PreviewLightDark @Composable fun ReactionItemPreview</ID>
<ID>ViewModelForwarding:AdaptiveContactsScreen.kt:ContactsScreen( onNavigateToShare = { backStack.add(ChannelsRoute.ChannelsGraph) }, sharedContactRequested = sharedContactRequested, requestChannelSet = requestChannelSet, onHandleDeepLink = onHandleDeepLink, onClearSharedContactRequested = onClearSharedContactRequested, onClearRequestChannelUrl = onClearRequestChannelUrl, viewModel = contactsViewModel, onClickNodeChip = { backStack.add(NodesRoute.NodeDetail(it)) }, onNavigateToMessages = { contactKey -> backStack.add(ContactsRoute.Messages(contactKey)) }, onNavigateToNodeDetails = { backStack.add(NodesRoute.NodeDetail(it)) }, scrollToTopEvents = scrollToTopEvents, activeContactKey = null, )</ID>
</CurrentIssues>
</SmellBaseline>

View File

@@ -521,7 +521,7 @@ private fun MessageInput(
@PreviewLightDark
@Composable
private fun MessageInputPreview() {
fun MessageInputPreview() {
AppTheme {
Surface {
Column(modifier = Modifier.padding(8.dp)) {

View File

@@ -23,13 +23,13 @@ import org.meshtastic.core.ui.theme.AppTheme
@PreviewLightDark
@Composable
private fun QuickChatItemPreview() {
fun QuickChatItemPreview() {
AppTheme { QuickChatItem(action = QuickChatAction(name = "TST", message = "Test", position = 0)) }
}
@PreviewLightDark
@Composable
private fun EditQuickChatDialogPreview() {
fun EditQuickChatDialogPreview() {
AppTheme {
EditQuickChatDialog(
action = QuickChatAction(name = "TST", message = "Test", position = 0),

View File

@@ -29,11 +29,11 @@ import org.meshtastic.proto.User
@PreviewLightDark
@Composable
private fun ReactionItemPreview() {
fun ReactionItemPreview() {
AppTheme {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
ReactionItem(emoji = "\uD83D\uDE42")
ReactionItem(emoji = "\uD83D\uDE42", emojiCount = 2)
ReactionItem(emoji = "+1")
ReactionItem(emoji = "+1", emojiCount = 2)
AddReactionButton()
}
}

View File

@@ -66,6 +66,18 @@
<ID>ParameterNaming:SignalMetrics.kt:onPointSelected: (Double) -> Unit</ID>
<ID>ParameterNaming:TimeFrameSelector.kt:onTimeFrameSelected: (TimeFrame) -> Unit</ID>
<ID>ParameterNaming:TracerouteChart.kt:onPointSelected: (Double) -> Unit</ID>
<ID>PreviewPublic:CommonCharts.kt:@PreviewLightDark @Composable fun LegendPreview</ID>
<ID>PreviewPublic:DeviceMetrics.kt:@PreviewLightDark @Suppress("detekt:MagicNumber") // Compose preview with fake data @Composable fun DeviceMetricsCardPreview</ID>
<ID>PreviewPublic:EnvironmentMetrics.kt:@PreviewLightDark @Suppress("MagicNumber") // Compose preview with fake data @Composable fun PreviewEnvironmentMetricsContent</ID>
<ID>PreviewPublic:NodeDetailComponentPreviews.kt:@PreviewLightDark @Composable fun DeviceActionsLocalPreview</ID>
<ID>PreviewPublic:NodeDetailComponentPreviews.kt:@PreviewLightDark @Composable fun DeviceActionsRemotePreview</ID>
<ID>PreviewPublic:NodeDetailComponentPreviews.kt:@PreviewLightDark @Composable fun NodeDetailsSectionPreview</ID>
<ID>PreviewPublic:NodeDetailComponentPreviews.kt:@PreviewLightDark @Composable fun PositionInlineContentPreview</ID>
<ID>PreviewPublic:NodeDetailComponentPreviews.kt:@PreviewLightDark @Composable fun TelemetricActionsSectionEmptyPreview</ID>
<ID>PreviewPublic:NodeDetailComponentPreviews.kt:@PreviewLightDark @Composable fun TelemetricActionsSectionPreview</ID>
<ID>PreviewPublic:NodeDetailPreviews.kt:@PreviewLightDark @Composable fun NodeDetailContentLoadingPreview</ID>
<ID>PreviewPublic:NodeDetailPreviews.kt:@PreviewLightDark @Composable fun NodeDetailContentLocalPreview</ID>
<ID>PreviewPublic:NodeDetailPreviews.kt:@PreviewLightDark @Composable fun NodeDetailContentRemotePreview</ID>
<ID>TooGenericExceptionCaught:MetricsViewModel.kt:MetricsViewModel$e: Exception</ID>
<ID>TooGenericExceptionCaught:NodeManagementActions.kt:NodeManagementActions$ex: Exception</ID>
<ID>ViewModelForwarding:NodeDetailScreens.kt:NodeDetailScaffold( modifier = modifier, uiState = uiState, viewModel = viewModel, navigateToMessages = navigateToMessages, onNavigate = onNavigate, onNavigateUp = onNavigateUp, compassViewModel = compassViewModel, )</ID>

View File

@@ -38,7 +38,7 @@ private val previewData = NodePreviewParameterProvider()
@PreviewLightDark
@Composable
private fun DeviceActionsRemotePreview() {
fun DeviceActionsRemotePreview() {
val node = previewData.mickeyMouse
AppTheme {
Surface {
@@ -65,7 +65,7 @@ private fun DeviceActionsRemotePreview() {
@PreviewLightDark
@Composable
private fun DeviceActionsLocalPreview() {
fun DeviceActionsLocalPreview() {
val node = previewData.mickeyMouse
AppTheme {
Surface {
@@ -90,7 +90,7 @@ private fun DeviceActionsLocalPreview() {
@PreviewLightDark
@Composable
private fun TelemetricActionsSectionPreview() {
fun TelemetricActionsSectionPreview() {
val node = previewData.mickeyMouse
AppTheme {
Surface {
@@ -118,7 +118,7 @@ private fun TelemetricActionsSectionPreview() {
@PreviewLightDark
@Composable
private fun TelemetricActionsSectionEmptyPreview() {
fun TelemetricActionsSectionEmptyPreview() {
val node = previewData.minnieMouse
AppTheme {
Surface {
@@ -142,7 +142,7 @@ private fun TelemetricActionsSectionEmptyPreview() {
@PreviewLightDark
@Composable
private fun PositionInlineContentPreview() {
fun PositionInlineContentPreview() {
val node = previewData.mickeyMouse
AppTheme {
Surface {
@@ -162,7 +162,7 @@ private fun PositionInlineContentPreview() {
@PreviewLightDark
@Composable
private fun NodeDetailsSectionPreview() {
fun NodeDetailsSectionPreview() {
val node = previewData.mickeyMouse
AppTheme { Surface { NodeDetailsSection(node = node) } }
}

View File

@@ -38,7 +38,7 @@ private val previewData = NodePreviewParameterProvider()
@PreviewLightDark
@Composable
private fun NodeDetailContentRemotePreview() {
fun NodeDetailContentRemotePreview() {
val node = previewData.mickeyMouse
AppTheme {
Surface {
@@ -67,7 +67,7 @@ private fun NodeDetailContentRemotePreview() {
@PreviewLightDark
@Composable
private fun NodeDetailContentLocalPreview() {
fun NodeDetailContentLocalPreview() {
val node = previewData.mickeyMouse
AppTheme {
Surface {
@@ -89,7 +89,7 @@ private fun NodeDetailContentLocalPreview() {
@PreviewLightDark
@Composable
private fun NodeDetailContentLoadingPreview() {
fun NodeDetailContentLoadingPreview() {
AppTheme {
Surface {
NodeDetailContent(

View File

@@ -253,9 +253,8 @@ fun MetricIndicator(color: Color, modifier: Modifier = Modifier) {
}
@PreviewLightDark
@Suppress("unused") // Compose preview
@Composable
private fun LegendPreview() {
fun LegendPreview() {
val data =
listOf(
LegendData(nameRes = Res.string.rssi, color = Color.Red, isLine = true),

View File

@@ -476,8 +476,8 @@ private fun DeviceMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick
@PreviewLightDark
@Suppress("detekt:MagicNumber") // Compose preview with fake data
@Composable
private fun DeviceMetricsCardPreview() {
val now = nowSeconds.toInt()
fun DeviceMetricsCardPreview() {
val now = 1700000000
val telemetry =
Telemetry(
time = now,

View File

@@ -44,7 +44,6 @@ import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.DateFormatter
import org.meshtastic.core.common.util.MetricFormatter
import org.meshtastic.core.common.util.formatString
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.model.util.TimeConstants.MS_PER_SEC
import org.meshtastic.core.resources.Res
@@ -541,7 +540,7 @@ private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFa
@PreviewLightDark
@Suppress("MagicNumber") // Compose preview with fake data
@Composable
private fun PreviewEnvironmentMetricsContent() {
fun PreviewEnvironmentMetricsContent() {
val fakeEnvMetrics =
org.meshtastic.proto.EnvironmentMetrics(
temperature = 22.5f,
@@ -563,6 +562,6 @@ private fun PreviewEnvironmentMetricsContent() {
rainfall_1h = 1.5f,
rainfall_24h = 12.3f,
)
val fakeTelemetry = Telemetry(time = nowSeconds.toInt(), environment_metrics = fakeEnvMetrics)
val fakeTelemetry = Telemetry(time = 1700000000, environment_metrics = fakeEnvMetrics)
AppTheme { Surface { EnvironmentMetricsContent(telemetry = fakeTelemetry, environmentDisplayFahrenheit = false) } }
}

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