@rmrdeveloper/sideroom-pi 8.12.0 → 8.14.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 (32) hide show
  1. package/README.md +16 -0
  2. package/assets/visual-context/browser/INSTALL.md +69 -0
  3. package/assets/visual-context/browser/background.js +578 -0
  4. package/assets/visual-context/browser/content.js +198 -0
  5. package/assets/visual-context/browser/icons/README.md +9 -0
  6. package/assets/visual-context/browser/icons/icon-128.png +0 -0
  7. package/assets/visual-context/browser/icons/icon-16.png +0 -0
  8. package/assets/visual-context/browser/icons/icon-32.png +0 -0
  9. package/assets/visual-context/browser/icons/icon-48.png +0 -0
  10. package/assets/visual-context/browser/manifest.json +20 -0
  11. package/assets/visual-context/browser/popup.css +19 -0
  12. package/assets/visual-context/browser/popup.html +27 -0
  13. package/assets/visual-context/browser/popup.js +141 -0
  14. package/extensions/visual-context/bridge.ts +240 -0
  15. package/extensions/visual-context/broker.ts +292 -0
  16. package/extensions/visual-context/envelope.ts +34 -0
  17. package/extensions/visual-context/index.ts +328 -0
  18. package/extensions/visual-context/literal.ts +34 -0
  19. package/extensions/visual-context/markup.ts +104 -0
  20. package/extensions/visual-context/match.ts +222 -0
  21. package/extensions/visual-context/model.ts +180 -0
  22. package/extensions/visual-context/pairing.ts +114 -0
  23. package/extensions/visual-context/parse-astro.ts +166 -0
  24. package/extensions/visual-context/parse-html.ts +72 -0
  25. package/extensions/visual-context/parse-jsx.ts +157 -0
  26. package/extensions/visual-context/parse-svelte.ts +128 -0
  27. package/extensions/visual-context/parse-vue.ts +133 -0
  28. package/extensions/visual-context/session.ts +192 -0
  29. package/extensions/visual-context/source.ts +197 -0
  30. package/extensions/visual-context/ui.ts +57 -0
  31. package/package.json +16 -7
  32. package/skills/sideroom-codebase-design/SKILL.md +28 -1
@@ -0,0 +1,192 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from '@earendil-works/pi-coding-agent';
5
+ import { type Static, Type } from 'typebox';
6
+ import { Check } from 'typebox/value';
7
+ import {
8
+ addSelection,
9
+ assertSelectionCapacity,
10
+ MAX_CANDIDATES,
11
+ MAX_SELECTIONS,
12
+ type PendingSelection,
13
+ parseSelection,
14
+ SelectionSchema,
15
+ selectionContext,
16
+ VISUAL_SNAPSHOT,
17
+ type VisualSelection,
18
+ } from './model.ts';
19
+ import { candidateIsCurrent, resolveSelection } from './source.ts';
20
+
21
+ const CandidateSchema = Type.Object(
22
+ {
23
+ path: Type.String({
24
+ minLength: 1,
25
+ maxLength: 2_048,
26
+ pattern: '^[^\\x00-\\x1f\\x7f]*$',
27
+ }),
28
+ startLine: Type.Integer({ minimum: 1 }),
29
+ endLine: Type.Integer({ minimum: 1 }),
30
+ fingerprint: Type.String({ pattern: '^[a-f0-9]{64}$' }),
31
+ score: Type.Integer({ minimum: 0 }),
32
+ reasons: Type.Array(Type.String({ maxLength: 200 }), { maxItems: 40 }),
33
+ },
34
+ { additionalProperties: false },
35
+ );
36
+ const SnapshotSchema = Type.Object(
37
+ {
38
+ pending: Type.Array(
39
+ Type.Object(
40
+ {
41
+ selection: SelectionSchema,
42
+ resolution: Type.Object(
43
+ {
44
+ candidates: Type.Array(CandidateSchema, {
45
+ maxItems: MAX_CANDIDATES,
46
+ }),
47
+ truncated: Type.Boolean(),
48
+ capped: Type.Boolean(),
49
+ },
50
+ { additionalProperties: false },
51
+ ),
52
+ chosen: Type.Optional(CandidateSchema),
53
+ },
54
+ { additionalProperties: false },
55
+ ),
56
+ { maxItems: MAX_SELECTIONS },
57
+ ),
58
+ },
59
+ { additionalProperties: false },
60
+ );
61
+
62
+ export interface VisualStore {
63
+ pending: readonly PendingSelection[];
64
+ }
65
+
66
+ export function createVisualStore(): VisualStore {
67
+ return { pending: [] };
68
+ }
69
+
70
+ export function commitPending({
71
+ store,
72
+ pi,
73
+ pending,
74
+ }: {
75
+ readonly store: VisualStore;
76
+ readonly pi: Pick<ExtensionAPI, 'appendEntry'>;
77
+ readonly pending: readonly PendingSelection[];
78
+ }): void {
79
+ store.pending = pending;
80
+ pi.appendEntry(VISUAL_SNAPSHOT, { pending });
81
+ }
82
+
83
+ export function receiveSelection({
84
+ store,
85
+ pi,
86
+ cwd,
87
+ selection,
88
+ }: {
89
+ readonly store: VisualStore;
90
+ readonly pi: Pick<ExtensionAPI, 'appendEntry'>;
91
+ readonly cwd: string;
92
+ readonly selection: VisualSelection;
93
+ }): { readonly candidates: number } {
94
+ const existing = store.pending.find(
95
+ (entry) => entry.selection.id === selection.id,
96
+ );
97
+ if (existing) {
98
+ return { candidates: existing.resolution.candidates.length };
99
+ }
100
+ assertSelectionCapacity(store.pending);
101
+ const resolution = resolveSelection(cwd, selection);
102
+ commitPending({
103
+ store,
104
+ pi,
105
+ pending: addSelection(store.pending, { selection, resolution }),
106
+ });
107
+ return { candidates: resolution.candidates.length };
108
+ }
109
+
110
+ export function restorePending({
111
+ store,
112
+ ctx,
113
+ }: {
114
+ readonly store: VisualStore;
115
+ readonly ctx: Pick<ExtensionContext, 'sessionManager' | 'cwd'>;
116
+ }): void {
117
+ store.pending = [];
118
+ const branch = ctx.sessionManager.getBranch();
119
+ for (let index = branch.length - 1; index >= 0; index -= 1) {
120
+ const entry = branch[index];
121
+ if (entry?.type !== 'custom' || entry.customType !== VISUAL_SNAPSHOT) {
122
+ continue;
123
+ }
124
+ if (!Check(SnapshotSchema, entry.data)) {
125
+ return;
126
+ }
127
+ const snapshot: Static<typeof SnapshotSchema> = entry.data;
128
+ store.pending = snapshot.pending.flatMap((pending) => {
129
+ const candidates = pending.resolution.candidates.filter(
130
+ (candidate) =>
131
+ candidate.startLine <= candidate.endLine &&
132
+ candidateIsCurrent(ctx.cwd, candidate),
133
+ );
134
+ let selection: VisualSelection;
135
+ try {
136
+ selection = parseSelection(pending.selection);
137
+ } catch (cause) {
138
+ throw new Error(
139
+ 'Cannot restore browser context: invalid page address in the session.',
140
+ { cause },
141
+ );
142
+ }
143
+ const chosen = candidates.find(
144
+ (candidate) =>
145
+ candidate.path === pending.chosen?.path &&
146
+ candidate.startLine === pending.chosen.startLine &&
147
+ candidate.fingerprint === pending.chosen.fingerprint,
148
+ );
149
+ return [
150
+ {
151
+ selection,
152
+ resolution: { ...pending.resolution, candidates },
153
+ chosen,
154
+ },
155
+ ];
156
+ });
157
+ return;
158
+ }
159
+ }
160
+
161
+ export function prepareContext({
162
+ store,
163
+ cwd,
164
+ }: {
165
+ readonly store: VisualStore;
166
+ readonly cwd: string;
167
+ }):
168
+ | { readonly text: string; readonly selectionIds: readonly string[] }
169
+ | undefined {
170
+ if (store.pending.length === 0) {
171
+ return undefined;
172
+ }
173
+ const pending = store.pending.map((entry) => {
174
+ const candidates = entry.resolution.candidates.filter((candidate) =>
175
+ candidateIsCurrent(cwd, candidate),
176
+ );
177
+ const chosen =
178
+ entry.chosen && candidates.includes(entry.chosen)
179
+ ? entry.chosen
180
+ : undefined;
181
+ return {
182
+ ...entry,
183
+ resolution: { ...entry.resolution, candidates },
184
+ chosen,
185
+ };
186
+ });
187
+ const context = selectionContext(pending);
188
+ return {
189
+ text: context,
190
+ selectionIds: pending.map((entry) => entry.selection.id),
191
+ };
192
+ }
@@ -0,0 +1,197 @@
1
+ import { readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
2
+ import { extname, join, relative, sep } from 'node:path';
3
+ import ignore from 'ignore';
4
+ import { projectRelativePath } from '../shared/file-path.ts';
5
+ import { isMissingFile } from '../shared/read-file.ts';
6
+ import { InvalidMarkup } from './markup.ts';
7
+ import { matchSource, sourceFingerprint } from './match.ts';
8
+ import {
9
+ MAX_CANDIDATES,
10
+ type SourceCandidate,
11
+ type SourceResolution,
12
+ type VisualSelection,
13
+ } from './model.ts';
14
+
15
+ const SOURCE_EXTENSIONS = new Set([
16
+ '.html',
17
+ '.htm',
18
+ '.vue',
19
+ '.svelte',
20
+ '.astro',
21
+ '.jsx',
22
+ '.tsx',
23
+ ]);
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
+ const MAX_ENTRIES = 4_096;
33
+ const MAX_FILE_BYTES = 262_144;
34
+ const MAX_SOURCE_BYTES = 8_388_608;
35
+ const MAX_DEPTH = 16;
36
+ const SENSITIVE_NAME =
37
+ /secret|credential|password|private.?key|(?:^|[._-])env(?:[._-]|$)/iu;
38
+
39
+ export function resolveSelection(
40
+ cwd: string,
41
+ selection: VisualSelection,
42
+ ): SourceResolution {
43
+ const root = realpathSync(cwd);
44
+ const candidates: SourceCandidate[] = [];
45
+ let entriesRead = 0;
46
+ let sourceBytes = 0;
47
+ 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 {
57
+ if (depth > MAX_DEPTH) {
58
+ truncated = true;
59
+ return;
60
+ }
61
+ const localPatterns = [...patterns, ...ignorePatterns(directory, root)];
62
+ const matcher = ignore().add(localPatterns);
63
+ for (const entry of readdirSync(directory, { withFileTypes: true }).sort(
64
+ (left, right) => left.name.localeCompare(right.name),
65
+ )) {
66
+ entriesRead += 1;
67
+ if (entriesRead > MAX_ENTRIES || sourceBytes >= MAX_SOURCE_BYTES) {
68
+ truncated = true;
69
+ return;
70
+ }
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
+ ) {
80
+ continue;
81
+ }
82
+ const absolutePath = join(directory, entry.name);
83
+ const path = relative(root, absolutePath).split(sep).join('/');
84
+ if (matcher.ignores(entry.isDirectory() ? `${path}/` : path)) {
85
+ continue;
86
+ }
87
+ if (entry.isDirectory()) {
88
+ if (!SKIP_DIRECTORIES.has(entry.name)) {
89
+ walk({
90
+ directory: absolutePath,
91
+ patterns: localPatterns,
92
+ depth: depth + 1,
93
+ });
94
+ }
95
+ continue;
96
+ }
97
+ if (
98
+ !entry.isFile() ||
99
+ !SOURCE_EXTENSIONS.has(extname(entry.name).toLowerCase())
100
+ ) {
101
+ continue;
102
+ }
103
+ const size = statSync(absolutePath).size;
104
+ if (size > MAX_FILE_BYTES || sourceBytes + size > MAX_SOURCE_BYTES) {
105
+ truncated = true;
106
+ continue;
107
+ }
108
+ sourceBytes += size;
109
+ const source = readFileSync(absolutePath, 'utf8');
110
+ try {
111
+ candidates.push(...matchSource({ path, source, selection }));
112
+ } catch (cause) {
113
+ if (cause instanceof InvalidMarkup) {
114
+ truncated = true;
115
+ continue;
116
+ }
117
+ throw new Error(`Cannot inspect visual source ${path}`, { cause });
118
+ }
119
+ }
120
+ }
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,
127
+ );
128
+ return {
129
+ candidates: candidates.slice(0, MAX_CANDIDATES),
130
+ truncated,
131
+ capped: candidates.length > MAX_CANDIDATES,
132
+ };
133
+ }
134
+
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 });
147
+ }
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}`);
158
+ }
159
+ }
160
+ return patterns;
161
+ }
162
+
163
+ export function candidateIsCurrent(
164
+ cwd: string,
165
+ candidate: SourceCandidate,
166
+ ): 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;
183
+ }
184
+ throw new Error(`Cannot verify selected source ${candidate.path}`, {
185
+ cause,
186
+ });
187
+ }
188
+ }
189
+
190
+ // A path that turned into a file on the way reports ENOTDIR, which means the
191
+ // remembered source is gone rather than unreadable.
192
+ function isGone(cause: unknown): boolean {
193
+ return (
194
+ isMissingFile(cause) ||
195
+ (cause instanceof Error && 'code' in cause && cause.code === 'ENOTDIR')
196
+ );
197
+ }
@@ -0,0 +1,57 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';
3
+ import {
4
+ candidateLabel,
5
+ elementLabel,
6
+ type PendingSelection,
7
+ VISUAL_WIDGET,
8
+ } from './model.ts';
9
+
10
+ export const BROWSER_EXTENSION_DIRECTORY = fileURLToPath(
11
+ new URL('../../assets/visual-context/browser/', import.meta.url),
12
+ );
13
+
14
+ function pendingLabel({ resolution, chosen }: PendingSelection): string {
15
+ if (chosen) {
16
+ return `${candidateLabel(chosen)} (selected candidate)`;
17
+ }
18
+ const truncated = resolution.truncated ? ' · search truncated' : '';
19
+ const capped = resolution.capped ? ' · more candidates' : '';
20
+ return `${String(resolution.candidates.length)} candidates${truncated}${capped}`;
21
+ }
22
+
23
+ export function renderVisualWidget(
24
+ pending: readonly PendingSelection[],
25
+ theme: Theme,
26
+ ): readonly string[] {
27
+ return [
28
+ '',
29
+ theme.fg(
30
+ 'dim',
31
+ `Browser elements (${String(pending.length)}) · F7: review`,
32
+ ),
33
+ ...pending.map((entry) =>
34
+ theme.fg(
35
+ entry.chosen ? 'accent' : 'muted',
36
+ `${elementLabel(entry.selection)} — ${pendingLabel(entry)}`,
37
+ ),
38
+ ),
39
+ ];
40
+ }
41
+
42
+ export function refreshVisualWidget(
43
+ pending: readonly PendingSelection[],
44
+ ctx: Pick<ExtensionContext, 'ui' | 'mode'>,
45
+ ): void {
46
+ if (ctx.mode !== 'tui') {
47
+ return;
48
+ }
49
+ if (pending.length === 0) {
50
+ ctx.ui.setWidget(VISUAL_WIDGET, undefined);
51
+ return;
52
+ }
53
+ ctx.ui.setWidget(VISUAL_WIDGET, (_tui, theme) => ({
54
+ render: () => [...renderVisualWidget(pending, theme)],
55
+ invalidate() {},
56
+ }));
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmrdeveloper/sideroom-pi",
3
- "version": "8.12.0",
3
+ "version": "8.14.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": [
@@ -17,14 +17,15 @@
17
17
  "commitlint": "commitlint",
18
18
  "types": "tsc --noEmit",
19
19
  "test": "node --test 'extensions/**/*.test.ts' 'scripts/**/*.test.mjs'",
20
- "lint": "biome lint extensions scripts commitlint.config.mjs",
21
- "lint:fix": "biome lint --write extensions scripts commitlint.config.mjs",
22
- "format": "biome format --write extensions scripts package.json biome.json commitlint.config.mjs .changeset/config.json",
23
- "format:check": "biome format extensions scripts package.json biome.json commitlint.config.mjs .changeset/config.json",
20
+ "lint": "biome lint extensions assets/visual-context/browser scripts commitlint.config.mjs",
21
+ "lint:fix": "biome lint --write extensions assets/visual-context/browser scripts commitlint.config.mjs",
22
+ "format": "biome format --write extensions assets/visual-context/browser scripts package.json biome.json commitlint.config.mjs .changeset/config.json",
23
+ "format:check": "biome format extensions assets/visual-context/browser scripts package.json biome.json commitlint.config.mjs .changeset/config.json",
24
24
  "preview": "node scripts/render-preview.mjs",
25
25
  "cache:report": "node scripts/cache-report.mjs",
26
+ "package:browser": "node scripts/package-browser.mjs",
26
27
  "check:if-blocks": "node scripts/check-if-blocks.mjs",
27
- "check": "npm run types && npm run check:if-blocks && biome check extensions scripts package.json biome.json commitlint.config.mjs .changeset/config.json && npm test"
28
+ "check": "npm run types && npm run check:if-blocks && biome check extensions assets/visual-context/browser scripts package.json biome.json commitlint.config.mjs .changeset/config.json && npm test"
28
29
  },
29
30
  "engines": {
30
31
  "node": ">=22.19.0"
@@ -92,10 +93,18 @@
92
93
  "@earendil-works/pi-coding-agent": "0.87.1",
93
94
  "@earendil-works/pi-tui": "0.87.1",
94
95
  "@types/node": "^22.20.1",
96
+ "@types/ws": "^8.18.2",
97
+ "fflate": "0.8.3",
95
98
  "typebox": "1.3.7",
96
99
  "typescript": "^5.6.0"
97
100
  },
98
101
  "dependencies": {
99
- "ignore": "7.0.5"
102
+ "@astrojs/compiler": "^4.0.0",
103
+ "@babel/parser": "^7.29.9",
104
+ "@vue/compiler-dom": "^3.5.43",
105
+ "ignore": "7.0.5",
106
+ "parse5": "^8.0.1",
107
+ "svelte": "^5.57.2",
108
+ "ws": "^8.22.0"
100
109
  }
101
110
  }
@@ -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