@rmrdeveloper/sideroom-pi 8.13.0 → 9.0.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/README.md +19 -9
- package/assets/artifacts/GUIDELINES_TEMPLATE.md +33 -2
- package/assets/visual-context/browser/INSTALL.md +11 -2
- package/assets/visual-context/browser/background.js +72 -60
- package/assets/visual-context/browser/content.js +45 -7
- package/assets/visual-context/browser/destinations.js +40 -0
- package/assets/visual-context/browser/manifest.json +1 -1
- package/extensions/guidelines/catalog.ts +9 -0
- package/extensions/guidelines/execute.ts +26 -0
- package/extensions/guidelines/guard.ts +46 -14
- package/extensions/guidelines/index.ts +31 -0
- package/extensions/guidelines/prompt.ts +2 -2
- package/extensions/guidelines/review.ts +1 -1
- package/extensions/jev/model.ts +13 -0
- package/extensions/visual-context/broker.ts +14 -6
- package/extensions/visual-context/compiler-config.ts +103 -0
- package/extensions/visual-context/configure.ts +421 -0
- package/extensions/visual-context/dialogs.ts +95 -0
- package/extensions/visual-context/files.ts +139 -0
- package/extensions/visual-context/index.ts +48 -56
- package/extensions/visual-context/match.ts +48 -16
- package/extensions/visual-context/model.ts +29 -3
- package/extensions/visual-context/projects.ts +226 -0
- package/extensions/visual-context/session.ts +150 -17
- package/extensions/visual-context/setup.ts +146 -0
- package/extensions/visual-context/source.ts +203 -100
- package/extensions/visual-context/ui.ts +5 -1
- package/package.json +5 -5
- package/skills/sideroom-codebase-design/SKILL.md +28 -1
- package/skills/sideroom-guidelines/SKILL.md +8 -5
- package/skills/sideroom-guidelines/references/languages/go.md +35 -1
- package/skills/sideroom-guidelines/references/languages/java.md +34 -1
- package/skills/sideroom-guidelines/references/languages/javascript.md +34 -1
- package/skills/sideroom-guidelines/references/languages/php-laravel.md +34 -1
- package/skills/sideroom-guidelines/references/languages/python.md +30 -1
- package/skills/sideroom-guidelines/references/languages/rust.md +34 -1
- package/skills/sideroom-guidelines/references/languages/typescript.md +38 -1
- package/skills/sideroom-visual-setup/SKILL.md +49 -0
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
-
import { extname, join, relative, sep } from 'node:path';
|
|
3
|
-
import ignore from 'ignore';
|
|
2
|
+
import { extname, isAbsolute, join, relative, sep } from 'node:path';
|
|
4
3
|
import { projectRelativePath } from '../shared/file-path.ts';
|
|
5
4
|
import { isMissingFile } from '../shared/read-file.ts';
|
|
5
|
+
import {
|
|
6
|
+
allowedPath,
|
|
7
|
+
createPathPolicy,
|
|
8
|
+
MAX_FILE_BYTES,
|
|
9
|
+
type VisualPathPolicy,
|
|
10
|
+
} from './files.ts';
|
|
6
11
|
import { InvalidMarkup } from './markup.ts';
|
|
7
|
-
import { matchSource, sourceFingerprint } from './match.ts';
|
|
12
|
+
import { matchSource, sourceFingerprint, sourceMatches } from './match.ts';
|
|
8
13
|
import {
|
|
9
14
|
MAX_CANDIDATES,
|
|
10
15
|
type SourceCandidate,
|
|
@@ -21,45 +26,42 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
21
26
|
'.jsx',
|
|
22
27
|
'.tsx',
|
|
23
28
|
]);
|
|
24
|
-
const SKIP_DIRECTORIES = new Set([
|
|
25
|
-
'node_modules',
|
|
26
|
-
'dist',
|
|
27
|
-
'build',
|
|
28
|
-
'coverage',
|
|
29
|
-
'vendor',
|
|
30
|
-
]);
|
|
31
|
-
const IGNORE_FILES = ['.gitignore', '.ignore', '.fdignore'];
|
|
32
29
|
const MAX_ENTRIES = 4_096;
|
|
33
|
-
const MAX_FILE_BYTES = 262_144;
|
|
34
30
|
const MAX_SOURCE_BYTES = 8_388_608;
|
|
35
31
|
const MAX_DEPTH = 16;
|
|
36
|
-
const SENSITIVE_NAME =
|
|
37
|
-
/secret|credential|password|private.?key|(?:^|[._-])env(?:[._-]|$)/iu;
|
|
38
32
|
|
|
39
33
|
export function resolveSelection(
|
|
40
34
|
cwd: string,
|
|
41
35
|
selection: VisualSelection,
|
|
42
36
|
): SourceResolution {
|
|
37
|
+
if (selection.source) {
|
|
38
|
+
return resolveCompilerLocation({ cwd, selection });
|
|
39
|
+
}
|
|
40
|
+
return scanSource({ cwd, selection, paths: createPathPolicy(cwd) });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface SourceScanInput {
|
|
44
|
+
readonly cwd: string;
|
|
45
|
+
readonly selection: VisualSelection;
|
|
46
|
+
readonly paths: VisualPathPolicy;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function scanSource({
|
|
50
|
+
cwd,
|
|
51
|
+
selection,
|
|
52
|
+
paths,
|
|
53
|
+
}: SourceScanInput): SourceResolution {
|
|
43
54
|
const root = realpathSync(cwd);
|
|
44
55
|
const candidates: SourceCandidate[] = [];
|
|
45
56
|
let entriesRead = 0;
|
|
46
57
|
let sourceBytes = 0;
|
|
47
58
|
let truncated = false;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
patterns,
|
|
51
|
-
depth,
|
|
52
|
-
}: {
|
|
53
|
-
readonly directory: string;
|
|
54
|
-
readonly patterns: readonly string[];
|
|
55
|
-
readonly depth: number;
|
|
56
|
-
}): void {
|
|
59
|
+
let capped = false;
|
|
60
|
+
function walk(directory: string, depth: number): void {
|
|
57
61
|
if (depth > MAX_DEPTH) {
|
|
58
62
|
truncated = true;
|
|
59
63
|
return;
|
|
60
64
|
}
|
|
61
|
-
const localPatterns = [...patterns, ...ignorePatterns(directory, root)];
|
|
62
|
-
const matcher = ignore().add(localPatterns);
|
|
63
65
|
for (const entry of readdirSync(directory, { withFileTypes: true }).sort(
|
|
64
66
|
(left, right) => left.name.localeCompare(right.name),
|
|
65
67
|
)) {
|
|
@@ -68,30 +70,13 @@ export function resolveSelection(
|
|
|
68
70
|
truncated = true;
|
|
69
71
|
return;
|
|
70
72
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
[...entry.name].some(
|
|
74
|
-
(character) =>
|
|
75
|
-
character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,
|
|
76
|
-
) ||
|
|
77
|
-
SENSITIVE_NAME.test(entry.name) ||
|
|
78
|
-
entry.isSymbolicLink()
|
|
79
|
-
) {
|
|
73
|
+
const absolutePath = paths.allowedPath(join(directory, entry.name));
|
|
74
|
+
if (!absolutePath) {
|
|
80
75
|
continue;
|
|
81
76
|
}
|
|
82
|
-
const absolutePath = join(directory, entry.name);
|
|
83
77
|
const path = relative(root, absolutePath).split(sep).join('/');
|
|
84
|
-
if (matcher.ignores(entry.isDirectory() ? `${path}/` : path)) {
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
78
|
if (entry.isDirectory()) {
|
|
88
|
-
|
|
89
|
-
walk({
|
|
90
|
-
directory: absolutePath,
|
|
91
|
-
patterns: localPatterns,
|
|
92
|
-
depth: depth + 1,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
79
|
+
walk(absolutePath, depth + 1);
|
|
95
80
|
continue;
|
|
96
81
|
}
|
|
97
82
|
if (
|
|
@@ -108,7 +93,14 @@ export function resolveSelection(
|
|
|
108
93
|
sourceBytes += size;
|
|
109
94
|
const source = readFileSync(absolutePath, 'utf8');
|
|
110
95
|
try {
|
|
111
|
-
|
|
96
|
+
for (const candidate of sourceMatches({ path, source, selection })) {
|
|
97
|
+
candidates.push(candidate);
|
|
98
|
+
candidates.sort(compareCandidates);
|
|
99
|
+
if (candidates.length > MAX_CANDIDATES) {
|
|
100
|
+
capped = true;
|
|
101
|
+
candidates.pop();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
112
104
|
} catch (cause) {
|
|
113
105
|
if (cause instanceof InvalidMarkup) {
|
|
114
106
|
truncated = true;
|
|
@@ -118,73 +110,184 @@ export function resolveSelection(
|
|
|
118
110
|
}
|
|
119
111
|
}
|
|
120
112
|
}
|
|
121
|
-
walk(
|
|
122
|
-
candidates
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
113
|
+
walk(root, 0);
|
|
114
|
+
return { candidates, truncated, capped };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function compareCandidates(
|
|
118
|
+
left: SourceCandidate,
|
|
119
|
+
right: SourceCandidate,
|
|
120
|
+
): number {
|
|
121
|
+
return (
|
|
122
|
+
right.score - left.score ||
|
|
123
|
+
left.path.localeCompare(right.path) ||
|
|
124
|
+
left.startLine - right.startLine
|
|
127
125
|
);
|
|
128
|
-
return {
|
|
129
|
-
candidates: candidates.slice(0, MAX_CANDIDATES),
|
|
130
|
-
truncated,
|
|
131
|
-
capped: candidates.length > MAX_CANDIDATES,
|
|
132
|
-
};
|
|
133
126
|
}
|
|
134
127
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
128
|
+
interface CompilerLocationInput {
|
|
129
|
+
readonly cwd: string;
|
|
130
|
+
readonly selection: VisualSelection;
|
|
131
|
+
readonly project?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function resolveCompilerLocation({
|
|
135
|
+
cwd,
|
|
136
|
+
selection,
|
|
137
|
+
project,
|
|
138
|
+
}: CompilerLocationInput): SourceResolution {
|
|
139
|
+
const location = selection.source;
|
|
140
|
+
if (!location) {
|
|
141
|
+
throw new Error('A compiler source location is required.');
|
|
142
|
+
}
|
|
143
|
+
const unresolved = (issue: string): SourceResolution => ({
|
|
144
|
+
candidates: [],
|
|
145
|
+
truncated: false,
|
|
146
|
+
capped: false,
|
|
147
|
+
issue,
|
|
148
|
+
});
|
|
149
|
+
if (!isAbsolute(location.path) && project === undefined) {
|
|
150
|
+
return unresolved(
|
|
151
|
+
'Associate this page with a project in F7 before resolving a relative compiler path.',
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const projectRoot = project === undefined ? cwd : allowedPath(cwd, project);
|
|
155
|
+
if (!projectRoot || !statSync(projectRoot).isDirectory()) {
|
|
156
|
+
return unresolved('The associated project is missing or excluded.');
|
|
157
|
+
}
|
|
158
|
+
const absolute = allowedPath(projectRoot, location.path);
|
|
159
|
+
if (
|
|
160
|
+
!absolute ||
|
|
161
|
+
!allowedPath(cwd, absolute) ||
|
|
162
|
+
!statSync(absolute).isFile() ||
|
|
163
|
+
statSync(absolute).size > MAX_FILE_BYTES
|
|
164
|
+
) {
|
|
165
|
+
return unresolved(
|
|
166
|
+
'The compiler path is missing, excluded, outside the project, or too large.',
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const path = relative(realpathSync(cwd), absolute).split(sep).join('/');
|
|
170
|
+
if (
|
|
171
|
+
![...SOURCE_EXTENSIONS, '.js', '.mjs', '.ts', '.mts'].includes(
|
|
172
|
+
extname(path).toLowerCase(),
|
|
173
|
+
)
|
|
174
|
+
) {
|
|
175
|
+
return unresolved('The compiler path has an unsupported source format.');
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const candidates = matchSource({
|
|
179
|
+
path,
|
|
180
|
+
source: readFileSync(absolute, 'utf8'),
|
|
181
|
+
selection,
|
|
182
|
+
location,
|
|
183
|
+
});
|
|
184
|
+
if (candidates.length !== 1) {
|
|
185
|
+
return unresolved(
|
|
186
|
+
'The compiler location does not match the selected tag in current local markup. Restart or refresh the development page.',
|
|
187
|
+
);
|
|
147
188
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const nested = prefix
|
|
155
|
-
? `${prefix}/${pattern.startsWith('/') ? pattern.slice(1) : pattern.includes('/') ? pattern : `**/${pattern}`}`
|
|
156
|
-
: pattern;
|
|
157
|
-
patterns.push(`${negated ? '!' : ''}${nested}`);
|
|
189
|
+
return { candidates, truncated: false, capped: false };
|
|
190
|
+
} catch (cause) {
|
|
191
|
+
if (cause instanceof InvalidMarkup) {
|
|
192
|
+
return unresolved(
|
|
193
|
+
'The compiler source could not be parsed; its location was not accepted.',
|
|
194
|
+
);
|
|
158
195
|
}
|
|
196
|
+
throw new Error(`Cannot verify compiler source ${path}`, { cause });
|
|
159
197
|
}
|
|
160
|
-
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function resolveProjectSelection({
|
|
201
|
+
cwd,
|
|
202
|
+
selection,
|
|
203
|
+
project,
|
|
204
|
+
}: CompilerLocationInput): SourceResolution {
|
|
205
|
+
if (selection.source) {
|
|
206
|
+
return resolveCompilerLocation({ cwd, selection, project });
|
|
207
|
+
}
|
|
208
|
+
if (project === undefined) {
|
|
209
|
+
return resolveSelection(cwd, selection);
|
|
210
|
+
}
|
|
211
|
+
const workspaceRoot = realpathSync(cwd);
|
|
212
|
+
const paths = createPathPolicy(workspaceRoot);
|
|
213
|
+
const root = paths.allowedPath(project);
|
|
214
|
+
if (!root || !statSync(root).isDirectory()) {
|
|
215
|
+
return {
|
|
216
|
+
candidates: [],
|
|
217
|
+
truncated: false,
|
|
218
|
+
capped: false,
|
|
219
|
+
issue: 'The associated project is missing or excluded.',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const resolution = scanSource({ cwd: root, selection, paths });
|
|
223
|
+
return {
|
|
224
|
+
...resolution,
|
|
225
|
+
candidates: resolution.candidates
|
|
226
|
+
.filter((candidate) => paths.allowedPath(join(root, candidate.path)))
|
|
227
|
+
.map((candidate) => ({
|
|
228
|
+
...candidate,
|
|
229
|
+
path: relative(workspaceRoot, join(root, candidate.path))
|
|
230
|
+
.split(sep)
|
|
231
|
+
.join('/'),
|
|
232
|
+
})),
|
|
233
|
+
};
|
|
161
234
|
}
|
|
162
235
|
|
|
163
236
|
export function candidateIsCurrent(
|
|
164
237
|
cwd: string,
|
|
165
238
|
candidate: SourceCandidate,
|
|
166
239
|
): boolean {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
240
|
+
return currentCandidates(cwd, [candidate]).length === 1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Each call owns its file versions; no validation survives into another prompt.
|
|
244
|
+
export function currentCandidates(
|
|
245
|
+
cwd: string,
|
|
246
|
+
candidates: readonly SourceCandidate[],
|
|
247
|
+
): readonly SourceCandidate[] {
|
|
248
|
+
let paths: VisualPathPolicy | undefined;
|
|
249
|
+
const versions = new Map<
|
|
250
|
+
string,
|
|
251
|
+
{ readonly fingerprint: string; readonly lines: number } | undefined
|
|
252
|
+
>();
|
|
253
|
+
return candidates.filter((candidate) => {
|
|
254
|
+
try {
|
|
255
|
+
paths ??= createPathPolicy(cwd);
|
|
256
|
+
const absolutePath = paths.allowedPath(candidate.path);
|
|
257
|
+
if (!absolutePath) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
if (!versions.has(absolutePath)) {
|
|
261
|
+
let version:
|
|
262
|
+
| { readonly fingerprint: string; readonly lines: number }
|
|
263
|
+
| undefined;
|
|
264
|
+
if (
|
|
265
|
+
projectRelativePath(cwd, absolutePath) !== undefined &&
|
|
266
|
+
statSync(absolutePath).size <= MAX_FILE_BYTES
|
|
267
|
+
) {
|
|
268
|
+
const source = readFileSync(absolutePath, 'utf8');
|
|
269
|
+
version = {
|
|
270
|
+
fingerprint: sourceFingerprint(source),
|
|
271
|
+
lines: source.split('\n').length,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
versions.set(absolutePath, version);
|
|
275
|
+
}
|
|
276
|
+
const version = versions.get(absolutePath);
|
|
277
|
+
return (
|
|
278
|
+
version !== undefined &&
|
|
279
|
+
version.fingerprint === candidate.fingerprint &&
|
|
280
|
+
candidate.endLine <= version.lines
|
|
281
|
+
);
|
|
282
|
+
} catch (cause) {
|
|
283
|
+
if (isGone(cause)) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
throw new Error(`Cannot verify selected source ${candidate.path}`, {
|
|
287
|
+
cause,
|
|
288
|
+
});
|
|
183
289
|
}
|
|
184
|
-
|
|
185
|
-
cause,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
290
|
+
});
|
|
188
291
|
}
|
|
189
292
|
|
|
190
293
|
// A path that turned into a file on the way reports ENOTDIR, which means the
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
candidateLabel,
|
|
5
5
|
elementLabel,
|
|
6
6
|
type PendingSelection,
|
|
7
|
+
SOURCE_METHOD,
|
|
7
8
|
VISUAL_WIDGET,
|
|
8
9
|
} from './model.ts';
|
|
9
10
|
|
|
@@ -13,7 +14,10 @@ export const BROWSER_EXTENSION_DIRECTORY = fileURLToPath(
|
|
|
13
14
|
|
|
14
15
|
function pendingLabel({ resolution, chosen }: PendingSelection): string {
|
|
15
16
|
if (chosen) {
|
|
16
|
-
return `${candidateLabel(chosen)} (selected candidate)`;
|
|
17
|
+
return `${candidateLabel(chosen)}${chosen.startColumn ? `:${String(chosen.startColumn)}` : ''} (${chosen.method === SOURCE_METHOD.compiler ? 'compiler location checked' : 'selected candidate'})`;
|
|
18
|
+
}
|
|
19
|
+
if (resolution.issue) {
|
|
20
|
+
return resolution.issue;
|
|
17
21
|
}
|
|
18
22
|
const truncated = resolution.truncated ? ' · search truncated' : '';
|
|
19
23
|
const capped = resolution.capped ? ' · more candidates' : '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmrdeveloper/sideroom-pi",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "Pi coding-agent package that asks clarifying questions, shows a live todo board, enforces coding guidelines, and will not call it done while your checks are red.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -81,8 +81,8 @@
|
|
|
81
81
|
"video": "https://raw.githubusercontent.com/RMRdeveloper/sideroom-pi/main/media/preview.mp4"
|
|
82
82
|
},
|
|
83
83
|
"peerDependencies": {
|
|
84
|
-
"@earendil-works/pi-coding-agent": "
|
|
85
|
-
"@earendil-works/pi-tui": "
|
|
84
|
+
"@earendil-works/pi-coding-agent": ">=1.2.0",
|
|
85
|
+
"@earendil-works/pi-tui": ">=1.2.0",
|
|
86
86
|
"typebox": "*"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
@@ -90,8 +90,8 @@
|
|
|
90
90
|
"@changesets/cli": "^3.0.2",
|
|
91
91
|
"@commitlint/cli": "^21.2.2",
|
|
92
92
|
"@commitlint/config-conventional": "^21.2.2",
|
|
93
|
-
"@earendil-works/pi-coding-agent": "
|
|
94
|
-
"@earendil-works/pi-tui": "
|
|
93
|
+
"@earendil-works/pi-coding-agent": "1.2.0",
|
|
94
|
+
"@earendil-works/pi-tui": "1.2.0",
|
|
95
95
|
"@types/node": "^22.20.1",
|
|
96
96
|
"@types/ws": "^8.18.2",
|
|
97
97
|
"fflate": "0.8.3",
|
|
@@ -4,7 +4,7 @@ description: "Improve an existing codebase for long-term maintainability and sca
|
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: RMRdeveloper
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Sideroom codebase design
|
|
@@ -33,6 +33,29 @@ A review request does not authorize a rewrite. An implementation request does
|
|
|
33
33
|
not require pausing after every recommendation. Ask only about consequential
|
|
34
34
|
choices or missing facts that the user and repository have not settled.
|
|
35
35
|
|
|
36
|
+
## Vocabulary (use these terms exactly)
|
|
37
|
+
|
|
38
|
+
Use these terms consistently in all recommendations and designs. Do not substitute "component", "service", "API", "boundary", "unit" or similar loose words. Consistent language is the point.
|
|
39
|
+
|
|
40
|
+
| Term | Meaning | Avoid |
|
|
41
|
+
|------|---------|-------|
|
|
42
|
+
| **Module** | Anything with an interface and an implementation. Scale-agnostic: a function, class, package, or tier-spanning slice. | unit, component, service |
|
|
43
|
+
| **Interface** | Everything a caller must know to use the module correctly: the type signature, plus invariants, ordering constraints, error modes, required configuration, and relevant performance or resource behaviour. | API, signature |
|
|
44
|
+
| **Implementation** | The code inside the module. Distinct from Adapter. | — |
|
|
45
|
+
| **Depth** | Leverage at the interface: the amount of useful behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface, **shallow** when the interface is nearly as complex as the implementation. | — |
|
|
46
|
+
| **Seam** | A place where you can alter behaviour without editing in that place (Michael Feathers). It is the *location* at which a module's interface lives. Where to put the seam is its own design decision, distinct from what goes behind it. | boundary |
|
|
47
|
+
| **Adapter** | A concrete thing that satisfies an interface at a seam. Describes *role* (what slot it fills), not substance. | — |
|
|
48
|
+
| **Leverage** | What callers get from depth: more capability per unit of interface they learn. One implementation pays back across many call sites and tests. | — |
|
|
49
|
+
| **Locality** | What maintainers get from depth: change, bugs, knowledge, and verification concentrate in one place rather than spreading across callers. | — |
|
|
50
|
+
|
|
51
|
+
**Principles that follow from the vocabulary:**
|
|
52
|
+
- Depth is a property of the **interface**, not of the implementation size. A deep module can be internally composed of small, mockable, swappable parts; they just aren't part of the interface.
|
|
53
|
+
- The **deletion test**: imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across callers, it was earning its keep.
|
|
54
|
+
- The interface is the test surface. Callers and tests cross the same seam. If you want to test past the interface, the module is probably the wrong shape.
|
|
55
|
+
- One adapter means a hypothetical seam. Two adapters (e.g. production + test stand-in) means a real one. Don't introduce a seam unless something actually varies across it.
|
|
56
|
+
|
|
57
|
+
When writing recommendations, use these terms. Explain any necessary design term once if the user is unfamiliar, then stick to them. Respect the project's existing domain vocabulary (`CONTEXT.md` or equivalent) for business concepts — do not replace established domain names just to impose this design glossary.
|
|
58
|
+
|
|
36
59
|
## Inspect before recommending
|
|
37
60
|
|
|
38
61
|
1. Read project instructions, existing domain vocabulary (`CONTEXT.md`, or the
|
|
@@ -55,6 +78,8 @@ choices or missing facts that the user and repository have not settled.
|
|
|
55
78
|
|
|
56
79
|
## Design principles
|
|
57
80
|
|
|
81
|
+
Apply the vocabulary above. In particular:
|
|
82
|
+
|
|
58
83
|
- A **module** presents an **interface** and hides an implementation. Its
|
|
59
84
|
interface includes everything callers must understand: inputs, outputs,
|
|
60
85
|
invariants, errors, ordering, configuration, and relevant resource behavior.
|
|
@@ -141,6 +166,8 @@ verification, and unresolved facts. After implementation, report changes,
|
|
|
141
166
|
checks, and remaining limitations. Keep review results in the conversation
|
|
142
167
|
unless the user requests a document. No graphic or HTML artifact is produced.
|
|
143
168
|
|
|
169
|
+
**Use the design vocabulary (module, interface, depth, seam, adapter, leverage, locality) consistently** in all recommendations and designs.
|
|
170
|
+
|
|
144
171
|
## Attribution
|
|
145
172
|
|
|
146
173
|
Adapted from Matt Pocock's
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sideroom-guidelines
|
|
3
|
-
description: "Follow the mandatory coding workflow before any write or edit, including bug fixes, features, refactors, tests, configuration, and documentation edits.
|
|
3
|
+
description: "Follow the mandatory coding workflow before any write or edit, including bug fixes, features, refactors, tests, configuration, and documentation edits. Load the complete skill and exact language guide through sideroom_guidelines or full read calls for Java, PHP/Laravel, JavaScript/JSX, TypeScript/TSX, Python, Go, or Rust targets. Discussion or read-only investigation alone does not require loading it."
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: RMRdeveloper
|
|
@@ -11,12 +11,14 @@ metadata:
|
|
|
11
11
|
|
|
12
12
|
## Activation Contract
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Before `write` or `edit`, call `sideroom_guidelines` with the target file's `path`. It returns this whole skill and the matching complete language guide, or only this skill for unsupported languages. Reuse loaded guides during the same agent run; load any additional target language before editing it. Reload after compaction or a new run.
|
|
15
|
+
|
|
16
|
+
Alternatively, load this whole skill with `read`, without `offset` or `limit`, using the absolute packaged path shown in Available Skills. For each supported target language, also fully load its exact guide from `references/languages/`. Both routes satisfy the same gate only on successful, complete delivery.
|
|
15
17
|
|
|
16
18
|
## Hard Rules
|
|
17
19
|
|
|
18
20
|
- Apply the loaded guide to changed code in the context of the full user requirements and established project contracts. Architectural rules require demonstrated evidence; KISS, DRY, and YAGNI never justify omitting requested behavior, required error handling, compatibility, or meaningful tests.
|
|
19
|
-
- Do not write or edit until exact full-file
|
|
21
|
+
- Do not write or edit until the required packaged documents are fully loaded through `sideroom_guidelines` or exact full-file `read` calls; failed or partial loads and same-named files do not count.
|
|
20
22
|
- Do not bypass the gate with Bash, a subprocess, or another mutation tool.
|
|
21
23
|
- Do not load language guides unrelated to files being changed.
|
|
22
24
|
- Fail fast, keep control flow flat, and validate once at the boundary.
|
|
@@ -36,8 +38,8 @@ Load this whole skill with `read`, without `offset` or `limit`, before the first
|
|
|
36
38
|
|
|
37
39
|
## Execution Steps
|
|
38
40
|
|
|
39
|
-
1.
|
|
40
|
-
2.
|
|
41
|
+
1. Load this complete skill through `sideroom_guidelines` with the target path, or a full `read` of its packaged path, before any mutation.
|
|
42
|
+
2. Ensure the matching complete language guide is loaded for supported extensions. The tool includes it automatically; with `read`, load it separately.
|
|
41
43
|
3. Implement a focused change that satisfies all current requirements and preserves required existing behavior.
|
|
42
44
|
4. Review the changes introduced by this run against the loaded guide; fix demonstrated violations caused by the change without refactoring unrelated code.
|
|
43
45
|
5. Run the relevant formatter, linter, type checks, and tests before finishing.
|
|
@@ -51,6 +53,7 @@ Load this whole skill with `read`, without `offset` or `limit`, before the first
|
|
|
51
53
|
| Explicit error, fail now | Fallbacks that hide the real failure |
|
|
52
54
|
| One responsibility per unit | Validate + transform + persist + notify together |
|
|
53
55
|
| Extract for real duplication, a present responsibility, or testability | Abstract only for hypothetical future reuse |
|
|
56
|
+
| Decomposition by intent (rule 20): name a coherent operation so its caller reads the flow without implementation details, even with one caller | Split only to shorten functions, or separate coupled steps into shared mutable state, excessive parameters, or functions that must be read together |
|
|
54
57
|
| Simplest design that satisfies the full requirements | Omit necessary structure to minimize code |
|
|
55
58
|
| Implement current requirements and established contracts completely | Add capabilities only for hypothetical future cases |
|
|
56
59
|
| Compose small focused units | Deep inheritance for unrelated behavior |
|
|
@@ -25,6 +25,7 @@ Rules every Go change must follow.
|
|
|
25
25
|
| 17 | [Dependency direction](#17-dependency-direction) | architectural |
|
|
26
26
|
| 18 | [Testability](#18-testability-as-a-design-constraint) | architectural |
|
|
27
27
|
| 19 | [Comments](#19-comments) | architectural |
|
|
28
|
+
| 20 | [Decomposition by intent](#20-decomposition-by-intent) | structural |
|
|
28
29
|
|
|
29
30
|
---
|
|
30
31
|
|
|
@@ -370,8 +371,41 @@ counter++
|
|
|
370
371
|
ticker := time.NewTicker(5 * time.Second)
|
|
371
372
|
```
|
|
372
373
|
|
|
374
|
+
### 20. Decomposition by intent
|
|
375
|
+
|
|
376
|
+
RULE: Extract a block into a named function when it represents a coherent operation and its name and contract let the caller understand the flow without reading its implementation. Keep orchestration separate from implementation details. Do not split solely to reduce line count; keep tightly coupled steps together when separating them would introduce shared mutable state, excessive parameters, or require reading both functions to understand either.
|
|
377
|
+
WHY: Good decomposition reduces how much a reader must understand at once. Function length is a review signal; extraction must improve comprehension rather than merely move code elsewhere.
|
|
378
|
+
|
|
379
|
+
```go
|
|
380
|
+
// bad
|
|
381
|
+
func createPackingSlip(items []PackingItem, destination string) PackingSlip {
|
|
382
|
+
unitCount := 0
|
|
383
|
+
weightGrams := 0
|
|
384
|
+
for _, item := range items {
|
|
385
|
+
unitCount += item.Quantity
|
|
386
|
+
weightGrams += item.Quantity * item.UnitWeightGrams
|
|
387
|
+
}
|
|
388
|
+
return PackingSlip{Destination: destination, Totals: PackingTotals{UnitCount: unitCount, WeightGrams: weightGrams}}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// good
|
|
392
|
+
func createPackingSlip(items []PackingItem, destination string) PackingSlip {
|
|
393
|
+
return PackingSlip{Destination: destination, Totals: calculatePackingTotals(items)}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
func calculatePackingTotals(items []PackingItem) PackingTotals {
|
|
397
|
+
unitCount := 0
|
|
398
|
+
weightGrams := 0
|
|
399
|
+
for _, item := range items {
|
|
400
|
+
unitCount += item.Quantity
|
|
401
|
+
weightGrams += item.Quantity * item.UnitWeightGrams
|
|
402
|
+
}
|
|
403
|
+
return PackingTotals{UnitCount: unitCount, WeightGrams: weightGrams}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
373
407
|
---
|
|
374
408
|
|
|
375
409
|
**When in doubt:** fail fast, keep it flat, keep it small.
|
|
376
410
|
|
|
377
|
-
**Enforcement note:** rules 1–2 are mechanical and should be backed by Go formatting or linting. Rules 3–10 are structural
|
|
411
|
+
**Enforcement note:** rules 1–2 are mechanical and should be backed by Go formatting or linting. Rules 3–10 and 20 are structural; rules 11–19 are architectural. Rule 20 requires semantic review, not a line-count limit or mechanical length detector. Rules 11–20 require review judgment.
|
|
@@ -25,6 +25,7 @@ Rules every Java change must follow.
|
|
|
25
25
|
| 17 | [Dependency direction](#17-dependency-direction) | architectural |
|
|
26
26
|
| 18 | [Testability](#18-testability-as-a-design-constraint) | architectural |
|
|
27
27
|
| 19 | [Comments](#19-comments) | architectural |
|
|
28
|
+
| 20 | [Decomposition by intent](#20-decomposition-by-intent) | structural |
|
|
28
29
|
|
|
29
30
|
---
|
|
30
31
|
|
|
@@ -250,8 +251,40 @@ counter.increment();
|
|
|
250
251
|
scheduler.scheduleAtFixedRate(this::checkPaymentStatus, 0, 5, SECONDS);
|
|
251
252
|
```
|
|
252
253
|
|
|
254
|
+
### 20. Decomposition by intent
|
|
255
|
+
|
|
256
|
+
RULE: Extract a block into a named function when it represents a coherent operation and its name and contract let the caller understand the flow without reading its implementation. Keep orchestration separate from implementation details. Do not split solely to reduce line count; keep tightly coupled steps together when separating them would introduce shared mutable state, excessive parameters, or require reading both functions to understand either.
|
|
257
|
+
WHY: Good decomposition reduces how much a reader must understand at once. Function length is a review signal; extraction must improve comprehension rather than merely move code elsewhere.
|
|
258
|
+
|
|
259
|
+
```java
|
|
260
|
+
// bad
|
|
261
|
+
PackingSlip createPackingSlip(List<PackingItem> items, String destination) {
|
|
262
|
+
int unitCount = 0;
|
|
263
|
+
long weightGrams = 0;
|
|
264
|
+
for (PackingItem item : items) {
|
|
265
|
+
unitCount += item.quantity();
|
|
266
|
+
weightGrams += item.quantity() * item.unitWeightGrams();
|
|
267
|
+
}
|
|
268
|
+
return new PackingSlip(destination, new PackingTotals(unitCount, weightGrams));
|
|
269
|
+
}
|
|
270
|
+
// good
|
|
271
|
+
PackingSlip createPackingSlip(List<PackingItem> items, String destination) {
|
|
272
|
+
return new PackingSlip(destination, calculatePackingTotals(items));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private PackingTotals calculatePackingTotals(List<PackingItem> items) {
|
|
276
|
+
int unitCount = 0;
|
|
277
|
+
long weightGrams = 0;
|
|
278
|
+
for (PackingItem item : items) {
|
|
279
|
+
unitCount += item.quantity();
|
|
280
|
+
weightGrams += item.quantity() * item.unitWeightGrams();
|
|
281
|
+
}
|
|
282
|
+
return new PackingTotals(unitCount, weightGrams);
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
253
286
|
---
|
|
254
287
|
|
|
255
288
|
**When in doubt:** fail fast, keep it flat, keep it small.
|
|
256
289
|
|
|
257
|
-
**Enforcement note:** rules 1–2 are mechanical and should be backed by Java formatting or linting. Rules 3–10 are structural
|
|
290
|
+
**Enforcement note:** rules 1–2 are mechanical and should be backed by Java formatting or linting. Rules 3–10 and 20 are structural; rules 11–19 are architectural. Rule 20 requires semantic review, not a line-count limit or mechanical length detector. Rules 11–20 require review judgment.
|