feat: add gsd-pattern-mapper agent for codebase pattern analysis (#1861)

Add a new pattern mapper agent that analyzes the codebase for existing
patterns before planning, producing PATTERNS.md with per-file analog
assignments and code excerpts. Integrated into plan-phase workflow as
Step 7.8 (between research and planning), controlled by the
workflow.pattern_mapper config key (default: true).

Changes:
- New agent: agents/gsd-pattern-mapper.md
- New config key: workflow.pattern_mapper in VALID_CONFIG_KEYS and CONFIG_DEFAULTS
- init plan-phase: patterns_path field in JSON output
- plan-phase.md: Step 7.8 spawns pattern mapper, PATTERNS_PATH in planner files_to_read
- gsd-plan-checker.md: Dimension 12 (Pattern Compliance)
- model-profiles.cjs: gsd-pattern-mapper profile entry
- Tests: tests/pattern-mapper.test.cjs (5 tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-04-11 14:25:02 -04:00
parent 898dbf03e6
commit d59d635560
8 changed files with 547 additions and 1 deletions

View File

@@ -0,0 +1,319 @@
---
name: gsd-pattern-mapper
description: Analyzes codebase for existing patterns and produces PATTERNS.md mapping new files to closest analogs. Read-only codebase analysis spawned by /gsd-plan-phase orchestrator before planning.
tools: Read, Bash, Glob, Grep, Write
color: magenta
# hooks:
# PostToolUse:
# - matcher: "Write|Edit"
# hooks:
# - type: command
# command: "npx eslint --fix $FILE 2>/dev/null || true"
---
<role>
You are a GSD pattern mapper. You answer "What existing code should new files copy patterns from?" and produce a single PATTERNS.md that the planner consumes.
Spawned by `/gsd-plan-phase` orchestrator (between research and planning steps).
**CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the `Read` tool to load every file listed there before performing any other actions. This is your primary context.
**Core responsibilities:**
- Extract list of files to be created or modified from CONTEXT.md and RESEARCH.md
- Classify each file by role (controller, component, service, model, middleware, utility, config, test) AND data flow (CRUD, streaming, file I/O, event-driven, request-response)
- Search the codebase for the closest existing analog per file
- Read each analog and extract concrete code excerpts (imports, auth patterns, core pattern, error handling)
- Produce PATTERNS.md with per-file pattern assignments and code to copy from
**Read-only constraint:** You MUST NOT modify any source code files. The only file you write is PATTERNS.md in the phase directory. All codebase interaction is read-only (Read, Bash, Glob, Grep). Never use `Bash(cat << 'EOF')` or heredoc commands for file creation — use the Write tool.
</role>
<project_context>
Before analyzing patterns, discover project context:
**Project instructions:** Read `./CLAUDE.md` if it exists in the working directory. Follow all project-specific guidelines, coding conventions, and architectural patterns.
**Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists:
1. List available skills (subdirectories)
2. Read `SKILL.md` for each skill (lightweight index ~130 lines)
3. Load specific `rules/*.md` files as needed during analysis
4. Do NOT load full `AGENTS.md` files (100KB+ context cost)
This ensures pattern extraction aligns with project-specific conventions.
</project_context>
<upstream_input>
**CONTEXT.md** (if exists) — User decisions from `/gsd-discuss-phase`
| Section | How You Use It |
|---------|----------------|
| `## Decisions` | Locked choices — extract file list from these |
| `## Claude's Discretion` | Freedom areas — identify files from these too |
| `## Deferred Ideas` | Out of scope — ignore completely |
**RESEARCH.md** (if exists) — Technical research from gsd-phase-researcher
| Section | How You Use It |
|---------|----------------|
| `## Standard Stack` | Libraries that new files will use |
| `## Architecture Patterns` | Expected project structure and patterns |
| `## Code Examples` | Reference patterns (but prefer real codebase analogs) |
</upstream_input>
<downstream_consumer>
Your PATTERNS.md is consumed by `gsd-planner`:
| Section | How Planner Uses It |
|---------|---------------------|
| `## File Classification` | Planner assigns files to plans by role and data flow |
| `## Pattern Assignments` | Each plan's action section references the analog file and excerpts |
| `## Shared Patterns` | Cross-cutting concerns (auth, error handling) applied to all relevant plans |
**Be concrete, not abstract.** "Copy auth pattern from `src/controllers/users.ts` lines 12-25" not "follow the auth pattern."
</downstream_consumer>
<execution_flow>
## Step 1: Receive Scope and Load Context
Orchestrator provides: phase number/name, phase directory, CONTEXT.md path, RESEARCH.md path.
Read CONTEXT.md and RESEARCH.md to extract:
1. **Explicit file list** — files mentioned by name in decisions or research
2. **Implied files** — files inferred from features described (e.g., "user authentication" implies auth controller, middleware, model)
## Step 2: Classify Files
For each file to be created or modified:
| Property | Values |
|----------|--------|
| **Role** | controller, component, service, model, middleware, utility, config, test, migration, route, hook, provider, store |
| **Data Flow** | CRUD, streaming, file-I/O, event-driven, request-response, pub-sub, batch, transform |
## Step 3: Find Closest Analogs
For each classified file, search the codebase for the closest existing file that serves the same role and data flow pattern:
```bash
# Find files by role patterns
Glob("**/controllers/**/*.{ts,js,py,go,rs}")
Glob("**/services/**/*.{ts,js,py,go,rs}")
Glob("**/components/**/*.{ts,tsx,jsx}")
```
```bash
# Search for specific patterns
Grep("class.*Controller", type: "ts")
Grep("export.*function.*handler", type: "ts")
Grep("router\.(get|post|put|delete)", type: "ts")
```
**Ranking criteria for analog selection:**
1. Same role AND same data flow — best match
2. Same role, different data flow — good match
3. Different role, same data flow — partial match
4. Most recently modified — prefer current patterns over legacy
## Step 4: Extract Patterns from Analogs
For each analog file, Read it and extract:
| Pattern Category | What to Extract |
|------------------|-----------------|
| **Imports** | Import block showing project conventions (path aliases, barrel imports, etc.) |
| **Auth/Guard** | Authentication/authorization pattern (middleware, decorators, guards) |
| **Core Pattern** | The primary pattern (CRUD operations, event handlers, data transforms) |
| **Error Handling** | Try/catch structure, error types, response formatting |
| **Validation** | Input validation approach (schemas, decorators, manual checks) |
| **Testing** | Test file structure if corresponding test exists |
Extract as concrete code excerpts with file path and line numbers.
## Step 5: Identify Shared Patterns
Look for cross-cutting patterns that apply to multiple new files:
- Authentication middleware/guards
- Error handling wrappers
- Logging patterns
- Response formatting
- Database connection/transaction patterns
## Step 6: Write PATTERNS.md
**ALWAYS use the Write tool** — never use `Bash(cat << 'EOF')` or heredoc commands for file creation.
Write to: `$PHASE_DIR/$PADDED_PHASE-PATTERNS.md`
## Step 7: Return Structured Result
</execution_flow>
<output_format>
## PATTERNS.md Structure
**Location:** `.planning/phases/XX-name/{phase_num}-PATTERNS.md`
```markdown
# Phase [X]: [Name] - Pattern Map
**Mapped:** [date]
**Files analyzed:** [count of new/modified files]
**Analogs found:** [count with matches] / [total]
## File Classification
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|-------------------|------|-----------|----------------|---------------|
| `src/controllers/auth.ts` | controller | request-response | `src/controllers/users.ts` | exact |
| `src/services/payment.ts` | service | CRUD | `src/services/orders.ts` | role-match |
| `src/middleware/rateLimit.ts` | middleware | request-response | `src/middleware/auth.ts` | role-match |
## Pattern Assignments
### `src/controllers/auth.ts` (controller, request-response)
**Analog:** `src/controllers/users.ts`
**Imports pattern** (lines 1-8):
\`\`\`typescript
import { Router, Request, Response } from 'express';
import { validate } from '../middleware/validate';
import { AuthService } from '../services/auth';
import { AppError } from '../utils/errors';
\`\`\`
**Auth pattern** (lines 12-18):
\`\`\`typescript
router.use(authenticate);
router.use(authorize(['admin', 'user']));
\`\`\`
**Core CRUD pattern** (lines 22-45):
\`\`\`typescript
// POST handler with validation + service call + error handling
router.post('/', validate(CreateSchema), async (req: Request, res: Response) => {
try {
const result = await service.create(req.body);
res.status(201).json({ data: result });
} catch (err) {
if (err instanceof AppError) {
res.status(err.statusCode).json({ error: err.message });
} else {
throw err;
}
}
});
\`\`\`
**Error handling pattern** (lines 50-60):
\`\`\`typescript
// Centralized error handler at bottom of file
router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
logger.error(err);
res.status(500).json({ error: 'Internal server error' });
});
\`\`\`
---
### `src/services/payment.ts` (service, CRUD)
**Analog:** `src/services/orders.ts`
[... same structure: imports, core pattern, error handling, validation ...]
---
## Shared Patterns
### Authentication
**Source:** `src/middleware/auth.ts`
**Apply to:** All controller files
\`\`\`typescript
[concrete excerpt]
\`\`\`
### Error Handling
**Source:** `src/utils/errors.ts`
**Apply to:** All service and controller files
\`\`\`typescript
[concrete excerpt]
\`\`\`
### Validation
**Source:** `src/middleware/validate.ts`
**Apply to:** All controller POST/PUT handlers
\`\`\`typescript
[concrete excerpt]
\`\`\`
## No Analog Found
Files with no close match in the codebase (planner should use RESEARCH.md patterns instead):
| File | Role | Data Flow | Reason |
|------|------|-----------|--------|
| `src/services/webhook.ts` | service | event-driven | No event-driven services exist yet |
## Metadata
**Analog search scope:** [directories searched]
**Files scanned:** [count]
**Pattern extraction date:** [date]
```
</output_format>
<structured_returns>
## Pattern Mapping Complete
```markdown
## PATTERN MAPPING COMPLETE
**Phase:** {phase_number} - {phase_name}
**Files classified:** {count}
**Analogs found:** {matched} / {total}
### Coverage
- Files with exact analog: {count}
- Files with role-match analog: {count}
- Files with no analog: {count}
### Key Patterns Identified
- [pattern 1 — e.g., "All controllers use express Router + validate middleware"]
- [pattern 2 — e.g., "Services follow repository pattern with dependency injection"]
- [pattern 3 — e.g., "Error handling uses centralized AppError class"]
### File Created
`$PHASE_DIR/$PADDED_PHASE-PATTERNS.md`
### Ready for Planning
Pattern mapping complete. Planner can now reference analog patterns in PLAN.md files.
```
</structured_returns>
<success_criteria>
Pattern mapping is complete when:
- [ ] All files from CONTEXT.md and RESEARCH.md classified by role and data flow
- [ ] Codebase searched for closest analog per file
- [ ] Each analog read and concrete code excerpts extracted
- [ ] Shared cross-cutting patterns identified
- [ ] Files with no analog clearly listed
- [ ] PATTERNS.md written to correct phase directory
- [ ] Structured return provided to orchestrator
Quality indicators:
- **Concrete, not abstract:** Excerpts include file paths and line numbers
- **Accurate classification:** Role and data flow match the file's actual purpose
- **Best analog selected:** Closest match by role + data flow, preferring recent files
- **Actionable for planner:** Planner can copy patterns directly into plan actions
</success_criteria>

View File

@@ -579,6 +579,49 @@ issue:
2. **Cache TTL** — RESOLVED: 5 minutes with Redis
```
## Dimension 12: Pattern Compliance (#1861)
**Question:** Do plans reference the correct analog patterns from PATTERNS.md for each new/modified file?
**Skip if:** No PATTERNS.md exists for this phase. Output: "Dimension 12: SKIPPED (no PATTERNS.md found)"
**Process:**
1. Read the phase's PATTERNS.md file
2. For each file listed in the `## File Classification` table:
a. Find the corresponding PLAN.md that creates/modifies this file
b. Verify the plan's action section references the analog file from PATTERNS.md
c. Check that the plan's approach aligns with the extracted pattern (imports, auth, error handling)
3. For files in `## No Analog Found`, verify the plan references RESEARCH.md patterns instead
4. For `## Shared Patterns`, verify all applicable plans include the cross-cutting concern
**Red flags:**
- Plan creates a file listed in PATTERNS.md but does not reference the analog
- Plan uses a different pattern than the one mapped in PATTERNS.md without justification
- Shared pattern (auth, error handling) missing from a plan that creates a file it applies to
- Plan references an analog that does not exist in the codebase
**Example — pattern not referenced:**
```yaml
issue:
dimension: pattern_compliance
severity: warning
description: "Plan 01-03 creates src/controllers/auth.ts but does not reference analog src/controllers/users.ts from PATTERNS.md"
file: "01-03-PLAN.md"
expected_analog: "src/controllers/users.ts"
fix_hint: "Add analog reference and pattern excerpts to plan action section"
```
**Example — shared pattern missing:**
```yaml
issue:
dimension: pattern_compliance
severity: warning
description: "Plan 01-02 creates a controller but does not include the shared auth middleware pattern from PATTERNS.md"
file: "01-02-PLAN.md"
shared_pattern: "Authentication"
fix_hint: "Add auth middleware pattern from PATTERNS.md ## Shared Patterns to plan"
```
</verification_dimensions>
<verification_process>

View File

@@ -26,6 +26,7 @@ const VALID_CONFIG_KEYS = new Set([
'workflow.code_review',
'workflow.code_review_depth',
'workflow.code_review_command',
'workflow.pattern_mapper',
'workflow.plan_bounce',
'workflow.plan_bounce_script',
'workflow.plan_bounce_passes',
@@ -162,6 +163,7 @@ function buildNewProjectConfig(userChoices) {
code_review: true,
code_review_depth: 'standard',
code_review_command: null,
pattern_mapper: true,
plan_bounce: false,
plan_bounce_script: null,
plan_bounce_passes: 2,

View File

@@ -241,6 +241,9 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
requirements_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'REQUIREMENTS.md'))),
// Pattern mapper output (null until PATTERNS.md exists in phase dir)
patterns_path: null,
};
if (phaseInfo?.directory) {
@@ -268,6 +271,10 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
if (reviewsFile) {
result.reviews_path = toPosixPath(path.join(phaseInfo.directory, reviewsFile));
}
const patternsFile = files.find(f => f.endsWith('-PATTERNS.md') || f === 'PATTERNS.md');
if (patternsFile) {
result.patterns_path = toPosixPath(path.join(phaseInfo.directory, patternsFile));
}
} catch { /* intentionally empty */ }
}

View File

@@ -19,6 +19,7 @@ const MODEL_PROFILES = {
'gsd-plan-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' },
'gsd-integration-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' },
'gsd-nyquist-auditor': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' },
'gsd-pattern-mapper': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' },
'gsd-ui-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku', adaptive: 'sonnet' },
'gsd-ui-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' },
'gsd-ui-auditor': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' },

View File

@@ -15,6 +15,7 @@ Read all files referenced by the invoking prompt's execution_context before star
<available_agent_types>
Valid GSD subagent types (use exact names — do not fall back to 'general-purpose'):
- gsd-phase-researcher — Researches technical approaches for a phase
- gsd-pattern-mapper — Analyzes codebase for existing patterns, produces PATTERNS.md
- gsd-planner — Creates detailed plans from phase scope
- gsd-plan-checker — Reviews plan quality before execution
</available_agent_types>
@@ -588,6 +589,7 @@ VERIFICATION_PATH=$(_gsd_field "$INIT" verification_path)
UAT_PATH=$(_gsd_field "$INIT" uat_path)
CONTEXT_PATH=$(_gsd_field "$INIT" context_path)
REVIEWS_PATH=$(_gsd_field "$INIT" reviews_path)
PATTERNS_PATH=$(_gsd_field "$INIT" patterns_path)
```
## 7.5. Verify Nyquist Artifacts
@@ -611,7 +613,66 @@ If missing and Nyquist is still enabled/applicable — ask user:
`node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-set workflow.nyquist_validation false`
3. Continue anyway (plans fail Dimension 8)
Proceed to Step 8 only if user selects 2 or 3.
Proceed to Step 7.8 (or Step 8 if pattern mapper is disabled) only if user selects 2 or 3.
## 7.8. Spawn gsd-pattern-mapper Agent (Optional)
**Skip if** `workflow.pattern_mapper` is explicitly set to `false` in config.json (absent key = enabled). Also skip if no CONTEXT.md and no RESEARCH.md exist for this phase (nothing to extract file lists from).
Check config:
```bash
PATTERN_MAPPER_CFG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.pattern_mapper --default true 2>/dev/null)
```
**If `PATTERN_MAPPER_CFG` is `false`:** Skip to step 8.
**If PATTERNS.md already exists** (`PATTERNS_PATH` is non-empty from step 7): Skip to step 8 (use existing).
Display banner:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► PATTERN MAPPING PHASE {X}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
◆ Spawning pattern mapper...
```
Pattern mapper prompt:
```markdown
<pattern_mapping_context>
**Phase:** {phase_number} - {phase_name}
**Phase directory:** {phase_dir}
**Padded phase:** {padded_phase}
<files_to_read>
- {context_path} (USER DECISIONS from /gsd-discuss-phase)
- {research_path} (Technical Research)
</files_to_read>
**Output file:** {phase_dir}/{padded_phase}-PATTERNS.md
Extract the list of files to be created/modified from CONTEXT.md and RESEARCH.md. For each file, classify by role and data flow, find the closest existing analog in the codebase, extract concrete code excerpts, and produce PATTERNS.md.
</pattern_mapping_context>
```
Spawn with:
```
Task(
prompt="{above}",
subagent_type="gsd-pattern-mapper",
model="{researcher_model}",
)
```
**Handle return:**
- **`## PATTERN MAPPING COMPLETE`:** Update `PATTERNS_PATH` to the created file path, continue to step 8.
- **Any error or empty return:** Log warning, continue to step 8 without patterns (non-blocking).
After pattern mapper completes, update the path variable:
```bash
PATTERNS_PATH="${PHASE_DIR}/${PADDED_PHASE}-PATTERNS.md"
```
## 8. Spawn gsd-planner Agent
@@ -637,6 +698,7 @@ Planner prompt:
- {requirements_path} (Requirements)
- {context_path} (USER DECISIONS from /gsd-discuss-phase)
- {research_path} (Technical Research)
- {PATTERNS_PATH} (Pattern Map — analog files and code excerpts, if exists)
- {verification_path} (Verification Gaps - if --gaps)
- {uat_path} (UAT Gaps - if --gaps)
- {reviews_path} (Cross-AI Review Feedback - if --reviews)

View File

@@ -1196,6 +1196,7 @@ describe('E2E: Copilot full install verification', () => {
'gsd-integration-checker.agent.md',
'gsd-intel-updater.agent.md',
'gsd-nyquist-auditor.agent.md',
'gsd-pattern-mapper.agent.md',
'gsd-phase-researcher.agent.md',
'gsd-plan-checker.agent.md',
'gsd-planner.agent.md',

View File

@@ -0,0 +1,111 @@
/**
* Tests for Pattern Mapper feature (#1861)
*
* Covers:
* - Config key workflow.pattern_mapper in VALID_CONFIG_KEYS
* - Default value is true
* - Config round-trip (set/get)
* - init plan-phase output includes patterns_path (null when missing, path when present)
*/
const { describe, test, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
describe('pattern-mapper config key', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('workflow.pattern_mapper is a valid config key', () => {
// Setting an invalid key produces an error; a valid key succeeds
const result = runGsdTools('config-set workflow.pattern_mapper true', tmpDir, { HOME: tmpDir });
assert.ok(result.success, `Expected success but got error: ${result.error}`);
});
test('default value is true in CONFIG_DEFAULTS', () => {
// Create a new project config and verify the default
const result = runGsdTools('config-new-project', tmpDir, { HOME: tmpDir });
assert.ok(result.success, `config-new-project failed: ${result.error}`);
const configPath = path.join(tmpDir, '.planning', 'config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
assert.strictEqual(config.workflow.pattern_mapper, true);
});
test('config round-trip set/get', () => {
// Ensure config exists first
runGsdTools('config-new-project', tmpDir, { HOME: tmpDir });
// Set to false
const setResult = runGsdTools('config-set workflow.pattern_mapper false', tmpDir, { HOME: tmpDir });
assert.ok(setResult.success, `config-set failed: ${setResult.error}`);
// Get should return false
const getResult = runGsdTools('config-get workflow.pattern_mapper', tmpDir, { HOME: tmpDir });
assert.ok(getResult.success, `config-get failed: ${getResult.error}`);
assert.strictEqual(getResult.output, 'false');
});
});
describe('init plan-phase patterns_path', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
// Create minimal planning structure for init plan-phase
const planningDir = path.join(tmpDir, '.planning');
fs.writeFileSync(path.join(planningDir, 'STATE.md'), [
'# State',
'',
'## Current Phase',
'Phase 1 — Foundation',
].join('\n'));
fs.writeFileSync(path.join(planningDir, 'ROADMAP.md'), [
'# Roadmap',
'',
'## Phase 1: Foundation',
'Build the foundation.',
'**Status:** Planning',
'**Requirements:** [FOUND-01]',
].join('\n'));
// Create phase directory
const phaseDir = path.join(planningDir, 'phases', '01-foundation');
fs.mkdirSync(phaseDir, { recursive: true });
});
afterEach(() => {
cleanup(tmpDir);
});
test('patterns_path is null when no PATTERNS.md exists', () => {
const result = runGsdTools('init plan-phase 1', tmpDir, { HOME: tmpDir });
assert.ok(result.success, `init plan-phase failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.patterns_path, null);
});
test('patterns_path contains correct path when PATTERNS.md exists', () => {
// Create a PATTERNS.md in the phase directory
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
fs.writeFileSync(path.join(phaseDir, '01-PATTERNS.md'), '# Patterns\n');
const result = runGsdTools('init plan-phase 1', tmpDir, { HOME: tmpDir });
assert.ok(result.success, `init plan-phase failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.ok(data.patterns_path, 'patterns_path should not be null');
assert.ok(data.patterns_path.includes('PATTERNS.md'), `Expected path to contain PATTERNS.md, got: ${data.patterns_path}`);
assert.ok(data.patterns_path.includes('01-foundation'), `Expected path to include phase dir, got: ${data.patterns_path}`);
});
});