@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.
Files changed (38) hide show
  1. package/README.md +19 -9
  2. package/assets/artifacts/GUIDELINES_TEMPLATE.md +33 -2
  3. package/assets/visual-context/browser/INSTALL.md +11 -2
  4. package/assets/visual-context/browser/background.js +72 -60
  5. package/assets/visual-context/browser/content.js +45 -7
  6. package/assets/visual-context/browser/destinations.js +40 -0
  7. package/assets/visual-context/browser/manifest.json +1 -1
  8. package/extensions/guidelines/catalog.ts +9 -0
  9. package/extensions/guidelines/execute.ts +26 -0
  10. package/extensions/guidelines/guard.ts +46 -14
  11. package/extensions/guidelines/index.ts +31 -0
  12. package/extensions/guidelines/prompt.ts +2 -2
  13. package/extensions/guidelines/review.ts +1 -1
  14. package/extensions/jev/model.ts +13 -0
  15. package/extensions/visual-context/broker.ts +14 -6
  16. package/extensions/visual-context/compiler-config.ts +103 -0
  17. package/extensions/visual-context/configure.ts +421 -0
  18. package/extensions/visual-context/dialogs.ts +95 -0
  19. package/extensions/visual-context/files.ts +139 -0
  20. package/extensions/visual-context/index.ts +48 -56
  21. package/extensions/visual-context/match.ts +48 -16
  22. package/extensions/visual-context/model.ts +29 -3
  23. package/extensions/visual-context/projects.ts +226 -0
  24. package/extensions/visual-context/session.ts +150 -17
  25. package/extensions/visual-context/setup.ts +146 -0
  26. package/extensions/visual-context/source.ts +203 -100
  27. package/extensions/visual-context/ui.ts +5 -1
  28. package/package.json +5 -5
  29. package/skills/sideroom-codebase-design/SKILL.md +28 -1
  30. package/skills/sideroom-guidelines/SKILL.md +8 -5
  31. package/skills/sideroom-guidelines/references/languages/go.md +35 -1
  32. package/skills/sideroom-guidelines/references/languages/java.md +34 -1
  33. package/skills/sideroom-guidelines/references/languages/javascript.md +34 -1
  34. package/skills/sideroom-guidelines/references/languages/php-laravel.md +34 -1
  35. package/skills/sideroom-guidelines/references/languages/python.md +30 -1
  36. package/skills/sideroom-guidelines/references/languages/rust.md +34 -1
  37. package/skills/sideroom-guidelines/references/languages/typescript.md +38 -1
  38. 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
- function walk({
49
- directory,
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
- if (
72
- entry.name.startsWith('.') ||
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
- if (!SKIP_DIRECTORIES.has(entry.name)) {
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
- candidates.push(...matchSource({ path, source, selection }));
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({ directory: root, patterns: [], depth: 0 });
122
- candidates.sort(
123
- (left, right) =>
124
- right.score - left.score ||
125
- left.path.localeCompare(right.path) ||
126
- left.startLine - right.startLine,
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
- function ignorePatterns(directory: string, root: string): readonly string[] {
136
- const prefix = relative(root, directory).split(sep).join('/');
137
- const patterns: string[] = [];
138
- for (const file of IGNORE_FILES) {
139
- let source: string;
140
- try {
141
- source = readFileSync(join(directory, file), 'utf8');
142
- } catch (cause) {
143
- if (isMissingFile(cause)) {
144
- continue;
145
- }
146
- throw new Error(`Cannot read ignore file ${file}`, { cause });
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
- for (const line of source.split(/\r?\n/u)) {
149
- if (!line || line.startsWith('#')) {
150
- continue;
151
- }
152
- const negated = line.startsWith('!');
153
- const pattern = negated ? line.slice(1) : line;
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
- return patterns;
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
- try {
168
- const absolutePath = realpathSync(join(cwd, candidate.path));
169
- if (
170
- projectRelativePath(cwd, absolutePath) === undefined ||
171
- statSync(absolutePath).size > MAX_FILE_BYTES
172
- ) {
173
- return false;
174
- }
175
- const source = readFileSync(absolutePath, 'utf8');
176
- return (
177
- sourceFingerprint(source) === candidate.fingerprint &&
178
- candidate.endLine <= source.split('\n').length
179
- );
180
- } catch (cause) {
181
- if (isGone(cause)) {
182
- return false;
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
- throw new Error(`Cannot verify selected source ${candidate.path}`, {
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": "8.13.0",
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": "0.87.1",
94
- "@earendil-works/pi-tui": "0.87.1",
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.0"
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. Read the complete skill first; also read the exact language guide for Java, PHP/Laravel, JavaScript/JSX, TypeScript/TSX, Python, Go, or Rust targets. Discussion or read-only investigation alone does not require loading it."
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
- Load this whole skill with `read`, without `offset` or `limit`, before the first `write` or `edit` in each agent run. Use the absolute skill path shown in Available Skills. For each supported target language, also fully load its exact guide from `references/languages/`.
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 reads of the required packaged paths succeed; partial reads and same-named files do not count.
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. Read this complete file from its packaged path before any mutation.
40
- 2. Fully read the matching packaged language guide, if the target has a supported extension.
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 checks. Rules 11–19 require review judgment.
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 checks. Rules 11–19 require review judgment.
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.