@rmrdeveloper/sideroom-pi 8.14.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-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",
|
|
@@ -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.
|
|
@@ -25,6 +25,7 @@ Rules every JavaScript 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
|
|
|
@@ -377,11 +378,43 @@ counter++;
|
|
|
377
378
|
setInterval(checkPaymentStatus, 5_000);
|
|
378
379
|
```
|
|
379
380
|
|
|
381
|
+
### 20. Decomposition by intent
|
|
382
|
+
|
|
383
|
+
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.
|
|
384
|
+
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.
|
|
385
|
+
|
|
386
|
+
```js
|
|
387
|
+
// bad
|
|
388
|
+
function createPackingSlip(items, destination) {
|
|
389
|
+
let unitCount = 0;
|
|
390
|
+
let weightGrams = 0;
|
|
391
|
+
for (const item of items) {
|
|
392
|
+
unitCount += item.quantity;
|
|
393
|
+
weightGrams += item.quantity * item.unitWeightGrams;
|
|
394
|
+
}
|
|
395
|
+
return { destination, totals: { unitCount, weightGrams } };
|
|
396
|
+
}
|
|
397
|
+
// good
|
|
398
|
+
function createPackingSlip(items, destination) {
|
|
399
|
+
return { destination, totals: calculatePackingTotals(items) };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function calculatePackingTotals(items) {
|
|
403
|
+
let unitCount = 0;
|
|
404
|
+
let weightGrams = 0;
|
|
405
|
+
for (const item of items) {
|
|
406
|
+
unitCount += item.quantity;
|
|
407
|
+
weightGrams += item.quantity * item.unitWeightGrams;
|
|
408
|
+
}
|
|
409
|
+
return { unitCount, weightGrams };
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
380
413
|
---
|
|
381
414
|
|
|
382
415
|
**When in doubt:** fail fast, keep it flat, keep it small.
|
|
383
416
|
|
|
384
|
-
**Enforcement note:** rules 1–2 are mechanical and should be backed by a JavaScript formatter/linter. Rules 3–10 are structural
|
|
417
|
+
**Enforcement note:** rules 1–2 are mechanical and should be backed by a JavaScript formatter/linter. 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.
|
|
385
418
|
|
|
386
419
|
## Language-specific conventions
|
|
387
420
|
|
|
@@ -25,6 +25,7 @@ Rules every PHP or Laravel 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
|
|
|
@@ -241,8 +242,40 @@ RULE: Comment only non-obvious intent — a trade-off, a constraint, a hazard na
|
|
|
241
242
|
Schedule::call($checkPaymentStatus)->everyFiveSeconds();
|
|
242
243
|
```
|
|
243
244
|
|
|
245
|
+
### 20. Decomposition by intent
|
|
246
|
+
|
|
247
|
+
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.
|
|
248
|
+
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.
|
|
249
|
+
|
|
250
|
+
```php
|
|
251
|
+
// bad
|
|
252
|
+
function createPackingSlip(array $items, string $destination): PackingSlip {
|
|
253
|
+
$unitCount = 0;
|
|
254
|
+
$weightGrams = 0;
|
|
255
|
+
foreach ($items as $item) {
|
|
256
|
+
$unitCount += $item->quantity;
|
|
257
|
+
$weightGrams += $item->quantity * $item->unitWeightGrams;
|
|
258
|
+
}
|
|
259
|
+
return new PackingSlip($destination, new PackingTotals($unitCount, $weightGrams));
|
|
260
|
+
}
|
|
261
|
+
// good
|
|
262
|
+
function createPackingSlip(array $items, string $destination): PackingSlip {
|
|
263
|
+
return new PackingSlip($destination, calculatePackingTotals($items));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function calculatePackingTotals(array $items): PackingTotals {
|
|
267
|
+
$unitCount = 0;
|
|
268
|
+
$weightGrams = 0;
|
|
269
|
+
foreach ($items as $item) {
|
|
270
|
+
$unitCount += $item->quantity;
|
|
271
|
+
$weightGrams += $item->quantity * $item->unitWeightGrams;
|
|
272
|
+
}
|
|
273
|
+
return new PackingTotals($unitCount, $weightGrams);
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
244
277
|
---
|
|
245
278
|
|
|
246
279
|
**When in doubt:** fail fast, keep it flat, keep it small.
|
|
247
280
|
|
|
248
|
-
**Enforcement note:** rules 1–2 are mechanical and should be backed by PHP/Laravel formatting or linting. Rules 3–10 are structural
|
|
281
|
+
**Enforcement note:** rules 1–2 are mechanical and should be backed by PHP/Laravel 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.
|