Skip to content

Commit fa3a079

Browse files
authored
Merge pull request #4 from negative-games/snapshot
Add Nexus deploy workflow, Maven publishing, and utility classes
2 parents f96b74f + 388fc4e commit fa3a079

15 files changed

Lines changed: 435 additions & 152 deletions

File tree

.github/copilot-instructions.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<!-- desloppify-begin -->
2+
<!-- desloppify-skill-version: 2 -->
3+
---
4+
name: desloppify
5+
description: >
6+
Codebase health scanner and technical debt tracker. Use when the user asks
7+
about code quality, technical debt, dead code, large files, god classes,
8+
duplicate functions, code smells, naming issues, import cycles, or coupling
9+
problems. Also use when asked for a health score, what to fix next, or to
10+
create a cleanup plan. Supports 28 languages.
11+
allowed-tools: Bash(desloppify *)
12+
---
13+
14+
# Desloppify
15+
16+
## 1. Your Job
17+
18+
Improve code quality by maximising the **strict score** honestly.
19+
20+
**The main thing you do is run `desloppify next`** — it tells you exactly what to fix and how. Fix it, resolve it, run `next` again. Keep going.
21+
22+
Follow the scan output's **INSTRUCTIONS FOR AGENTS** — don't substitute your own analysis.
23+
24+
## 2. The Workflow
25+
26+
Two loops. The **outer loop** rescans periodically to measure progress.
27+
The **inner loop** is where you spend most of your time: fixing issues one by one.
28+
29+
### Outer loop — scan and check
30+
31+
```bash
32+
desloppify scan --path . # analyse the codebase
33+
desloppify status # check scores — are we at target?
34+
```
35+
If not at target, work the inner loop. Rescan periodically — especially after clearing a cluster or batch of related fixes. Issues cascade-resolve and new ones may surface.
36+
37+
### Inner loop — fix issues
38+
39+
Repeat until the queue is clear:
40+
41+
```
42+
1. desloppify next ← tells you exactly what to fix next
43+
2. Fix the issue in code
44+
3. Resolve it (next shows you the exact command including required attestation)
45+
```
46+
47+
Score may temporarily drop after fixes — cascade effects are normal, keep going.
48+
If `next` suggests an auto-fixer, run `desloppify fix <fixer> --dry-run` to preview, then apply.
49+
50+
**To be strategic**, use `plan` to shape what `next` gives you:
51+
```bash
52+
desloppify plan # see the full ordered queue
53+
desloppify plan move <pat> top # reorder — what unblocks the most?
54+
desloppify plan cluster create <name> # group related issues to batch-fix
55+
desloppify plan focus <cluster> # scope next to one cluster
56+
desloppify plan defer <pat> # push low-value items aside
57+
desloppify plan skip <pat> # hide from next
58+
desloppify plan done <pat> # mark complete
59+
desloppify plan reopen <pat> # reopen
60+
```
61+
62+
### Subjective reviews
63+
64+
The scan will prompt you when a subjective review is needed — just follow its instructions.
65+
If you need to trigger one manually:
66+
```bash
67+
desloppify review --run-batches --runner codex --parallel --scan-after-import
68+
```
69+
70+
### Other useful commands
71+
72+
```bash
73+
desloppify next --count 5 # top 5 priorities
74+
desloppify next --cluster <name> # drill into a cluster
75+
desloppify show <pattern> # filter by file/detector/ID
76+
desloppify show --status open # all open findings
77+
desloppify plan skip --permanent "<id>" --note "reason" # accept debt (lowers strict score)
78+
desloppify scan --path . --reset-subjective # reset subjective baseline to 0
79+
```
80+
81+
## 3. Reference
82+
83+
### How scoring works
84+
85+
Overall score = **40% mechanical** + **60% subjective**.
86+
87+
- **Mechanical (40%)**: auto-detected issues — duplication, dead code, smells, unused imports, security. Fixed by changing code and rescanning.
88+
- **Subjective (60%)**: design quality review — naming, error handling, abstractions, clarity. Starts at **0%** until reviewed. The scan will prompt you when a review is needed.
89+
- **Strict score** is the north star: wontfix items count as open. The gap between overall and strict is your wontfix debt.
90+
- **Score types**: overall (lenient), strict (wontfix counts), objective (mechanical only), verified (confirmed fixes only).
91+
92+
### Subjective reviews in detail
93+
94+
- **Preferred**: `desloppify review --run-batches --runner codex --parallel --scan-after-import` — does everything in one command.
95+
- **Manual path**: `desloppify review --prepare` → review per dimension → `desloppify review --import file.json`.
96+
- Import first, fix after — import creates tracked state entries for correlation.
97+
- Integrity: reviewers score from evidence only. Scores hitting exact targets trigger auto-reset.
98+
- Even moderate scores (60-80) dramatically improve overall health.
99+
- Stale dimensions auto-surface in `next` — just follow the queue.
100+
101+
### Key concepts
102+
103+
- **Tiers**: T1 auto-fix → T2 quick manual → T3 judgment call → T4 major refactor.
104+
- **Auto-clusters**: related findings are auto-grouped in `next`. Drill in with `next --cluster <name>`.
105+
- **Zones**: production/script (scored), test/config/generated/vendor (not scored). Fix with `zone set`.
106+
- **Wontfix cost**: widens the lenient↔strict gap. Challenge past decisions when the gap grows.
107+
- Score can temporarily drop after fixes (cascade effects are normal).
108+
109+
## 4. Escalate Tool Issues Upstream
110+
111+
When desloppify itself appears wrong or inconsistent:
112+
113+
1. Capture a minimal repro (`command`, `path`, `expected`, `actual`).
114+
2. Open a GitHub issue in `peteromallet/desloppify`.
115+
3. If you can fix it safely, open a PR linked to that issue.
116+
4. If unsure whether it is tool bug vs user workflow, issue first, PR second.
117+
118+
## Prerequisite
119+
120+
`command -v desloppify >/dev/null 2>&1 && echo "desloppify: installed" || echo "NOT INSTALLED — run: pip install --upgrade git+https://github.com/peteromallet/desloppify.git"`
121+
122+
<!-- desloppify-end -->
123+
124+
## VS Code Copilot Overlay
125+
126+
VS Code Copilot supports native subagents via `.github/agents/` definitions.
127+
Use them for context-isolated subjective reviews.
128+
129+
### Subjective review
130+
131+
1. **Preferred**: `desloppify review --run-batches --runner codex --parallel --scan-after-import`.
132+
2. **Copilot/cloud path**: `desloppify review --external-start --external-runner claude` → use generated prompt/template → run printed `--external-submit` command.
133+
3. **Manual path**: define a reviewer agent, split dimensions, merge, import.
134+
135+
For the manual path, define a reviewer in `.github/agents/desloppify-reviewer.md`:
136+
137+
```yaml
138+
---
139+
name: desloppify-reviewer
140+
tools: ['read', 'search']
141+
---
142+
You are a code quality reviewer. You will be given a codebase path, a set of
143+
dimensions to score, and what each dimension means. Read the code, score each
144+
dimension 0-100 from evidence only, and return JSON in the required format.
145+
Do not anchor to target thresholds. When evidence is mixed, score lower and
146+
explain uncertainty.
147+
```
148+
149+
And an orchestrator in `.github/agents/desloppify-review-orchestrator.md`:
150+
151+
```yaml
152+
---
153+
name: desloppify-review-orchestrator
154+
tools: ['agent', 'read', 'search']
155+
agents: ['desloppify-reviewer']
156+
---
157+
```
158+
159+
Split dimensions across `desloppify-reviewer` calls (Copilot runs them concurrently), merge assessments (average overlaps) and findings, then import.
160+
161+
### Review integrity
162+
163+
1. Do not use prior chat context, score history, or target-threshold anchoring while scoring.
164+
2. Score from evidence only; when mixed, score lower and explain uncertainty.
165+
3. Return JSON matching the format in the base skill doc. For `--external-submit`, include `session` from the generated template.
166+
4. `findings` MUST match `query.system_prompt` exactly. Use `"findings": []` when no defects found.
167+
5. Import is fail-closed: invalid findings abort unless `--allow-partial` is passed.
168+
169+
<!-- desloppify-overlay: copilot -->
170+
<!-- desloppify-end -->

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ bin/
4141
### Mac OS ###
4242
.DS_Store
4343
/.idea/
44+
/.desloppify

common/build.gradle.kts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ dependencies {
3939
// Lombok
4040
compileOnly("org.projectlombok:lombok:1.18.32")
4141
annotationProcessor("org.projectlombok:lombok:1.18.32")
42+
43+
// Testing
44+
testImplementation(platform("org.junit:junit-bom:5.12.2"))
45+
testImplementation("org.junit.jupiter:junit-jupiter")
46+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
4247
}
4348

4449
java {
@@ -53,6 +58,10 @@ tasks.shadowJar {
5358
archiveClassifier.set("")
5459
}
5560

61+
tasks.test {
62+
useJUnitPlatform()
63+
}
64+
5665
publishing {
5766
publications {
5867
create<MavenPublication>("mavenJava") {
@@ -85,4 +94,4 @@ publishing {
8594
}
8695
}
8796
}
88-
}
97+
}

common/src/main/java/games/negative/engine/util/NumberUtil.java

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -391,18 +391,19 @@ public static String condense(BigDecimal number) {
391391
*/
392392
public static String condense(BigDecimal number, final char[] set) {
393393
BigDecimal thousand = BigDecimal.valueOf(1000);
394+
String condensed;
394395
if (number.compareTo(thousand) < 0) {
395-
return number.stripTrailingZeros().toPlainString(); // Return the number itself if less than 1000.
396-
}
397-
398-
int exp = (int) (Math.floor(Math.log10(number.doubleValue()) / 3));
399-
400-
String suffixes = (set == null) ? SUFFIXES : new String(set);
401-
char suffix = suffixes.charAt(Math.min(exp - 1, suffixes.length() - 1));
396+
condensed = number.stripTrailingZeros().toPlainString(); // Return the number itself if less than 1000.
397+
} else {
398+
int exp = (int) (Math.floor(Math.log10(number.doubleValue()) / 3));
402399

403-
BigDecimal result = number.divide(thousand.pow(exp), 1, RoundingMode.HALF_UP);
400+
String suffixes = (set == null) ? SUFFIXES : new String(set);
401+
char suffix = suffixes.charAt(Math.min(exp - 1, suffixes.length() - 1));
404402

405-
return String.format("%.1f%c", result, suffix);
403+
BigDecimal result = number.divide(thousand.pow(exp), 1, RoundingMode.HALF_UP);
404+
condensed = String.format("%.1f%c", result, suffix);
405+
}
406+
return condensed;
406407
}
407408

408409
/**

common/src/main/java/games/negative/engine/util/TimeUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ private static Duration parseUnitFormat(String input) {
256256
if (Character.isDigit(c)) {
257257
numberBuffer.append(c);
258258
} else if (Character.isWhitespace(c)) {
259-
continue; // Allow whitespace
259+
// Allow whitespace.
260260
} else {
261261
if (numberBuffer.isEmpty()) {
262262
throw new IllegalArgumentException(
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package games.negative.engine.util;
2+
3+
import games.negative.engine.util.IntList;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.util.List;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
10+
class IntListTest {
11+
12+
@Test
13+
void parseSupportsSinglesAndRanges() {
14+
List<Integer> values = IntList.parse(List.of("1", "3-5", " 7 "));
15+
16+
assertEquals(List.of(1, 3, 4, 5, 7), values);
17+
}
18+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package games.negative.engine.util;
2+
3+
import games.negative.engine.util.NumberUtil;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.math.BigDecimal;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
10+
class NumberUtilTest {
11+
12+
@Test
13+
void fancyAddsExpectedSuffixes() {
14+
assertEquals("1st", NumberUtil.fancy(1));
15+
assertEquals("12th", NumberUtil.fancy(12));
16+
assertEquals("23rd", NumberUtil.fancy(23));
17+
}
18+
19+
@Test
20+
void condenseFormatsValues() {
21+
assertEquals("1.5k", NumberUtil.condense(1500));
22+
assertEquals("999", NumberUtil.condense(new BigDecimal("999")));
23+
}
24+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package games.negative.engine.util;
2+
3+
import games.negative.engine.util.OptionalBool;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.util.Optional;
7+
import java.util.concurrent.atomic.AtomicInteger;
8+
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
import static org.junit.jupiter.api.Assertions.assertFalse;
11+
import static org.junit.jupiter.api.Assertions.assertSame;
12+
import static org.junit.jupiter.api.Assertions.assertTrue;
13+
14+
class OptionalBoolTest {
15+
16+
@Test
17+
void ofReturnsSingletonInstances() {
18+
assertSame(OptionalBool.of(true), OptionalBool.of(true));
19+
assertSame(OptionalBool.of(false), OptionalBool.of(false));
20+
}
21+
22+
@Test
23+
void mapIfTrueAndFalseBehaveAsExpected() {
24+
Optional<String> whenTrue = OptionalBool.of(true).mapIfTrue(() -> "value");
25+
Optional<String> whenFalse = OptionalBool.of(false).mapIfTrue(() -> "value");
26+
27+
assertEquals(Optional.of("value"), whenTrue);
28+
assertTrue(whenFalse.isEmpty());
29+
}
30+
31+
@Test
32+
void ifTrueOrElseRunsCorrectBranch() {
33+
AtomicInteger marker = new AtomicInteger(0);
34+
OptionalBool.of(false).ifTrueOrElse(
35+
() -> marker.set(1),
36+
() -> marker.set(2)
37+
);
38+
39+
assertFalse(OptionalBool.of(false).isTrue());
40+
assertEquals(2, marker.get());
41+
}
42+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package games.negative.engine.util;
2+
3+
import games.negative.engine.util.TimeUtil;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.time.Duration;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
10+
class TimeUtilTest {
11+
12+
@Test
13+
void formatAndParseRoundTripUnitFormat() {
14+
Duration duration = TimeUtil.parse("1h30m");
15+
assertEquals(Duration.ofMinutes(90), duration);
16+
assertEquals("1h 30m", TimeUtil.format(duration, true));
17+
}
18+
19+
@Test
20+
void formatAndParseRoundTripColonFormat() {
21+
Duration duration = Duration.ofHours(1).plusMinutes(5).plusSeconds(2);
22+
String formatted = TimeUtil.formatColonSeparated(duration);
23+
assertEquals("1:05:02", formatted);
24+
assertEquals(duration, TimeUtil.parseColonSeparated(formatted));
25+
}
26+
}

0 commit comments

Comments
 (0)