Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Build and Test

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
java-version: [21]

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK ${{ matrix.java-version }}
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java-version }}
distribution: 'temurin'

- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-

- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@v3

- name: Make gradlew executable
run: chmod +x ./gradlew

- name: Compile code
run: ./gradlew compileJava --no-daemon

- name: Compile test code
run: ./gradlew compileTestJava --no-daemon

- name: Run unit tests
run: ./gradlew test --no-daemon --continue

- name: Build application
run: ./gradlew build --no-daemon

- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: |
build/test-results/test/*.xml

- name: Upload build artifacts
uses: actions/upload-artifact@v4
if: success()
with:
name: jar-artifacts-java-${{ matrix.java-version }}
path: build/libs/

- name: Upload test coverage
uses: actions/upload-artifact@v4
if: always()
with:
name: test-coverage-java-${{ matrix.java-version }}
path: build/reports/
59 changes: 59 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: CI Pipeline

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'

- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-

- name: Make gradlew executable
run: chmod +x ./gradlew

- name: Run tests
run: ./gradlew test --no-daemon --stacktrace

- name: Generate test report
uses: dorny/test-reporter@v1
if: success() || failure()
with:
name: Test Results
path: 'build/test-results/test/*.xml'
reporter: java-junit

- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: build/test-results/test/

- name: Upload coverage reports
uses: actions/upload-artifact@v4
if: always()
with:
name: test-reports
path: build/reports/tests/test/
22 changes: 21 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,30 @@ We welcome all kinds of contributions, whether you're fixing a bug, writing docu
2. Set up the project

## Running Tests
To run tests:
To run tests locally:
```bash
./gradlew test
```

To run the full build (including tests):
```bash
./gradlew build
```

## Continuous Integration
This project uses GitHub Actions for continuous integration. All pull requests and pushes to main/develop branches will automatically:
- Build the project with Java 21
- Run all unit tests (currently 61 tests)
- Generate test reports and artifacts
- Validate the Gradle wrapper

The CI pipeline ensures code quality by:
- ✅ Running all unit tests including markdown export functionality
- ✅ Compiling both main and test code
- ✅ Generating test coverage reports
- ✅ Uploading build artifacts

Make sure all tests pass locally before submitting a pull request. The CI checks must pass before merging.

## License
By contributing, you agree that your contributions will be licensed under the MIT License.
36 changes: 29 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# Parpt - Project Audit & Revenue Prioritization Tool

[![CI Pipeline](https://github.com/Stephenson-Software/Parpt/actions/workflows/ci.yml/badge.svg)](https://github.com/Stephenson-Software/Parpt/actions/workflows/ci.yml)
[![Build and Test](https://github.com/Stephenson-Software/Parpt/actions/workflows/build.yml/badge.svg)](https://github.com/Stephenson-Software/Parpt/actions/workflows/build.yml)

Parpt is an interactive CLI tool that helps developers, indie creators and teams evaluate and prioritize their software projects using structured metrics like ICE and RICE.

## Features
- Guided project scoring using ICE and RICE methods
- Calculate ICE (Impact, Confidence, Ease) scores
- Calculate RICE (Reach, Impact, Confidence, Effort) scores
- Save project entries to Markdown and JSON
- Obsidian-compatible for review workflows
- Export projects to Markdown and JSON formats
- Obsidian-compatible markdown export with sorting by ICE/RICE scores
- Rank and sort projects by monetization, potential, feasibility and effort
- Spring Boot architecture with interactive shell
- 100% local-first and open source
Expand All @@ -30,19 +34,37 @@ java -jar build/libs/parpt.jar
Run the CLI:
java -jar parpt.jar

Available commands:
- `create` - Create a new project with guided scoring
- `list` - List all projects with scores
- `view <project-name>` - View detailed project information
- `export` - Export all projects to Markdown format
- `help` - Show available commands

You'll be prompted to enter:
- Project name and description
- Detailed scoring for each category (1-5 scale)
- Parpt will then:
- Calculate ICE and RICE scores
- Save the results to `projects.json` and `projects.md`
- Save the results to `projects.json`
- Allow you to export to `projects.md` sorted by priority
- Help you sort and review your efforts over time

### Export Examples
```bash
# Export projects sorted by ICE score (default)
export

# Export projects sorted by RICE score
export --sort rice
```

## Roadmap
- [ ] Project input and validation loop
- [ ] Score calculation engine
- [ ] Markdown and JSON writer modules
- [ ] CLI configuration and persistence
- [x] Project input and validation loop
- [x] Score calculation engine
- [x] Markdown and JSON writer modules
- [x] CLI configuration and persistence
- [x] Obsidian-compatible markdown export with sorting
- [ ] Visualization of project scores
- [ ] Batch project comparison features

Expand Down
Empty file modified gradlew
100644 → 100755
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.preponderous.parpt.command;

import com.preponderous.parpt.domain.Project;
import com.preponderous.parpt.repo.ProjectMarkdownWriter;
import com.preponderous.parpt.service.ProjectService;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;

import java.util.List;

@ShellComponent
public class ExportProjectsCommand {

private final ProjectService projectService;
private final ProjectMarkdownWriter markdownWriter;

public ExportProjectsCommand(ProjectService projectService, ProjectMarkdownWriter markdownWriter) {
this.projectService = projectService;
this.markdownWriter = markdownWriter;
}

@ShellMethod(key = "export", value = "Exports all projects to Markdown format, sorted by score.")
public String execute(
@ShellOption(value = {"-s", "--sort"}, help = "Sort by 'ice' or 'rice' score (default: ice)", defaultValue = "ice") String sortBy
) {
List<Project> projects = projectService.getProjects();

if (projects.isEmpty()) {
return "No projects found to export.";
}

boolean sortByRice = "rice".equalsIgnoreCase(sortBy);

if (!sortByRice && !"ice".equalsIgnoreCase(sortBy)) {
return "Invalid sort option. Use 'ice' or 'rice'.";
}

try {
markdownWriter.writeMarkdown(projects, sortByRice);
String scoreType = sortByRice ? "RICE" : "ICE";
return String.format("Successfully exported %d projects to projects.md, sorted by %s score.",
projects.size(), scoreType);
} catch (Exception e) {
return "Failed to export projects: " + e.getMessage();
}
}
}
100 changes: 100 additions & 0 deletions src/main/java/com/preponderous/parpt/export/MarkdownFormatter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.preponderous.parpt.export;

import com.preponderous.parpt.domain.Project;
import com.preponderous.parpt.score.ScoreCalculator;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
* Component responsible for formatting project data into markdown strings.
* This class handles the actual markdown formatting logic without dealing with file I/O.
*/
@Component
public class MarkdownFormatter {

private final ScoreCalculator scoreCalculator;
private final ScoreDescriptionProvider scoreDescriptionProvider;

public MarkdownFormatter(ScoreCalculator scoreCalculator, ScoreDescriptionProvider scoreDescriptionProvider) {
this.scoreCalculator = scoreCalculator;
this.scoreDescriptionProvider = scoreDescriptionProvider;
}

/**
* Generates the markdown header with timestamp and sorting information.
*
* @param sortByRice true if sorted by RICE, false if sorted by ICE
* @return formatted header string
*/
public String formatHeader(boolean sortByRice) {
StringBuilder header = new StringBuilder();
header.append("# Project Priorities\n\n");
header.append("*Generated on ")
.append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
.append("*\n\n");

String scoreType = sortByRice ? "RICE" : "ICE";
header.append("*Sorted by ").append(scoreType).append(" score (highest to lowest)*\n\n");

return header.toString();
}

/**
* Formats a single project into markdown.
*
* @param project the project to format
* @param rank the ranking position (1, 2, 3, etc.)
* @return formatted project markdown string
*/
public String formatProject(Project project, int rank) {
StringBuilder content = new StringBuilder();

double iceScore = scoreCalculator.ice(project);
double riceScore = scoreCalculator.rice(project);

// Project header and description
content.append("## ").append(rank).append(". ").append(project.getName()).append("\n\n");
content.append("**Description:** ").append(project.getDescription()).append("\n\n");

// Scores summary
content.append("### Scores\n");
content.append("- **ICE Score:** ").append(String.format("%.2f", iceScore)).append("\n");
content.append("- **RICE Score:** ").append(String.format("%.2f", riceScore)).append("\n\n");

// Individual components
content.append("### Components\n");
content.append(formatScoreComponent("Impact", project.getImpact()));
content.append(formatScoreComponent("Confidence", project.getConfidence()));
content.append(formatScoreComponent("Ease", project.getEase()));
content.append(formatScoreComponent("Reach", project.getReach()));
content.append(formatScoreComponent("Effort", project.getEffort()));
content.append("\n");

content.append("---\n\n");

return content.toString();
}

/**
* Formats a single score component line.
*
* @param componentName the name of the component (Impact, Confidence, etc.)
* @param score the numerical score (1-5)
* @return formatted component line
*/
private String formatScoreComponent(String componentName, int score) {
return "- **" + componentName + ":** " + score + "/5 (" +
scoreDescriptionProvider.getDescription(score) + ")\n";
}

/**
* Formats the "no projects found" message.
*
* @return formatted no projects message
*/
public String formatNoProjectsMessage() {
return "No projects found.\n";
}
}
Loading