substrate-ai 0.6.0 → 0.6.1

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.
@@ -0,0 +1,376 @@
1
+ /**
2
+ * Unit tests for buildStackAwareDevNotes (Story 37-7)
3
+ *
4
+ * AC1: TypeScript/Node.js Single Project
5
+ * AC2: Go Single Project
6
+ * AC3: JVM (Gradle or Maven) Single Project
7
+ * AC4: Rust/Cargo Single Project
8
+ * AC5: Python Single Project
9
+ * AC6: Turborepo Monorepo
10
+ * AC7: No Profile / Backward Compatibility
11
+ */
12
+
13
+ import { describe, it, expect } from 'vitest'
14
+ import {
15
+ buildStackAwareDevNotes,
16
+ DEV_WORKFLOW_START_MARKER,
17
+ DEV_WORKFLOW_END_MARKER,
18
+ } from '../build-dev-notes.js'
19
+ import type { ProjectProfile } from '../../../modules/project-profile/types.js'
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Helpers
23
+ // ---------------------------------------------------------------------------
24
+
25
+ function makeNodeProfile(buildCommand: string, testCommand = 'npm test'): ProjectProfile {
26
+ return {
27
+ project: {
28
+ type: 'single',
29
+ language: 'typescript',
30
+ buildTool: 'npm',
31
+ buildCommand,
32
+ testCommand,
33
+ },
34
+ }
35
+ }
36
+
37
+ function makeGoProfile(): ProjectProfile {
38
+ return {
39
+ project: {
40
+ type: 'single',
41
+ language: 'go',
42
+ buildTool: 'go',
43
+ buildCommand: 'go build ./...',
44
+ testCommand: 'go test ./...',
45
+ },
46
+ }
47
+ }
48
+
49
+ function makeGradleProfile(): ProjectProfile {
50
+ return {
51
+ project: {
52
+ type: 'single',
53
+ language: 'java',
54
+ buildTool: 'gradle',
55
+ buildCommand: './gradlew build',
56
+ testCommand: './gradlew test',
57
+ },
58
+ }
59
+ }
60
+
61
+ function makeMavenProfile(): ProjectProfile {
62
+ return {
63
+ project: {
64
+ type: 'single',
65
+ language: 'java',
66
+ buildTool: 'maven',
67
+ buildCommand: 'mvn compile',
68
+ testCommand: 'mvn test',
69
+ },
70
+ }
71
+ }
72
+
73
+ function makeCargoProfile(): ProjectProfile {
74
+ return {
75
+ project: {
76
+ type: 'single',
77
+ language: 'rust',
78
+ buildTool: 'cargo',
79
+ buildCommand: 'cargo build',
80
+ testCommand: 'cargo test',
81
+ },
82
+ }
83
+ }
84
+
85
+ function makePythonPoetryProfile(): ProjectProfile {
86
+ return {
87
+ project: {
88
+ type: 'single',
89
+ language: 'python',
90
+ buildTool: 'poetry',
91
+ buildCommand: 'poetry install',
92
+ testCommand: 'pytest',
93
+ },
94
+ }
95
+ }
96
+
97
+ function makePythonPipProfile(): ProjectProfile {
98
+ return {
99
+ project: {
100
+ type: 'single',
101
+ language: 'python',
102
+ buildTool: 'pip',
103
+ buildCommand: 'pip install -e .',
104
+ testCommand: 'pytest',
105
+ },
106
+ }
107
+ }
108
+
109
+ function makeTurborepoProfile(): ProjectProfile {
110
+ return {
111
+ project: {
112
+ type: 'monorepo',
113
+ tool: 'turborepo',
114
+ buildCommand: 'turbo build',
115
+ testCommand: 'turbo test',
116
+ packages: [
117
+ {
118
+ path: 'apps/web',
119
+ language: 'typescript',
120
+ buildTool: 'pnpm',
121
+ framework: 'nextjs',
122
+ testCommand: 'pnpm test',
123
+ },
124
+ {
125
+ path: 'apps/lock-service',
126
+ language: 'go',
127
+ buildTool: 'go',
128
+ },
129
+ {
130
+ path: 'apps/pricing-worker',
131
+ language: 'typescript',
132
+ buildTool: 'pnpm',
133
+ framework: 'node',
134
+ testCommand: 'pnpm test',
135
+ },
136
+ ],
137
+ },
138
+ }
139
+ }
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // AC7: null profile — empty string
143
+ // ---------------------------------------------------------------------------
144
+
145
+ describe('AC7: null profile returns empty string', () => {
146
+ it('returns empty string for null profile', () => {
147
+ expect(buildStackAwareDevNotes(null)).toBe('')
148
+ })
149
+
150
+ it('returns empty string for undefined (coerced to null)', () => {
151
+ // The function signature is ProjectProfile | null, but test defensive cast
152
+ expect(buildStackAwareDevNotes(null)).toBe('')
153
+ })
154
+ })
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Marker contract
158
+ // ---------------------------------------------------------------------------
159
+
160
+ describe('Markers: non-null profile always includes start/end markers', () => {
161
+ it('wraps output in dev-workflow markers for Go profile', () => {
162
+ const result = buildStackAwareDevNotes(makeGoProfile())
163
+ expect(result).toContain(DEV_WORKFLOW_START_MARKER)
164
+ expect(result).toContain(DEV_WORKFLOW_END_MARKER)
165
+ const startIdx = result.indexOf(DEV_WORKFLOW_START_MARKER)
166
+ const endIdx = result.indexOf(DEV_WORKFLOW_END_MARKER)
167
+ expect(startIdx).toBeLessThan(endIdx)
168
+ })
169
+ })
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // AC1: TypeScript/Node.js
173
+ // ---------------------------------------------------------------------------
174
+
175
+ describe('AC1: TypeScript/Node.js profiles', () => {
176
+ it('npm build command → includes npm run build and npm test', () => {
177
+ const result = buildStackAwareDevNotes(makeNodeProfile('npm run build'))
178
+ // Use backtick-wrapped strings for precise matching (avoids false substring matches)
179
+ expect(result).toContain('`npm run build`')
180
+ expect(result).toContain('`npm test`')
181
+ expect(result).not.toContain('pnpm run')
182
+ expect(result).not.toContain('yarn build')
183
+ })
184
+
185
+ it('pnpm build command → includes pnpm run build and pnpm test', () => {
186
+ const result = buildStackAwareDevNotes(makeNodeProfile('pnpm run build', 'pnpm test'))
187
+ // Use backtick-wrapped strings for precise matching
188
+ expect(result).toContain('`pnpm run build`')
189
+ expect(result).toContain('`pnpm test`')
190
+ // Note: 'pnpm run build' contains 'npm run build' as a substring (p+npm run build),
191
+ // so we check for the backtick-delimited command instead
192
+ expect(result).not.toContain('`npm run build`')
193
+ })
194
+
195
+ it('yarn build command → includes yarn build and yarn test', () => {
196
+ const result = buildStackAwareDevNotes(makeNodeProfile('yarn build', 'yarn test'))
197
+ expect(result).toContain('yarn build')
198
+ expect(result).toContain('yarn test')
199
+ })
200
+
201
+ it('bun build command → includes bun run build and bun test', () => {
202
+ const result = buildStackAwareDevNotes(makeNodeProfile('bun run build', 'bun test'))
203
+ expect(result).toContain('bun run build')
204
+ expect(result).toContain('bun test')
205
+ })
206
+
207
+ it('fallback to npm when buildCommand does not hint a package manager', () => {
208
+ const result = buildStackAwareDevNotes(makeNodeProfile('node build.js'))
209
+ expect(result).toContain('npm run build')
210
+ expect(result).toContain('npm test')
211
+ })
212
+ })
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // AC2: Go
216
+ // ---------------------------------------------------------------------------
217
+
218
+ describe('AC2: Go single project', () => {
219
+ it('includes go build ./...', () => {
220
+ const result = buildStackAwareDevNotes(makeGoProfile())
221
+ expect(result).toContain('go build ./...')
222
+ })
223
+
224
+ it('includes go test ./...', () => {
225
+ const result = buildStackAwareDevNotes(makeGoProfile())
226
+ expect(result).toContain('go test ./...')
227
+ })
228
+
229
+ it('includes -run flag note for targeted test execution', () => {
230
+ const result = buildStackAwareDevNotes(makeGoProfile())
231
+ expect(result).toContain('-run')
232
+ })
233
+
234
+ it('includes -v flag note', () => {
235
+ const result = buildStackAwareDevNotes(makeGoProfile())
236
+ expect(result).toContain('-v')
237
+ })
238
+ })
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // AC3: JVM — Gradle and Maven
242
+ // ---------------------------------------------------------------------------
243
+
244
+ describe('AC3: Gradle single project', () => {
245
+ it('includes ./gradlew build', () => {
246
+ const result = buildStackAwareDevNotes(makeGradleProfile())
247
+ expect(result).toContain('./gradlew build')
248
+ })
249
+
250
+ it('includes ./gradlew test', () => {
251
+ const result = buildStackAwareDevNotes(makeGradleProfile())
252
+ expect(result).toContain('./gradlew test')
253
+ })
254
+
255
+ it('includes --tests note for targeted execution', () => {
256
+ const result = buildStackAwareDevNotes(makeGradleProfile())
257
+ expect(result).toContain('--tests')
258
+ })
259
+ })
260
+
261
+ describe('AC3: Maven single project', () => {
262
+ it('includes mvn compile', () => {
263
+ const result = buildStackAwareDevNotes(makeMavenProfile())
264
+ expect(result).toContain('mvn compile')
265
+ })
266
+
267
+ it('includes mvn test', () => {
268
+ const result = buildStackAwareDevNotes(makeMavenProfile())
269
+ expect(result).toContain('mvn test')
270
+ })
271
+
272
+ it('includes -Dtest note for targeted execution', () => {
273
+ const result = buildStackAwareDevNotes(makeMavenProfile())
274
+ expect(result).toContain('-Dtest')
275
+ })
276
+ })
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // AC4: Rust/Cargo
280
+ // ---------------------------------------------------------------------------
281
+
282
+ describe('AC4: Rust/Cargo single project', () => {
283
+ it('includes cargo build', () => {
284
+ const result = buildStackAwareDevNotes(makeCargoProfile())
285
+ expect(result).toContain('cargo build')
286
+ })
287
+
288
+ it('includes cargo test', () => {
289
+ const result = buildStackAwareDevNotes(makeCargoProfile())
290
+ expect(result).toContain('cargo test')
291
+ })
292
+
293
+ it('includes --nocapture note', () => {
294
+ const result = buildStackAwareDevNotes(makeCargoProfile())
295
+ expect(result).toContain('--nocapture')
296
+ })
297
+ })
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // AC5: Python
301
+ // ---------------------------------------------------------------------------
302
+
303
+ describe('AC5: Python single project', () => {
304
+ it('poetry profile → includes poetry install and pytest', () => {
305
+ const result = buildStackAwareDevNotes(makePythonPoetryProfile())
306
+ expect(result).toContain('poetry install')
307
+ expect(result).toContain('pytest')
308
+ })
309
+
310
+ it('pip profile → includes pip install and pytest', () => {
311
+ const result = buildStackAwareDevNotes(makePythonPipProfile())
312
+ expect(result).toContain('pip install')
313
+ expect(result).toContain('pytest')
314
+ })
315
+ })
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // AC6: Turborepo Monorepo
319
+ // ---------------------------------------------------------------------------
320
+
321
+ describe('AC6: Turborepo monorepo', () => {
322
+ it('includes root turbo build command', () => {
323
+ const result = buildStackAwareDevNotes(makeTurborepoProfile())
324
+ expect(result).toContain('turbo build')
325
+ })
326
+
327
+ it('includes root turbo test command', () => {
328
+ const result = buildStackAwareDevNotes(makeTurborepoProfile())
329
+ expect(result).toContain('turbo test')
330
+ })
331
+
332
+ it('includes package table with path column', () => {
333
+ const result = buildStackAwareDevNotes(makeTurborepoProfile())
334
+ expect(result).toContain('apps/web')
335
+ expect(result).toContain('apps/lock-service')
336
+ expect(result).toContain('apps/pricing-worker')
337
+ })
338
+
339
+ it('includes package table with language column', () => {
340
+ const result = buildStackAwareDevNotes(makeTurborepoProfile())
341
+ expect(result).toContain('typescript')
342
+ expect(result).toContain('go')
343
+ })
344
+
345
+ it('uses — for missing framework', () => {
346
+ const result = buildStackAwareDevNotes(makeTurborepoProfile())
347
+ // lock-service has no framework
348
+ expect(result).toContain('—')
349
+ })
350
+
351
+ it('uses package testCommand when provided', () => {
352
+ const result = buildStackAwareDevNotes(makeTurborepoProfile())
353
+ expect(result).toContain('pnpm test')
354
+ })
355
+
356
+ it('falls back to stack default test command when testCommand is absent', () => {
357
+ const result = buildStackAwareDevNotes(makeTurborepoProfile())
358
+ // lock-service is Go with no testCommand, should default to go test ./...
359
+ expect(result).toContain('go test ./...')
360
+ })
361
+
362
+ it('monorepo without packages still renders root commands', () => {
363
+ const profile: ProjectProfile = {
364
+ project: {
365
+ type: 'monorepo',
366
+ tool: 'turborepo',
367
+ buildCommand: 'turbo build',
368
+ testCommand: 'turbo test',
369
+ packages: [],
370
+ },
371
+ }
372
+ const result = buildStackAwareDevNotes(profile)
373
+ expect(result).toContain('turbo build')
374
+ expect(result).toContain('turbo test')
375
+ })
376
+ })
@@ -0,0 +1,256 @@
1
+ /**
2
+ * build-dev-notes.ts — Stack-aware dev workflow section generator for CLAUDE.md.
3
+ *
4
+ * Generates a human-readable "Dev Workflow" section for CLAUDE.md based on
5
+ * a project's detected profile. This is a pure function — no filesystem I/O,
6
+ * no YAML parsing, no async. Accepts a `ProjectProfile | null` and returns
7
+ * a `string`.
8
+ *
9
+ * Wrapped in `<!-- dev-workflow:start -->` / `<!-- dev-workflow:end -->` markers
10
+ * for idempotent re-runs (analogous to the substrate section markers).
11
+ */
12
+
13
+ import type { ProjectProfile, PackageEntry } from '../../modules/project-profile/types.js'
14
+
15
+ export const DEV_WORKFLOW_START_MARKER = '<!-- dev-workflow:start -->'
16
+ export const DEV_WORKFLOW_END_MARKER = '<!-- dev-workflow:end -->'
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Package manager detection helpers
20
+ // ---------------------------------------------------------------------------
21
+
22
+ type PackageManager = 'pnpm' | 'yarn' | 'bun' | 'npm'
23
+
24
+ function detectPackageManager(buildCommand: string): PackageManager {
25
+ if (buildCommand.includes('pnpm')) return 'pnpm'
26
+ if (buildCommand.includes('yarn')) return 'yarn'
27
+ if (buildCommand.includes('bun')) return 'bun'
28
+ return 'npm'
29
+ }
30
+
31
+ function getBuildCmd(pm: PackageManager): string {
32
+ switch (pm) {
33
+ case 'pnpm':
34
+ return 'pnpm run build'
35
+ case 'yarn':
36
+ return 'yarn build'
37
+ case 'bun':
38
+ return 'bun run build'
39
+ default:
40
+ return 'npm run build'
41
+ }
42
+ }
43
+
44
+ function getTestCmd(pm: PackageManager): string {
45
+ switch (pm) {
46
+ case 'pnpm':
47
+ return 'pnpm test'
48
+ case 'yarn':
49
+ return 'yarn test'
50
+ case 'bun':
51
+ return 'bun test'
52
+ default:
53
+ return 'npm test'
54
+ }
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Per-stack default test command (used in monorepo package table fallback)
59
+ // ---------------------------------------------------------------------------
60
+
61
+ function stackDefaultTestCommand(pkg: PackageEntry): string {
62
+ if (pkg.testCommand) return pkg.testCommand
63
+ switch (pkg.language) {
64
+ case 'go':
65
+ return 'go test ./...'
66
+ case 'rust':
67
+ return 'cargo test'
68
+ case 'java':
69
+ case 'kotlin':
70
+ if (pkg.buildTool === 'maven') return 'mvn test'
71
+ return './gradlew test'
72
+ case 'python':
73
+ return 'pytest'
74
+ default: {
75
+ // TypeScript/JavaScript — infer package manager from buildTool or buildCommand
76
+ const buildToolPm = pkg.buildTool as PackageManager | undefined
77
+ if (buildToolPm === 'pnpm') return 'pnpm test'
78
+ if (buildToolPm === 'yarn') return 'yarn test'
79
+ if (buildToolPm === 'bun') return 'bun test'
80
+ return 'npm test'
81
+ }
82
+ }
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Per-stack section builders
87
+ // ---------------------------------------------------------------------------
88
+
89
+ function buildNodeSection(buildCommand: string): string {
90
+ const pm = detectPackageManager(buildCommand)
91
+ const buildCmd = getBuildCmd(pm)
92
+ const testCmd = getTestCmd(pm)
93
+
94
+ return [
95
+ '## Dev Workflow',
96
+ '',
97
+ '**Build:** `' + buildCmd + '`',
98
+ '**Test:** `' + testCmd + '`',
99
+ '',
100
+ '### Testing Notes',
101
+ '- Run targeted tests during development to avoid slow feedback loops',
102
+ '- Run the full suite before merging',
103
+ ].join('\n')
104
+ }
105
+
106
+ function buildGoSection(): string {
107
+ return [
108
+ '## Dev Workflow',
109
+ '',
110
+ '**Build:** `go build ./...`',
111
+ '**Test:** `go test ./...`',
112
+ '',
113
+ '### Testing Notes',
114
+ '- Run targeted tests: `go test ./pkg/... -v -run TestFunctionName`',
115
+ '- Run with short flag to skip long-running tests: `go test ./... -short`',
116
+ '- Verbose output: `go test ./... -v`',
117
+ ].join('\n')
118
+ }
119
+
120
+ function buildGradleSection(): string {
121
+ return [
122
+ '## Dev Workflow',
123
+ '',
124
+ '**Build:** `./gradlew build`',
125
+ '**Test:** `./gradlew test`',
126
+ '',
127
+ '### Testing Notes',
128
+ '- Run a specific test class: `./gradlew test --tests "com.example.ClassName"`',
129
+ '- Run a specific method: `./gradlew test --tests "com.example.ClassName.methodName"`',
130
+ ].join('\n')
131
+ }
132
+
133
+ function buildMavenSection(): string {
134
+ return [
135
+ '## Dev Workflow',
136
+ '',
137
+ '**Build:** `mvn compile`',
138
+ '**Test:** `mvn test`',
139
+ '',
140
+ '### Testing Notes',
141
+ '- Run a specific test class: `mvn test -Dtest=ClassName`',
142
+ '- Run a specific method: `mvn test -Dtest="ClassName#methodName"`',
143
+ ].join('\n')
144
+ }
145
+
146
+ function buildCargoSection(): string {
147
+ return [
148
+ '## Dev Workflow',
149
+ '',
150
+ '**Build:** `cargo build`',
151
+ '**Test:** `cargo test`',
152
+ '',
153
+ '### Testing Notes',
154
+ '- Show test output: `cargo test -- --nocapture`',
155
+ '- Run a specific test: `cargo test test_function_name`',
156
+ '- Run tests in a module: `cargo test --lib test_module`',
157
+ ].join('\n')
158
+ }
159
+
160
+ function buildPythonSection(buildCommand: string): string {
161
+ // Derive install command from buildCommand
162
+ let installCmd: string
163
+ if (buildCommand.includes('poetry')) {
164
+ installCmd = 'poetry install'
165
+ } else {
166
+ installCmd = 'pip install -e .'
167
+ }
168
+
169
+ return [
170
+ '## Dev Workflow',
171
+ '',
172
+ '**Install:** `' + installCmd + '`',
173
+ '**Test:** `pytest -v`',
174
+ '',
175
+ '### Testing Notes',
176
+ '- Run targeted tests: `pytest -k "test_name" -v`',
177
+ '- Run a specific file and test: `pytest tests/test_foo.py::test_bar -v`',
178
+ ].join('\n')
179
+ }
180
+
181
+ function buildMonorepoSection(profile: ProjectProfile): string {
182
+ const { project } = profile
183
+ const lines: string[] = [
184
+ '## Dev Workflow',
185
+ '',
186
+ `**Root build:** \`${project.buildCommand}\``,
187
+ `**Root test:** \`${project.testCommand}\``,
188
+ ]
189
+
190
+ const packages = project.packages ?? []
191
+ if (packages.length > 0) {
192
+ lines.push('')
193
+ lines.push('### Package Structure')
194
+ lines.push('')
195
+ lines.push('| Package | Language | Framework | Test Command |')
196
+ lines.push('|---------|----------|-----------|--------------|')
197
+ for (const pkg of packages) {
198
+ const lang = pkg.language
199
+ const framework = pkg.framework ?? '—'
200
+ const testCmd = stackDefaultTestCommand(pkg)
201
+ lines.push(`| ${pkg.path} | ${lang} | ${framework} | ${testCmd} |`)
202
+ }
203
+ }
204
+
205
+ return lines.join('\n')
206
+ }
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Public API
210
+ // ---------------------------------------------------------------------------
211
+
212
+ /**
213
+ * Generates a stack-aware "Dev Workflow" section for inclusion in CLAUDE.md.
214
+ *
215
+ * Returns an empty string when `profile` is null (backward-compatible — the
216
+ * caller should skip prepending the dev workflow block in that case).
217
+ *
218
+ * When a profile is present, returns a string wrapped in
219
+ * `<!-- dev-workflow:start -->` / `<!-- dev-workflow:end -->` markers.
220
+ */
221
+ export function buildStackAwareDevNotes(profile: ProjectProfile | null): string {
222
+ if (!profile) return ''
223
+
224
+ const { project } = profile
225
+
226
+ let body: string
227
+
228
+ if (project.type === 'monorepo') {
229
+ body = buildMonorepoSection(profile)
230
+ } else {
231
+ // Single-stack project — dispatch on language / buildTool
232
+ const buildTool = project.buildTool
233
+ const language = project.language
234
+
235
+ if (buildTool === 'go' || language === 'go') {
236
+ body = buildGoSection()
237
+ } else if (buildTool === 'gradle') {
238
+ body = buildGradleSection()
239
+ } else if (buildTool === 'maven') {
240
+ body = buildMavenSection()
241
+ } else if (buildTool === 'cargo' || language === 'rust') {
242
+ body = buildCargoSection()
243
+ } else if (language === 'python') {
244
+ body = buildPythonSection(project.buildCommand)
245
+ } else {
246
+ // TypeScript / JavaScript / default Node.js
247
+ body = buildNodeSection(project.buildCommand)
248
+ }
249
+ }
250
+
251
+ return [
252
+ DEV_WORKFLOW_START_MARKER,
253
+ body,
254
+ DEV_WORKFLOW_END_MARKER,
255
+ ].join('\n')
256
+ }
@@ -1,4 +1,4 @@
1
- import { registerRunCommand, runRunAction } from "./run-B1WEe6SY.js";
1
+ import { registerRunCommand, runRunAction } from "./run-IDOmPys1.js";
2
2
  import "./logger-D2fS2ccL.js";
3
3
  import "./config-migrator-DtZW1maj.js";
4
4
  import "./helpers-BihqWgVe.js";