Skip to content

Commit 1bbdb8e

Browse files
Merge pull request #17 from push-based/feature/add-save-locations
feat(angular-mcp-server): add save location for contracts
2 parents 577b171 + 18131d7 commit 1bbdb8e

File tree

8 files changed

+112
-96
lines changed

8 files changed

+112
-96
lines changed

docs/component-refactoring-flow.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,12 +234,12 @@ The rule implements a comprehensive validation process:
234234
### Tools used
235235

236236
- `build_component_contract` - Creates post-refactor component contract
237-
- Parameters: `componentFile`, `dsComponentName` (set to "AUTO")
237+
- Parameters: `saveLocation`, `templateFile`, `styleFile`, `typescriptFile`, `dsComponentName`
238238
- Returns: updated contract path with refactored component state
239239
- Purpose: Capture final component state for comparison
240240

241241
- `diff_component_contract` - Compares baseline and updated contracts
242-
- Parameters: `contractBeforePath`, `contractAfterPath`, `dsComponentName` (set to "AUTO")
242+
- Parameters: `saveLocation`, `contractBeforePath`, `contractAfterPath`, `dsComponentName`
243243
- Returns: detailed diff analysis showing specific changes
244244
- Purpose: Identify and analyze all modifications made during refactoring
245245

docs/contracts.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,17 @@ Because contracts track every public-facing facet of a component, any refactor t
6565
Rules taking care about the contract building during the workflow, but if you need to build it "manually" say in the chat:
6666

6767
```
68-
build_component_contract(<component-file.ts>, dsComponentName)
68+
build_component_contract(saveLocation, templateFile, styleFile, typescriptFile, dsComponentName)
6969
```
7070

71-
> Replace `<component-file.ts>` with the path to your component and set `dsComponentName` to the design-system component (e.g., `DsBadge`). The tool analyses the template, TypeScript, and styles, then saves a timestamped `*.contract.json` to
72-
> `.cursor/tmp/contracts/<ds-component-kebab>/`.
71+
> Replace the parameters with:
72+
> - `saveLocation`: Path where to save the contract file (supports absolute and relative paths)
73+
> - `templateFile`: Path to the component template file (.html or .ts for inline)
74+
> - `styleFile`: Path to the component style file (.scss, .css, etc.)
75+
> - `typescriptFile`: Path to the TypeScript component file (.ts)
76+
> - `dsComponentName`: Optional design system component name (e.g., `DsBadge`)
77+
>
78+
> The tool analyses the template, TypeScript, and styles, then saves the contract to your specified location.
7379
7480
## When to Build a Contract
7581

@@ -160,10 +166,13 @@ What happens when QA finds a bug or a reviewer requests changes **after** the in
160166
3. **Locate the original baseline contract** – this is the contract that was captured for the initial state (usually the very first timestamp in the folder).
161167
4. **Generate a diff** between the baseline and the latest contract:
162168
```
163-
User: diff_component_contract(<baseline>.contract.json, <latest>.contract.json, dsComponentName)
169+
User: diff_component_contract(saveLocation, contractBeforePath, contractAfterPath, dsComponentName)
164170
```
165-
The diff file will land under
166-
`.cursor/tmp/contracts/<ds-component-kebab>/diffs/`.
171+
> Replace the parameters with:
172+
> - `saveLocation`: Path where to save the diff result file (supports absolute and relative paths)
173+
> - `contractBeforePath`: Path to the baseline contract file
174+
> - `contractAfterPath`: Path to the latest contract file
175+
> - `dsComponentName`: Optional design system component name
167176
5. **Review the diff output using AI** – attach the diff and ask it to analyze it.
168177
* If only intentional changes appear, proceed to merge / re-test.
169178
* If unexpected API, DOM, or style changes surface, iterate on the fix and repeat steps 1-4.

docs/ds-refactoring-flow.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ This is your last chance to make changes before opening the pull request.
566566
- Features: Automatic ESLint config resolution, comprehensive rule coverage
567567

568568
- `build_component_contract` - Creates contracts for refactored components
569-
- Parameters: `directory`, `templateFile`, `styleFile`, `typescriptFile`, `dsComponentName`
569+
- Parameters: `saveLocation`, `templateFile`, `styleFile`, `typescriptFile`, `dsComponentName`
570570
- Returns: JSON contract with public API, DOM structure, and styles
571571
- Purpose: Capture post-refactoring component state
572572

@@ -576,9 +576,9 @@ This is your last chance to make changes before opening the pull request.
576576
- Purpose: Identify before/after contract pairs for comparison
577577

578578
- `diff_component_contract` - Compares component contracts
579-
- Parameters: `directory`, `contractBeforePath`, `contractAfterPath`, `dsComponentName`
579+
- Parameters: `saveLocation`, `contractBeforePath`, `contractAfterPath`, `dsComponentName`
580580
- Returns: Detailed diff highlighting changes in API, DOM, and styles
581-
- Saves: Diff files to `.cursor/tmp/contracts/<component>/diffs/`
581+
- Saves: Diff files to the specified saveLocation path
582582

583583
### Flow
584584

docs/tools.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,20 +113,22 @@ This document provides comprehensive guidance for AI agents working with Angular
113113
**Purpose**: Creates static surface contracts for component templates and styles
114114
**AI Usage**: Generate contracts before refactoring to track breaking changes
115115
**Key Parameters**:
116-
- `directory`: Component directory
116+
- `saveLocation`: Path where to save the contract file (supports absolute and relative paths)
117117
- `templateFile`: Template file name (.html or .ts for inline)
118118
- `styleFile`: Style file name (.scss, .css, etc.)
119119
- `typescriptFile`: TypeScript component file (.ts)
120+
- `dsComponentName`: Optional design system component name
120121
**Output**: Component contract file with API surface
121122
**Best Practice**: Create contracts before major refactoring for comparison
122123

123124
#### `diff_component_contract`
124125
**Purpose**: Compares before/after contracts to identify breaking changes
125126
**AI Usage**: Validate that refactoring doesn't introduce breaking changes
126127
**Key Parameters**:
127-
- `directory`: Component directory
128+
- `saveLocation`: Path where to save the diff result file (supports absolute and relative paths)
128129
- `contractBeforePath`: Path to pre-refactoring contract
129130
- `contractAfterPath`: Path to post-refactoring contract
131+
- `dsComponentName`: Optional design system component name
130132
**Output**: Diff analysis showing breaking changes
131133
**Best Practice**: Essential validation step after component modifications
132134

packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,36 @@ import {
44
} from '../../shared/utils/handler-helpers.js';
55
import { buildComponentContractSchema } from './models/schema.js';
66
import { buildComponentContract } from './utils/build-contract.js';
7-
import {
8-
saveContract,
9-
generateContractSummary,
10-
} from '../shared/utils/contract-file-ops.js';
7+
import { generateContractSummary } from '../shared/utils/contract-file-ops.js';
118
import { ContractResult } from './models/types.js';
129
import { resolveCrossPlatformPath } from '../../shared/utils/cross-platform-path.js';
1310

1411
interface BuildComponentContractOptions extends BaseHandlerOptions {
15-
directory: string;
12+
saveLocation: string;
1613
templateFile: string;
1714
styleFile: string;
1815
typescriptFile: string;
19-
dsComponentName: string;
16+
dsComponentName?: string;
2017
}
2118

2219
export const buildComponentContractHandler = createHandler<
2320
BuildComponentContractOptions,
2421
ContractResult
2522
>(
2623
buildComponentContractSchema.name,
27-
async (params, { cwd, workspaceRoot }) => {
24+
async (params, { cwd, workspaceRoot: _workspaceRoot }) => {
2825
const {
29-
directory,
26+
saveLocation,
3027
templateFile,
3128
styleFile,
3229
typescriptFile,
33-
dsComponentName,
30+
dsComponentName = '',
3431
} = params;
3532

36-
const effectiveTemplatePath = resolveCrossPlatformPath(
37-
directory,
38-
templateFile,
39-
);
40-
const effectiveScssPath = resolveCrossPlatformPath(directory, styleFile);
33+
const effectiveTemplatePath = resolveCrossPlatformPath(cwd, templateFile);
34+
const effectiveScssPath = resolveCrossPlatformPath(cwd, styleFile);
4135
const effectiveTypescriptPath = resolveCrossPlatformPath(
42-
directory,
36+
cwd,
4337
typescriptFile,
4438
);
4539

@@ -50,18 +44,41 @@ export const buildComponentContractHandler = createHandler<
5044
effectiveTypescriptPath,
5145
);
5246

53-
const { contractFilePath, hash } = await saveContract(
47+
const contractString = JSON.stringify(contract, null, 2);
48+
const hash = require('node:crypto')
49+
.createHash('sha256')
50+
.update(contractString)
51+
.digest('hex');
52+
53+
const effectiveSaveLocation = resolveCrossPlatformPath(cwd, saveLocation);
54+
55+
const { mkdir, writeFile } = await import('node:fs/promises');
56+
const { dirname } = await import('node:path');
57+
await mkdir(dirname(effectiveSaveLocation), { recursive: true });
58+
59+
const contractData = {
5460
contract,
55-
workspaceRoot,
56-
effectiveTemplatePath,
57-
effectiveScssPath,
58-
cwd,
59-
dsComponentName,
61+
hash: `sha256-${hash}`,
62+
metadata: {
63+
templatePath: effectiveTemplatePath,
64+
scssPath: effectiveScssPath,
65+
typescriptPath: effectiveTypescriptPath,
66+
timestamp: new Date().toISOString(),
67+
dsComponentName,
68+
},
69+
};
70+
71+
await writeFile(
72+
effectiveSaveLocation,
73+
JSON.stringify(contractData, null, 2),
74+
'utf-8',
6075
);
6176

77+
const contractFilePath = effectiveSaveLocation;
78+
6279
return {
6380
contract,
64-
hash,
81+
hash: `sha256-${hash}`,
6582
contractFilePath,
6683
};
6784
},

packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { ToolSchemaOptions } from '@push-based/models';
2-
import {
3-
COMMON_ANNOTATIONS,
4-
createProjectAnalysisSchema,
5-
} from '../../../shared';
2+
import { COMMON_ANNOTATIONS } from '../../../shared';
63

74
/**
85
* Schema for building component contracts
@@ -12,33 +9,35 @@ export const buildComponentContractSchema: ToolSchemaOptions = {
129
description:
1310
"Generate a static surface contract for a component's template and SCSS.",
1411
inputSchema: {
15-
...createProjectAnalysisSchema({
12+
type: 'object',
13+
properties: {
14+
saveLocation: {
15+
type: 'string',
16+
description:
17+
'Path where to save the contract file. Supports both absolute and relative paths.',
18+
},
1619
templateFile: {
1720
type: 'string',
1821
description:
19-
'File name of the component template file (.html) or TypeScript component file (.ts) for inline templates',
22+
'Path to the component template file (.html) or TypeScript component file (.ts) for inline templates. Supports both absolute and relative paths.',
2023
},
2124
styleFile: {
2225
type: 'string',
2326
description:
24-
'File name of the component style file (.scss, .sass, .less, .css)',
27+
'Path to the component style file (.scss, .sass, .less, .css). Supports both absolute and relative paths.',
2528
},
2629
typescriptFile: {
2730
type: 'string',
28-
description: 'File name of the TypeScript component file (.ts)',
31+
description:
32+
'Path to the TypeScript component file (.ts). Supports both absolute and relative paths.',
2933
},
3034
dsComponentName: {
3135
type: 'string',
3236
description: 'The name of the design system component being used',
37+
default: '',
3338
},
34-
}),
35-
required: [
36-
'directory',
37-
'templateFile',
38-
'styleFile',
39-
'typescriptFile',
40-
'dsComponentName',
41-
],
39+
},
40+
required: ['saveLocation', 'templateFile', 'styleFile', 'typescriptFile'],
4241
},
4342
annotations: {
4443
title: 'Build Component Contract',

packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/diff-component-contract.tool.ts

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import {
99
import { diffComponentContractSchema } from './models/schema.js';
1010
import type { DomPathDictionary } from '../shared/models/types.js';
1111
import { loadContract } from '../shared/utils/contract-file-ops.js';
12-
import { componentNameToKebabCase } from '../../shared/utils/component-validation.js';
13-
import { basename } from 'node:path';
1412
import {
1513
consolidateAndPruneRemoveOperationsWithDeduplication,
1614
groupChangesByDomainAndType,
@@ -20,10 +18,10 @@ import { writeFile, mkdir } from 'node:fs/promises';
2018
import diff from 'microdiff';
2119

2220
interface DiffComponentContractOptions extends BaseHandlerOptions {
23-
directory: string;
21+
saveLocation: string;
2422
contractBeforePath: string;
2523
contractAfterPath: string;
26-
dsComponentName: string;
24+
dsComponentName?: string;
2725
}
2826

2927
export const diffComponentContractHandler = createHandler<
@@ -34,15 +32,19 @@ export const diffComponentContractHandler = createHandler<
3432
}
3533
>(
3634
diffComponentContractSchema.name,
37-
async (params, { workspaceRoot }) => {
35+
async (params, { cwd, workspaceRoot }) => {
36+
const {
37+
saveLocation,
38+
contractBeforePath,
39+
contractAfterPath,
40+
dsComponentName = '',
41+
} = params;
42+
3843
const effectiveBeforePath = resolveCrossPlatformPath(
39-
params.directory,
40-
params.contractBeforePath,
41-
);
42-
const effectiveAfterPath = resolveCrossPlatformPath(
43-
params.directory,
44-
params.contractAfterPath,
44+
cwd,
45+
contractBeforePath,
4546
);
47+
const effectiveAfterPath = resolveCrossPlatformPath(cwd, contractAfterPath);
4648

4749
const contractBefore = await loadContract(effectiveBeforePath);
4850
const contractAfter = await loadContract(effectiveAfterPath);
@@ -57,35 +59,21 @@ export const diffComponentContractHandler = createHandler<
5759
const diffData = {
5860
before: effectiveBeforePath,
5961
after: effectiveAfterPath,
60-
dsComponentName: params.dsComponentName,
62+
dsComponentName,
6163
timestamp: new Date().toISOString(),
6264
domPathDictionary: domPathDict.paths,
6365
changes: groupedChanges,
6466
summary: generateDiffSummary(processedResult, groupedChanges),
6567
};
6668

67-
// Normalize absolute paths to relative paths for portability
6869
const normalizedDiffData = normalizePathsInObject(diffData, workspaceRoot);
6970

70-
// Create component-specific diffs directory
71-
const componentKebab = componentNameToKebabCase(params.dsComponentName);
72-
const diffDir = resolveCrossPlatformPath(
73-
workspaceRoot,
74-
`.cursor/tmp/contracts/${componentKebab}/diffs`,
75-
);
76-
await mkdir(diffDir, { recursive: true });
71+
const effectiveSaveLocation = resolveCrossPlatformPath(cwd, saveLocation);
72+
73+
const { dirname } = await import('node:path');
74+
await mkdir(dirname(effectiveSaveLocation), { recursive: true });
7775

78-
// Generate simplified diff filename: diff-{componentName}-{timestamp}.json
79-
const componentBaseName = basename(
80-
effectiveBeforePath,
81-
'.contract.json',
82-
).split('-')[0]; // Extract component name before timestamp
83-
const timestamp = new Date()
84-
.toISOString()
85-
.replace(/[-:]/g, '')
86-
.replace(/\.\d+Z$/, 'Z');
87-
const diffFileName = `diff-${componentBaseName}-${timestamp}.json`;
88-
const diffFilePath = resolveCrossPlatformPath(diffDir, diffFileName);
76+
const diffFilePath = effectiveSaveLocation;
8977

9078
const formattedJson = JSON.stringify(normalizedDiffData, null, 2);
9179
await writeFile(diffFilePath, formattedJson, 'utf-8');

packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/models/schema.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { ToolSchemaOptions } from '@push-based/models';
2-
import {
3-
createProjectAnalysisSchema,
4-
COMMON_ANNOTATIONS,
5-
} from '../../../shared';
2+
import { COMMON_ANNOTATIONS } from '../../../shared';
63

74
/**
85
* Schema for diffing component contracts
@@ -12,26 +9,30 @@ export const diffComponentContractSchema: ToolSchemaOptions = {
129
description:
1310
'Compare before/after contracts for parity and surface breaking changes.',
1411
inputSchema: {
15-
...createProjectAnalysisSchema({
12+
type: 'object',
13+
properties: {
14+
saveLocation: {
15+
type: 'string',
16+
description:
17+
'Path where to save the diff result file. Supports both absolute and relative paths.',
18+
},
1619
contractBeforePath: {
1720
type: 'string',
18-
description: 'Path to the contract file before refactoring',
21+
description:
22+
'Path to the contract file before refactoring. Supports both absolute and relative paths.',
1923
},
2024
contractAfterPath: {
2125
type: 'string',
22-
description: 'Path to the contract file after refactoring',
26+
description:
27+
'Path to the contract file after refactoring. Supports both absolute and relative paths.',
2328
},
2429
dsComponentName: {
2530
type: 'string',
2631
description: 'The name of the design system component being used',
32+
default: '',
2733
},
28-
}),
29-
required: [
30-
'directory',
31-
'contractBeforePath',
32-
'contractAfterPath',
33-
'dsComponentName',
34-
],
34+
},
35+
required: ['saveLocation', 'contractBeforePath', 'contractAfterPath'],
3536
},
3637
annotations: {
3738
title: 'Diff Component Contract',

0 commit comments

Comments
 (0)