csharp-code-calisthenics-reviewer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/bin/installer.js +328 -0
- package/package.json +25 -0
- package/scripts/build.js +110 -0
- package/src/codex-plugin/.codex-plugin/plugin.json +6 -0
- package/src/codex-plugin/skills/csharp-code-calisthenics-reviewer/SKILL.md +63 -0
- package/src/codex-plugin/skills/csharp-code-calisthenics-reviewer/examples/input/OrderService.cs +51 -0
- package/src/codex-plugin/skills/csharp-code-calisthenics-reviewer/examples/output/review.md +43 -0
- package/src/codex-plugin/skills/csharp-code-calisthenics-reviewer/references/calisthenics-checklist.md +17 -0
- package/src/raw-skill/SKILL.md +63 -0
- package/src/raw-skill/examples/input/OrderService.cs +51 -0
- package/src/raw-skill/examples/output/review.md +43 -0
- package/src/raw-skill/references/calisthenics-checklist.md +17 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "local-marketplace",
|
|
3
|
+
"interface": {
|
|
4
|
+
"displayName": "Local Plugins"
|
|
5
|
+
},
|
|
6
|
+
"plugins": [
|
|
7
|
+
{
|
|
8
|
+
"name": "csharp-code-calisthenics-reviewer",
|
|
9
|
+
"source": {
|
|
10
|
+
"source": "local",
|
|
11
|
+
"path": "./plugins/csharp-code-calisthenics-reviewer"
|
|
12
|
+
},
|
|
13
|
+
"policy": {
|
|
14
|
+
"installation": "AVAILABLE",
|
|
15
|
+
"authentication": "ON_INSTALL"
|
|
16
|
+
},
|
|
17
|
+
"category": "Productivity"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leonardo de Oliveira
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# csharp-code-calisthenics-reviewer
|
|
2
|
+
|
|
3
|
+
`csharp-code-calisthenics-reviewer` is an npm-distributed skill package for reviewing a single C# file or small class against a narrow set of code-calisthenics rules and suggesting a deterministic refactor plan.
|
|
4
|
+
|
|
5
|
+
## What v1 reviews
|
|
6
|
+
|
|
7
|
+
The skill checks exactly these 5 rules:
|
|
8
|
+
|
|
9
|
+
- Only one level of indentation per method
|
|
10
|
+
- Avoid `else`
|
|
11
|
+
- Wrap primitives in value objects when it makes sense
|
|
12
|
+
- First-class collections
|
|
13
|
+
- Small methods with intention-revealing names
|
|
14
|
+
|
|
15
|
+
The skill is instruction-heavy by design. It does not perform full automatic rewrites by default, but it may include short illustrative snippets when they clarify a proposed refactor.
|
|
16
|
+
|
|
17
|
+
## Repo layout
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
src/
|
|
21
|
+
raw-skill/
|
|
22
|
+
SKILL.md
|
|
23
|
+
references/calisthenics-checklist.md
|
|
24
|
+
examples/input/OrderService.cs
|
|
25
|
+
examples/output/review.md
|
|
26
|
+
codex-plugin/
|
|
27
|
+
.codex-plugin/plugin.json
|
|
28
|
+
skills/csharp-code-calisthenics-reviewer/
|
|
29
|
+
bin/
|
|
30
|
+
installer.js
|
|
31
|
+
scripts/
|
|
32
|
+
build.js
|
|
33
|
+
.agents/
|
|
34
|
+
plugins/marketplace.json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`src/raw-skill` is the canonical source. `npm run build` mirrors it into the Codex plugin skill folder and synchronizes the plugin manifest version from `package.json`.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
### npm / npx
|
|
42
|
+
|
|
43
|
+
Build first:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install
|
|
47
|
+
npm run build
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Run from the published package or locally:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx csharp-code-calisthenics-reviewer install --target codex --scope personal
|
|
54
|
+
npx csharp-code-calisthenics-reviewer install --target codex --scope repo --path /path/to/repo
|
|
55
|
+
npx csharp-code-calisthenics-reviewer install --target dir --path /path/to/destination
|
|
56
|
+
npx csharp-code-calisthenics-reviewer install --target claude --path /path/to/destination
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Optional flags:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
--dry-run
|
|
63
|
+
--force
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Skillfish
|
|
67
|
+
|
|
68
|
+
Use the raw skill bundle:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx csharp-code-calisthenics-reviewer install --target dir --path /path/to/skills/csharp-code-calisthenics-reviewer
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Codex local plugin install
|
|
75
|
+
|
|
76
|
+
Personal install:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npx csharp-code-calisthenics-reviewer install --target codex --scope personal
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This installs the plugin to `~/.codex/plugins/csharp-code-calisthenics-reviewer` and creates or updates `~/.agents/plugins/marketplace.json`.
|
|
83
|
+
|
|
84
|
+
Repo install:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx csharp-code-calisthenics-reviewer install --target codex --scope repo --path /path/to/repo
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This installs the plugin to `<repo>/plugins/csharp-code-calisthenics-reviewer` and creates or updates `<repo>/.agents/plugins/marketplace.json`.
|
|
91
|
+
|
|
92
|
+
### Claude-compatible path copy
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx csharp-code-calisthenics-reviewer install --target claude --path /path/to/destination
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This performs an explicit path-based copy only. v1 does not assume or document an official Claude install location.
|
|
99
|
+
|
|
100
|
+
## Build and verify
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npm run build
|
|
104
|
+
python C:/Users/USUARIO/.codex/skills/.system/skill-creator/scripts/quick_validate.py src/raw-skill
|
|
105
|
+
npm pack
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
On Windows PowerShell, if `npm pack` is blocked by `npm.ps1` execution policy or by the global npm cache, use:
|
|
109
|
+
|
|
110
|
+
```powershell
|
|
111
|
+
npm run pack:ps
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Marketplace metadata
|
|
115
|
+
|
|
116
|
+
The Codex plugin manifest lives at `src/codex-plugin/.codex-plugin/plugin.json`.
|
|
117
|
+
|
|
118
|
+
The repository also includes `.agents/plugins/marketplace.json` as a repo-local marketplace catalog entry that points to `./plugins/csharp-code-calisthenics-reviewer`. Installer runs generate or update marketplace entries at the chosen destination without duplicating existing entries.
|
package/bin/installer.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const PLUGIN_NAME = "csharp-code-calisthenics-reviewer";
|
|
8
|
+
const DEFAULT_ENTRY = {
|
|
9
|
+
name: PLUGIN_NAME,
|
|
10
|
+
source: {
|
|
11
|
+
source: "local",
|
|
12
|
+
path: `./plugins/${PLUGIN_NAME}`
|
|
13
|
+
},
|
|
14
|
+
policy: {
|
|
15
|
+
installation: "AVAILABLE",
|
|
16
|
+
authentication: "ON_INSTALL"
|
|
17
|
+
},
|
|
18
|
+
category: "Productivity"
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function fail(message) {
|
|
22
|
+
console.error(message);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
const parsed = {
|
|
28
|
+
command: null,
|
|
29
|
+
target: null,
|
|
30
|
+
scope: null,
|
|
31
|
+
path: null,
|
|
32
|
+
force: false,
|
|
33
|
+
dryRun: false
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const tokens = [...argv];
|
|
37
|
+
if (tokens.length > 0 && !tokens[0].startsWith("--")) {
|
|
38
|
+
parsed.command = tokens.shift();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
42
|
+
const token = tokens[index];
|
|
43
|
+
switch (token) {
|
|
44
|
+
case "--target":
|
|
45
|
+
parsed.target = tokens[++index];
|
|
46
|
+
break;
|
|
47
|
+
case "--scope":
|
|
48
|
+
parsed.scope = tokens[++index];
|
|
49
|
+
break;
|
|
50
|
+
case "--path":
|
|
51
|
+
parsed.path = tokens[++index];
|
|
52
|
+
break;
|
|
53
|
+
case "--force":
|
|
54
|
+
parsed.force = true;
|
|
55
|
+
break;
|
|
56
|
+
case "--dry-run":
|
|
57
|
+
parsed.dryRun = true;
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
fail(`Unknown argument: ${token}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolvePackageRoot() {
|
|
68
|
+
return path.resolve(__dirname, "..");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ensureExists(targetPath, label) {
|
|
72
|
+
if (!fs.existsSync(targetPath)) {
|
|
73
|
+
fail(`${label} not found: ${targetPath}. Run "npm run build" before installing.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readJson(filePath, fallback) {
|
|
78
|
+
if (!fs.existsSync(filePath)) {
|
|
79
|
+
return fallback;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function writeJson(filePath, value, dryRun) {
|
|
86
|
+
const payload = `${JSON.stringify(value, null, 2)}\n`;
|
|
87
|
+
if (dryRun) {
|
|
88
|
+
console.log(`[dry-run] write ${filePath}`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
93
|
+
fs.writeFileSync(filePath, payload, "utf8");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function removeDir(targetPath, dryRun) {
|
|
97
|
+
if (!fs.existsSync(targetPath)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (dryRun) {
|
|
102
|
+
console.log(`[dry-run] remove ${targetPath}`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function listFiles(rootPath) {
|
|
110
|
+
const results = [];
|
|
111
|
+
|
|
112
|
+
function visit(currentPath) {
|
|
113
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const absolutePath = path.join(currentPath, entry.name);
|
|
116
|
+
if (entry.isDirectory()) {
|
|
117
|
+
visit(absolutePath);
|
|
118
|
+
} else {
|
|
119
|
+
results.push(path.relative(rootPath, absolutePath));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
visit(rootPath);
|
|
125
|
+
return results.sort();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function directoriesMatch(sourcePath, destinationPath) {
|
|
129
|
+
if (!fs.existsSync(sourcePath) || !fs.existsSync(destinationPath)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const sourceFiles = listFiles(sourcePath);
|
|
134
|
+
const destinationFiles = listFiles(destinationPath);
|
|
135
|
+
if (sourceFiles.length !== destinationFiles.length) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (let index = 0; index < sourceFiles.length; index += 1) {
|
|
140
|
+
if (sourceFiles[index] !== destinationFiles[index]) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const sourceContent = fs.readFileSync(path.join(sourcePath, sourceFiles[index]), "utf8");
|
|
145
|
+
const destinationContent = fs.readFileSync(path.join(destinationPath, destinationFiles[index]), "utf8");
|
|
146
|
+
if (sourceContent !== destinationContent) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function copyDir(sourcePath, destinationPath, options) {
|
|
155
|
+
const { dryRun } = options;
|
|
156
|
+
if (dryRun) {
|
|
157
|
+
console.log(`[dry-run] copy ${sourcePath} -> ${destinationPath}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fs.mkdirSync(destinationPath, { recursive: true });
|
|
162
|
+
fs.cpSync(sourcePath, destinationPath, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function copyIntoDestination(sourcePath, destinationPath, options) {
|
|
166
|
+
const { force, dryRun } = options;
|
|
167
|
+
const destinationExists = fs.existsSync(destinationPath);
|
|
168
|
+
|
|
169
|
+
if (destinationExists && !force) {
|
|
170
|
+
if (directoriesMatch(sourcePath, destinationPath)) {
|
|
171
|
+
console.log(`Destination already matches source: ${destinationPath}`);
|
|
172
|
+
return "unchanged";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fail(`Destination already exists and differs: ${destinationPath}. Re-run with --force to overwrite.`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (destinationExists) {
|
|
179
|
+
removeDir(destinationPath, dryRun);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
copyDir(sourcePath, destinationPath, options);
|
|
183
|
+
return dryRun ? "dry-run" : "copied";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function ensureMarketplaceShape(marketplace) {
|
|
187
|
+
const next = marketplace ?? {};
|
|
188
|
+
if (typeof next.name !== "string") {
|
|
189
|
+
next.name = "local-marketplace";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!next.interface || typeof next.interface !== "object") {
|
|
193
|
+
next.interface = { displayName: "Local Plugins" };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (typeof next.interface.displayName !== "string") {
|
|
197
|
+
next.interface.displayName = "Local Plugins";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!Array.isArray(next.plugins)) {
|
|
201
|
+
next.plugins = [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return next;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function updateMarketplace(marketplacePath, entry, dryRun) {
|
|
208
|
+
const marketplace = ensureMarketplaceShape(readJson(marketplacePath, null));
|
|
209
|
+
const existingIndex = marketplace.plugins.findIndex((plugin) => plugin.name === entry.name);
|
|
210
|
+
|
|
211
|
+
if (existingIndex >= 0) {
|
|
212
|
+
marketplace.plugins[existingIndex] = entry;
|
|
213
|
+
} else {
|
|
214
|
+
marketplace.plugins.push(entry);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
writeJson(marketplacePath, marketplace, dryRun);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function installCodex(scope, repoPath, options) {
|
|
221
|
+
const packageRoot = resolvePackageRoot();
|
|
222
|
+
const pluginSource = path.join(packageRoot, "src", "codex-plugin");
|
|
223
|
+
ensureExists(pluginSource, "Built Codex plugin");
|
|
224
|
+
|
|
225
|
+
let pluginDestination;
|
|
226
|
+
let marketplacePath;
|
|
227
|
+
let entry;
|
|
228
|
+
|
|
229
|
+
if (scope === "personal") {
|
|
230
|
+
const homeDir = os.homedir();
|
|
231
|
+
pluginDestination = path.join(homeDir, ".codex", "plugins", PLUGIN_NAME);
|
|
232
|
+
marketplacePath = path.join(homeDir, ".agents", "plugins", "marketplace.json");
|
|
233
|
+
entry = {
|
|
234
|
+
...DEFAULT_ENTRY,
|
|
235
|
+
source: {
|
|
236
|
+
source: "local",
|
|
237
|
+
path: pluginDestination
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
} else if (scope === "repo") {
|
|
241
|
+
if (!repoPath) {
|
|
242
|
+
fail('Missing required "--path" for repo-scoped Codex install.');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const repoRoot = path.resolve(repoPath);
|
|
246
|
+
pluginDestination = path.join(repoRoot, "plugins", PLUGIN_NAME);
|
|
247
|
+
marketplacePath = path.join(repoRoot, ".agents", "plugins", "marketplace.json");
|
|
248
|
+
entry = { ...DEFAULT_ENTRY };
|
|
249
|
+
} else {
|
|
250
|
+
fail('Codex installs require "--scope personal" or "--scope repo".');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const status = copyIntoDestination(pluginSource, pluginDestination, options);
|
|
254
|
+
updateMarketplace(marketplacePath, entry, options.dryRun);
|
|
255
|
+
|
|
256
|
+
if (status === "dry-run") {
|
|
257
|
+
console.log(`Dry-run completed for Codex plugin destination ${pluginDestination}`);
|
|
258
|
+
} else if (status === "unchanged") {
|
|
259
|
+
console.log(`Codex plugin already up to date at ${pluginDestination}`);
|
|
260
|
+
} else {
|
|
261
|
+
console.log(`Installed Codex plugin to ${pluginDestination}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log(`${options.dryRun ? "Dry-run updated" : "Updated"} marketplace at ${marketplacePath}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function installRawSkill(targetPath, options, label) {
|
|
268
|
+
const packageRoot = resolvePackageRoot();
|
|
269
|
+
const rawSkillSource = path.join(packageRoot, "src", "raw-skill");
|
|
270
|
+
ensureExists(rawSkillSource, "Raw skill bundle");
|
|
271
|
+
|
|
272
|
+
if (!targetPath) {
|
|
273
|
+
fail('Missing required "--path" for this install target.');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const destination = path.resolve(targetPath);
|
|
277
|
+
const status = copyIntoDestination(rawSkillSource, destination, options);
|
|
278
|
+
if (status === "dry-run") {
|
|
279
|
+
console.log(`Dry-run completed for ${label} destination ${destination}`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (status === "unchanged") {
|
|
284
|
+
console.log(`${label} bundle already up to date at ${destination}`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
console.log(`Installed ${label} bundle to ${destination}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function printUsageAndExit() {
|
|
292
|
+
const lines = [
|
|
293
|
+
"Usage:",
|
|
294
|
+
" csharp-code-calisthenics-reviewer install --target codex --scope personal [--force] [--dry-run]",
|
|
295
|
+
" csharp-code-calisthenics-reviewer install --target codex --scope repo --path <repo-root> [--force] [--dry-run]",
|
|
296
|
+
" csharp-code-calisthenics-reviewer install --target dir --path <destination> [--force] [--dry-run]",
|
|
297
|
+
" csharp-code-calisthenics-reviewer install --target claude --path <destination> [--force] [--dry-run]"
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
console.log(lines.join("\n"));
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function main() {
|
|
305
|
+
const args = parseArgs(process.argv.slice(2));
|
|
306
|
+
|
|
307
|
+
if (args.command !== "install" || !args.target) {
|
|
308
|
+
printUsageAndExit();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const options = { force: args.force, dryRun: args.dryRun };
|
|
312
|
+
|
|
313
|
+
switch (args.target) {
|
|
314
|
+
case "codex":
|
|
315
|
+
installCodex(args.scope, args.path, options);
|
|
316
|
+
break;
|
|
317
|
+
case "dir":
|
|
318
|
+
installRawSkill(args.path, options, "raw skill");
|
|
319
|
+
break;
|
|
320
|
+
case "claude":
|
|
321
|
+
installRawSkill(args.path, options, "Claude-compatible");
|
|
322
|
+
break;
|
|
323
|
+
default:
|
|
324
|
+
fail(`Unsupported target: ${args.target}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "csharp-code-calisthenics-reviewer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Deterministic C# code calisthenics review skill packaged as a raw skill bundle and Codex plugin.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"csharp-code-calisthenics-reviewer": "./bin/installer.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "node scripts/build.js",
|
|
11
|
+
"pack:ps": "powershell -NoProfile -ExecutionPolicy Bypass -Command \"$env:npm_config_cache=(Get-Location).Path + '\\.tmp\\npm-cache'; npm.cmd pack\""
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src/raw-skill/**",
|
|
15
|
+
"src/codex-plugin/**",
|
|
16
|
+
"scripts/build.js",
|
|
17
|
+
"bin/installer.js",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
".agents/plugins/marketplace.json"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/scripts/build.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const PLUGIN_NAME = "csharp-code-calisthenics-reviewer";
|
|
5
|
+
const REQUIRED_SKILL_FILES = [
|
|
6
|
+
"SKILL.md",
|
|
7
|
+
path.join("references", "calisthenics-checklist.md"),
|
|
8
|
+
path.join("examples", "input", "OrderService.cs"),
|
|
9
|
+
path.join("examples", "output", "review.md")
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function fail(message) {
|
|
13
|
+
throw new Error(message);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readJson(filePath) {
|
|
17
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeJson(filePath, value) {
|
|
21
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
22
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function listFiles(rootPath) {
|
|
26
|
+
const results = [];
|
|
27
|
+
|
|
28
|
+
function visit(currentPath) {
|
|
29
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const absolutePath = path.join(currentPath, entry.name);
|
|
32
|
+
const relativePath = path.relative(rootPath, absolutePath);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
visit(absolutePath);
|
|
35
|
+
} else {
|
|
36
|
+
results.push(relativePath);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
visit(rootPath);
|
|
42
|
+
return results.sort();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function assertRequiredFiles(rootPath) {
|
|
46
|
+
for (const relativePath of REQUIRED_SKILL_FILES) {
|
|
47
|
+
const absolutePath = path.join(rootPath, relativePath);
|
|
48
|
+
if (!fs.existsSync(absolutePath)) {
|
|
49
|
+
fail(`Required skill file is missing: ${absolutePath}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function copyDir(sourcePath, destinationPath) {
|
|
55
|
+
fs.rmSync(destinationPath, { recursive: true, force: true });
|
|
56
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
57
|
+
fs.cpSync(sourcePath, destinationPath, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function assertParity(sourcePath, destinationPath) {
|
|
61
|
+
const sourceFiles = listFiles(sourcePath);
|
|
62
|
+
const destinationFiles = listFiles(destinationPath);
|
|
63
|
+
const sourceJoined = sourceFiles.join("\n");
|
|
64
|
+
const destinationJoined = destinationFiles.join("\n");
|
|
65
|
+
|
|
66
|
+
if (sourceJoined !== destinationJoined) {
|
|
67
|
+
fail("Raw skill and plugin skill file lists differ after build.");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const relativePath of sourceFiles) {
|
|
71
|
+
const sourceContent = fs.readFileSync(path.join(sourcePath, relativePath), "utf8");
|
|
72
|
+
const destinationContent = fs.readFileSync(path.join(destinationPath, relativePath), "utf8");
|
|
73
|
+
if (sourceContent !== destinationContent) {
|
|
74
|
+
fail(`Skill parity mismatch after build: ${relativePath}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function syncPluginManifest(packageRoot) {
|
|
80
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
81
|
+
const pluginJsonPath = path.join(packageRoot, "src", "codex-plugin", ".codex-plugin", "plugin.json");
|
|
82
|
+
const packageJson = readJson(packageJsonPath);
|
|
83
|
+
const pluginJson = readJson(pluginJsonPath);
|
|
84
|
+
|
|
85
|
+
pluginJson.name = PLUGIN_NAME;
|
|
86
|
+
pluginJson.version = packageJson.version;
|
|
87
|
+
pluginJson.skills = "./skills/";
|
|
88
|
+
|
|
89
|
+
writeJson(pluginJsonPath, pluginJson);
|
|
90
|
+
|
|
91
|
+
const reloaded = readJson(pluginJsonPath);
|
|
92
|
+
if (reloaded.version !== packageJson.version) {
|
|
93
|
+
fail("Plugin manifest version did not match package.json after synchronization.");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function main() {
|
|
98
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
99
|
+
const rawSkillRoot = path.join(packageRoot, "src", "raw-skill");
|
|
100
|
+
const pluginSkillRoot = path.join(packageRoot, "src", "codex-plugin", "skills", PLUGIN_NAME);
|
|
101
|
+
|
|
102
|
+
assertRequiredFiles(rawSkillRoot);
|
|
103
|
+
copyDir(rawSkillRoot, pluginSkillRoot);
|
|
104
|
+
assertParity(rawSkillRoot, pluginSkillRoot);
|
|
105
|
+
syncPluginManifest(packageRoot);
|
|
106
|
+
|
|
107
|
+
console.log("Build completed successfully.");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: csharp-code-calisthenics-reviewer
|
|
3
|
+
description: Review a single C# file or small C# class against a fixed, narrow set of code-calisthenics rules and produce a deterministic refactor plan. Use when asked to critique or review C# code for indentation depth, else usage, primitive obsession, missing first-class collections, and overly large or poorly named methods.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# C# Code Calisthenics Reviewer
|
|
7
|
+
|
|
8
|
+
Review one C# file or one small class at a time. Keep the review narrow, deterministic, and grounded in the provided code.
|
|
9
|
+
|
|
10
|
+
## Rules
|
|
11
|
+
|
|
12
|
+
Check only these 5 rules:
|
|
13
|
+
|
|
14
|
+
1. Only one level of indentation per method
|
|
15
|
+
2. Avoid `else`
|
|
16
|
+
3. Wrap primitives in value objects when it makes sense
|
|
17
|
+
4. First-class collections
|
|
18
|
+
5. Small methods with intention-revealing names
|
|
19
|
+
|
|
20
|
+
Do not expand into the broader code-calisthenics catalog unless the user explicitly asks for that.
|
|
21
|
+
|
|
22
|
+
## Working style
|
|
23
|
+
|
|
24
|
+
- Read the code carefully before judging it.
|
|
25
|
+
- Stay concrete and repeatable. Do not use random scoring, vague style commentary, or speculative claims.
|
|
26
|
+
- Base every finding on visible code in the provided file or class.
|
|
27
|
+
- Mention guard-clause opportunities when they help flatten control flow.
|
|
28
|
+
- Do not perform a full automatic rewrite by default.
|
|
29
|
+
- Include a short illustrative snippet only when it makes a recommendation easier to understand.
|
|
30
|
+
|
|
31
|
+
## Review process
|
|
32
|
+
|
|
33
|
+
1. Identify the class or methods under review.
|
|
34
|
+
2. Evaluate each of the 5 rules in order.
|
|
35
|
+
3. Record only the findings that are supported by the code.
|
|
36
|
+
4. Prioritize issues that reduce nesting, split responsibilities, and clarify domain language.
|
|
37
|
+
5. End with a refactor plan ordered by highest leverage first.
|
|
38
|
+
|
|
39
|
+
## Output format
|
|
40
|
+
|
|
41
|
+
Use this structure:
|
|
42
|
+
|
|
43
|
+
### Findings
|
|
44
|
+
|
|
45
|
+
For each rule:
|
|
46
|
+
|
|
47
|
+
- `Rule:` the exact rule name
|
|
48
|
+
- `Assessment:` `Pass`, `Concern`, or `Not applicable`
|
|
49
|
+
- `Evidence:` a brief code-specific explanation
|
|
50
|
+
- `Suggestion:` one concrete next step
|
|
51
|
+
|
|
52
|
+
### Refactor plan
|
|
53
|
+
|
|
54
|
+
Provide 3 to 6 ordered steps. Each step should describe a refactor action, not just a diagnosis.
|
|
55
|
+
|
|
56
|
+
### Optional snippet
|
|
57
|
+
|
|
58
|
+
Only include this section when a very short snippet clarifies one suggested refactor. Keep it focused on one change, not a full rewritten file.
|
|
59
|
+
|
|
60
|
+
## References
|
|
61
|
+
|
|
62
|
+
- Read `references/calisthenics-checklist.md` for the review checklist and refactor priorities.
|
|
63
|
+
- Use `examples/input/OrderService.cs` and `examples/output/review.md` as the canonical example of expected scope and output shape.
|
package/src/codex-plugin/skills/csharp-code-calisthenics-reviewer/examples/input/OrderService.cs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
|
|
5
|
+
namespace Example;
|
|
6
|
+
|
|
7
|
+
public class OrderService
|
|
8
|
+
{
|
|
9
|
+
public void Process(List<Order> orders, decimal discountRate, string customerType)
|
|
10
|
+
{
|
|
11
|
+
if (orders == null)
|
|
12
|
+
{
|
|
13
|
+
throw new ArgumentNullException(nameof(orders));
|
|
14
|
+
}
|
|
15
|
+
else
|
|
16
|
+
{
|
|
17
|
+
foreach (var order in orders)
|
|
18
|
+
{
|
|
19
|
+
if (order.Total > 100)
|
|
20
|
+
{
|
|
21
|
+
if (customerType == "VIP")
|
|
22
|
+
{
|
|
23
|
+
order.Total = order.Total - (order.Total * discountRate);
|
|
24
|
+
}
|
|
25
|
+
else
|
|
26
|
+
{
|
|
27
|
+
order.Total = order.Total - 5;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Save(orders);
|
|
33
|
+
Notify(customerType, orders.Count);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private void Save(List<Order> orders)
|
|
38
|
+
{
|
|
39
|
+
Console.WriteLine($"Saved {orders.Count} orders.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private void Notify(string customerType, int orderCount)
|
|
43
|
+
{
|
|
44
|
+
Console.WriteLine($"Notified {customerType} customer about {orderCount} orders.");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public class Order
|
|
49
|
+
{
|
|
50
|
+
public decimal Total { get; set; }
|
|
51
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Findings
|
|
2
|
+
|
|
3
|
+
- Rule: Only one level of indentation per method
|
|
4
|
+
Assessment: Concern
|
|
5
|
+
Evidence: `Process` nests a null check, a `foreach`, an order-total condition, and a customer-type condition. The deepest branch goes well beyond one indentation level.
|
|
6
|
+
Suggestion: Use a guard clause for `orders == null`, then extract the discount decision into a separate method or policy object.
|
|
7
|
+
|
|
8
|
+
- Rule: Avoid `else`
|
|
9
|
+
Assessment: Concern
|
|
10
|
+
Evidence: `Process` contains an `else` after the null check and another `else` inside the discount logic.
|
|
11
|
+
Suggestion: Replace both branches with guard clauses or early returns so the happy path stays flat.
|
|
12
|
+
|
|
13
|
+
- Rule: Wrap primitives in value objects when it makes sense
|
|
14
|
+
Assessment: Concern
|
|
15
|
+
Evidence: `discountRate` and `customerType` carry domain meaning but are represented as raw `decimal` and `string`.
|
|
16
|
+
Suggestion: Introduce value objects such as `DiscountRate` and `CustomerType` to make valid states explicit.
|
|
17
|
+
|
|
18
|
+
- Rule: First-class collections
|
|
19
|
+
Assessment: Concern
|
|
20
|
+
Evidence: `List<Order>` is passed through `Process` and `Save` as a raw collection with behavior spread across the service.
|
|
21
|
+
Suggestion: Introduce an `OrderBatch` or similar collection abstraction to hold iteration and batch-level behavior.
|
|
22
|
+
|
|
23
|
+
- Rule: Small methods with intention-revealing names
|
|
24
|
+
Assessment: Concern
|
|
25
|
+
Evidence: `Process` validates input, applies pricing rules, persists data, and sends notifications. The name is broad and hides multiple responsibilities.
|
|
26
|
+
Suggestion: Split the work into smaller methods with explicit names such as `ApplyDiscounts`, `SaveBatch`, and `NotifyCustomer`.
|
|
27
|
+
|
|
28
|
+
# Refactor plan
|
|
29
|
+
|
|
30
|
+
1. Replace the outer `if/else` with a guard clause so `Process` starts with the valid path.
|
|
31
|
+
2. Extract discount selection into a small method or domain object to remove nested conditional logic.
|
|
32
|
+
3. Introduce value objects for `CustomerType` and `DiscountRate` to replace raw primitives.
|
|
33
|
+
4. Replace raw `List<Order>` parameters with an `OrderBatch` collection abstraction.
|
|
34
|
+
5. Split `Process` into smaller methods whose names reveal validation, pricing, persistence, and notification separately.
|
|
35
|
+
|
|
36
|
+
# Optional snippet
|
|
37
|
+
|
|
38
|
+
```csharp
|
|
39
|
+
if (orders is null)
|
|
40
|
+
{
|
|
41
|
+
throw new ArgumentNullException(nameof(orders));
|
|
42
|
+
}
|
|
43
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Code Calisthenics Checklist
|
|
2
|
+
|
|
3
|
+
## Core checks
|
|
4
|
+
- More than one indentation level inside a method?
|
|
5
|
+
- Any else blocks?
|
|
6
|
+
- Any primitive obsession?
|
|
7
|
+
- Any collection passed around without its own abstraction?
|
|
8
|
+
- Any method doing more than one thing?
|
|
9
|
+
- Any name that hides domain meaning?
|
|
10
|
+
- Any obvious guard clause missing?
|
|
11
|
+
|
|
12
|
+
## Refactor priorities
|
|
13
|
+
1. Flatten control flow
|
|
14
|
+
2. Extract domain concepts
|
|
15
|
+
3. Improve names
|
|
16
|
+
4. Separate responsibilities
|
|
17
|
+
5. Reduce incidental complexity
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: csharp-code-calisthenics-reviewer
|
|
3
|
+
description: Review a single C# file or small C# class against a fixed, narrow set of code-calisthenics rules and produce a deterministic refactor plan. Use when asked to critique or review C# code for indentation depth, else usage, primitive obsession, missing first-class collections, and overly large or poorly named methods.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# C# Code Calisthenics Reviewer
|
|
7
|
+
|
|
8
|
+
Review one C# file or one small class at a time. Keep the review narrow, deterministic, and grounded in the provided code.
|
|
9
|
+
|
|
10
|
+
## Rules
|
|
11
|
+
|
|
12
|
+
Check only these 5 rules:
|
|
13
|
+
|
|
14
|
+
1. Only one level of indentation per method
|
|
15
|
+
2. Avoid `else`
|
|
16
|
+
3. Wrap primitives in value objects when it makes sense
|
|
17
|
+
4. First-class collections
|
|
18
|
+
5. Small methods with intention-revealing names
|
|
19
|
+
|
|
20
|
+
Do not expand into the broader code-calisthenics catalog unless the user explicitly asks for that.
|
|
21
|
+
|
|
22
|
+
## Working style
|
|
23
|
+
|
|
24
|
+
- Read the code carefully before judging it.
|
|
25
|
+
- Stay concrete and repeatable. Do not use random scoring, vague style commentary, or speculative claims.
|
|
26
|
+
- Base every finding on visible code in the provided file or class.
|
|
27
|
+
- Mention guard-clause opportunities when they help flatten control flow.
|
|
28
|
+
- Do not perform a full automatic rewrite by default.
|
|
29
|
+
- Include a short illustrative snippet only when it makes a recommendation easier to understand.
|
|
30
|
+
|
|
31
|
+
## Review process
|
|
32
|
+
|
|
33
|
+
1. Identify the class or methods under review.
|
|
34
|
+
2. Evaluate each of the 5 rules in order.
|
|
35
|
+
3. Record only the findings that are supported by the code.
|
|
36
|
+
4. Prioritize issues that reduce nesting, split responsibilities, and clarify domain language.
|
|
37
|
+
5. End with a refactor plan ordered by highest leverage first.
|
|
38
|
+
|
|
39
|
+
## Output format
|
|
40
|
+
|
|
41
|
+
Use this structure:
|
|
42
|
+
|
|
43
|
+
### Findings
|
|
44
|
+
|
|
45
|
+
For each rule:
|
|
46
|
+
|
|
47
|
+
- `Rule:` the exact rule name
|
|
48
|
+
- `Assessment:` `Pass`, `Concern`, or `Not applicable`
|
|
49
|
+
- `Evidence:` a brief code-specific explanation
|
|
50
|
+
- `Suggestion:` one concrete next step
|
|
51
|
+
|
|
52
|
+
### Refactor plan
|
|
53
|
+
|
|
54
|
+
Provide 3 to 6 ordered steps. Each step should describe a refactor action, not just a diagnosis.
|
|
55
|
+
|
|
56
|
+
### Optional snippet
|
|
57
|
+
|
|
58
|
+
Only include this section when a very short snippet clarifies one suggested refactor. Keep it focused on one change, not a full rewritten file.
|
|
59
|
+
|
|
60
|
+
## References
|
|
61
|
+
|
|
62
|
+
- Read `references/calisthenics-checklist.md` for the review checklist and refactor priorities.
|
|
63
|
+
- Use `examples/input/OrderService.cs` and `examples/output/review.md` as the canonical example of expected scope and output shape.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
|
|
5
|
+
namespace Example;
|
|
6
|
+
|
|
7
|
+
public class OrderService
|
|
8
|
+
{
|
|
9
|
+
public void Process(List<Order> orders, decimal discountRate, string customerType)
|
|
10
|
+
{
|
|
11
|
+
if (orders == null)
|
|
12
|
+
{
|
|
13
|
+
throw new ArgumentNullException(nameof(orders));
|
|
14
|
+
}
|
|
15
|
+
else
|
|
16
|
+
{
|
|
17
|
+
foreach (var order in orders)
|
|
18
|
+
{
|
|
19
|
+
if (order.Total > 100)
|
|
20
|
+
{
|
|
21
|
+
if (customerType == "VIP")
|
|
22
|
+
{
|
|
23
|
+
order.Total = order.Total - (order.Total * discountRate);
|
|
24
|
+
}
|
|
25
|
+
else
|
|
26
|
+
{
|
|
27
|
+
order.Total = order.Total - 5;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Save(orders);
|
|
33
|
+
Notify(customerType, orders.Count);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private void Save(List<Order> orders)
|
|
38
|
+
{
|
|
39
|
+
Console.WriteLine($"Saved {orders.Count} orders.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private void Notify(string customerType, int orderCount)
|
|
43
|
+
{
|
|
44
|
+
Console.WriteLine($"Notified {customerType} customer about {orderCount} orders.");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public class Order
|
|
49
|
+
{
|
|
50
|
+
public decimal Total { get; set; }
|
|
51
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Findings
|
|
2
|
+
|
|
3
|
+
- Rule: Only one level of indentation per method
|
|
4
|
+
Assessment: Concern
|
|
5
|
+
Evidence: `Process` nests a null check, a `foreach`, an order-total condition, and a customer-type condition. The deepest branch goes well beyond one indentation level.
|
|
6
|
+
Suggestion: Use a guard clause for `orders == null`, then extract the discount decision into a separate method or policy object.
|
|
7
|
+
|
|
8
|
+
- Rule: Avoid `else`
|
|
9
|
+
Assessment: Concern
|
|
10
|
+
Evidence: `Process` contains an `else` after the null check and another `else` inside the discount logic.
|
|
11
|
+
Suggestion: Replace both branches with guard clauses or early returns so the happy path stays flat.
|
|
12
|
+
|
|
13
|
+
- Rule: Wrap primitives in value objects when it makes sense
|
|
14
|
+
Assessment: Concern
|
|
15
|
+
Evidence: `discountRate` and `customerType` carry domain meaning but are represented as raw `decimal` and `string`.
|
|
16
|
+
Suggestion: Introduce value objects such as `DiscountRate` and `CustomerType` to make valid states explicit.
|
|
17
|
+
|
|
18
|
+
- Rule: First-class collections
|
|
19
|
+
Assessment: Concern
|
|
20
|
+
Evidence: `List<Order>` is passed through `Process` and `Save` as a raw collection with behavior spread across the service.
|
|
21
|
+
Suggestion: Introduce an `OrderBatch` or similar collection abstraction to hold iteration and batch-level behavior.
|
|
22
|
+
|
|
23
|
+
- Rule: Small methods with intention-revealing names
|
|
24
|
+
Assessment: Concern
|
|
25
|
+
Evidence: `Process` validates input, applies pricing rules, persists data, and sends notifications. The name is broad and hides multiple responsibilities.
|
|
26
|
+
Suggestion: Split the work into smaller methods with explicit names such as `ApplyDiscounts`, `SaveBatch`, and `NotifyCustomer`.
|
|
27
|
+
|
|
28
|
+
# Refactor plan
|
|
29
|
+
|
|
30
|
+
1. Replace the outer `if/else` with a guard clause so `Process` starts with the valid path.
|
|
31
|
+
2. Extract discount selection into a small method or domain object to remove nested conditional logic.
|
|
32
|
+
3. Introduce value objects for `CustomerType` and `DiscountRate` to replace raw primitives.
|
|
33
|
+
4. Replace raw `List<Order>` parameters with an `OrderBatch` collection abstraction.
|
|
34
|
+
5. Split `Process` into smaller methods whose names reveal validation, pricing, persistence, and notification separately.
|
|
35
|
+
|
|
36
|
+
# Optional snippet
|
|
37
|
+
|
|
38
|
+
```csharp
|
|
39
|
+
if (orders is null)
|
|
40
|
+
{
|
|
41
|
+
throw new ArgumentNullException(nameof(orders));
|
|
42
|
+
}
|
|
43
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Code Calisthenics Checklist
|
|
2
|
+
|
|
3
|
+
## Core checks
|
|
4
|
+
- More than one indentation level inside a method?
|
|
5
|
+
- Any else blocks?
|
|
6
|
+
- Any primitive obsession?
|
|
7
|
+
- Any collection passed around without its own abstraction?
|
|
8
|
+
- Any method doing more than one thing?
|
|
9
|
+
- Any name that hides domain meaning?
|
|
10
|
+
- Any obvious guard clause missing?
|
|
11
|
+
|
|
12
|
+
## Refactor priorities
|
|
13
|
+
1. Flatten control flow
|
|
14
|
+
2. Extract domain concepts
|
|
15
|
+
3. Improve names
|
|
16
|
+
4. Separate responsibilities
|
|
17
|
+
5. Reduce incidental complexity
|