Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/component-refactoring-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,12 @@ The rule implements a comprehensive validation process:
### Tools used

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

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

Expand Down
21 changes: 15 additions & 6 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,17 @@ Because contracts track every public-facing facet of a component, any refactor t
Rules taking care about the contract building during the workflow, but if you need to build it "manually" say in the chat:

```
build_component_contract(<component-file.ts>, dsComponentName)
build_component_contract(saveLocation, templateFile, styleFile, typescriptFile, dsComponentName)
```

> 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
> `.cursor/tmp/contracts/<ds-component-kebab>/`.
> Replace the parameters with:
> - `saveLocation`: Path where to save the contract file (supports absolute and relative paths)
> - `templateFile`: Path to the component template file (.html or .ts for inline)
> - `styleFile`: Path to the component style file (.scss, .css, etc.)
> - `typescriptFile`: Path to the TypeScript component file (.ts)
> - `dsComponentName`: Optional design system component name (e.g., `DsBadge`)
>
> The tool analyses the template, TypeScript, and styles, then saves the contract to your specified location.

## When to Build a Contract

Expand Down Expand Up @@ -160,10 +166,13 @@ What happens when QA finds a bug or a reviewer requests changes **after** the in
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).
4. **Generate a diff** between the baseline and the latest contract:
```
User: diff_component_contract(<baseline>.contract.json, <latest>.contract.json, dsComponentName)
User: diff_component_contract(saveLocation, contractBeforePath, contractAfterPath, dsComponentName)
```
The diff file will land under
`.cursor/tmp/contracts/<ds-component-kebab>/diffs/`.
> Replace the parameters with:
> - `saveLocation`: Path where to save the diff result file (supports absolute and relative paths)
> - `contractBeforePath`: Path to the baseline contract file
> - `contractAfterPath`: Path to the latest contract file
> - `dsComponentName`: Optional design system component name
5. **Review the diff output using AI** – attach the diff and ask it to analyze it.
* If only intentional changes appear, proceed to merge / re-test.
* If unexpected API, DOM, or style changes surface, iterate on the fix and repeat steps 1-4.
Expand Down
6 changes: 3 additions & 3 deletions docs/ds-refactoring-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ This is your last chance to make changes before opening the pull request.
- Features: Automatic ESLint config resolution, comprehensive rule coverage

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

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

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

### Flow

Expand Down
6 changes: 4 additions & 2 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,22 @@ This document provides comprehensive guidance for AI agents working with Angular
**Purpose**: Creates static surface contracts for component templates and styles
**AI Usage**: Generate contracts before refactoring to track breaking changes
**Key Parameters**:
- `directory`: Component directory
- `saveLocation`: Path where to save the contract file (supports absolute and relative paths)
- `templateFile`: Template file name (.html or .ts for inline)
- `styleFile`: Style file name (.scss, .css, etc.)
- `typescriptFile`: TypeScript component file (.ts)
- `dsComponentName`: Optional design system component name
**Output**: Component contract file with API surface
**Best Practice**: Create contracts before major refactoring for comparison

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,36 @@ import {
} from '../../shared/utils/handler-helpers.js';
import { buildComponentContractSchema } from './models/schema.js';
import { buildComponentContract } from './utils/build-contract.js';
import {
saveContract,
generateContractSummary,
} from '../shared/utils/contract-file-ops.js';
import { generateContractSummary } from '../shared/utils/contract-file-ops.js';
import { ContractResult } from './models/types.js';
import { resolveCrossPlatformPath } from '../../shared/utils/cross-platform-path.js';

interface BuildComponentContractOptions extends BaseHandlerOptions {
directory: string;
saveLocation: string;
templateFile: string;
styleFile: string;
typescriptFile: string;
dsComponentName: string;
dsComponentName?: string;
}

export const buildComponentContractHandler = createHandler<
BuildComponentContractOptions,
ContractResult
>(
buildComponentContractSchema.name,
async (params, { cwd, workspaceRoot }) => {
async (params, { cwd, workspaceRoot: _workspaceRoot }) => {
const {
directory,
saveLocation,
templateFile,
styleFile,
typescriptFile,
dsComponentName,
dsComponentName = '',
} = params;

const effectiveTemplatePath = resolveCrossPlatformPath(
directory,
templateFile,
);
const effectiveScssPath = resolveCrossPlatformPath(directory, styleFile);
const effectiveTemplatePath = resolveCrossPlatformPath(cwd, templateFile);
const effectiveScssPath = resolveCrossPlatformPath(cwd, styleFile);
const effectiveTypescriptPath = resolveCrossPlatformPath(
directory,
cwd,
typescriptFile,
);

Expand All @@ -50,18 +44,41 @@ export const buildComponentContractHandler = createHandler<
effectiveTypescriptPath,
);

const { contractFilePath, hash } = await saveContract(
const contractString = JSON.stringify(contract, null, 2);
const hash = require('node:crypto')
.createHash('sha256')
.update(contractString)
.digest('hex');

const effectiveSaveLocation = resolveCrossPlatformPath(cwd, saveLocation);

const { mkdir, writeFile } = await import('node:fs/promises');
const { dirname } = await import('node:path');
await mkdir(dirname(effectiveSaveLocation), { recursive: true });

const contractData = {
contract,
workspaceRoot,
effectiveTemplatePath,
effectiveScssPath,
cwd,
dsComponentName,
hash: `sha256-${hash}`,
metadata: {
templatePath: effectiveTemplatePath,
scssPath: effectiveScssPath,
typescriptPath: effectiveTypescriptPath,
timestamp: new Date().toISOString(),
dsComponentName,
},
};

await writeFile(
effectiveSaveLocation,
JSON.stringify(contractData, null, 2),
'utf-8',
);

const contractFilePath = effectiveSaveLocation;

return {
contract,
hash,
hash: `sha256-${hash}`,
contractFilePath,
};
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { ToolSchemaOptions } from '@push-based/models';
import {
COMMON_ANNOTATIONS,
createProjectAnalysisSchema,
} from '../../../shared';
import { COMMON_ANNOTATIONS } from '../../../shared';

/**
* Schema for building component contracts
Expand All @@ -12,33 +9,35 @@ export const buildComponentContractSchema: ToolSchemaOptions = {
description:
"Generate a static surface contract for a component's template and SCSS.",
inputSchema: {
...createProjectAnalysisSchema({
type: 'object',
properties: {
saveLocation: {
type: 'string',
description:
'Path where to save the contract file. Supports both absolute and relative paths.',
},
templateFile: {
type: 'string',
description:
'File name of the component template file (.html) or TypeScript component file (.ts) for inline templates',
'Path to the component template file (.html) or TypeScript component file (.ts) for inline templates. Supports both absolute and relative paths.',
},
styleFile: {
type: 'string',
description:
'File name of the component style file (.scss, .sass, .less, .css)',
'Path to the component style file (.scss, .sass, .less, .css). Supports both absolute and relative paths.',
},
typescriptFile: {
type: 'string',
description: 'File name of the TypeScript component file (.ts)',
description:
'Path to the TypeScript component file (.ts). Supports both absolute and relative paths.',
},
dsComponentName: {
type: 'string',
description: 'The name of the design system component being used',
default: '',
},
}),
required: [
'directory',
'templateFile',
'styleFile',
'typescriptFile',
'dsComponentName',
],
},
required: ['saveLocation', 'templateFile', 'styleFile', 'typescriptFile'],
},
annotations: {
title: 'Build Component Contract',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import {
import { diffComponentContractSchema } from './models/schema.js';
import type { DomPathDictionary } from '../shared/models/types.js';
import { loadContract } from '../shared/utils/contract-file-ops.js';
import { componentNameToKebabCase } from '../../shared/utils/component-validation.js';
import { basename } from 'node:path';
import {
consolidateAndPruneRemoveOperationsWithDeduplication,
groupChangesByDomainAndType,
Expand All @@ -20,10 +18,10 @@ import { writeFile, mkdir } from 'node:fs/promises';
import diff from 'microdiff';

interface DiffComponentContractOptions extends BaseHandlerOptions {
directory: string;
saveLocation: string;
contractBeforePath: string;
contractAfterPath: string;
dsComponentName: string;
dsComponentName?: string;
}

export const diffComponentContractHandler = createHandler<
Expand All @@ -34,15 +32,19 @@ export const diffComponentContractHandler = createHandler<
}
>(
diffComponentContractSchema.name,
async (params, { workspaceRoot }) => {
async (params, { cwd, workspaceRoot }) => {
const {
saveLocation,
contractBeforePath,
contractAfterPath,
dsComponentName = '',
} = params;

const effectiveBeforePath = resolveCrossPlatformPath(
params.directory,
params.contractBeforePath,
);
const effectiveAfterPath = resolveCrossPlatformPath(
params.directory,
params.contractAfterPath,
cwd,
contractBeforePath,
);
const effectiveAfterPath = resolveCrossPlatformPath(cwd, contractAfterPath);

const contractBefore = await loadContract(effectiveBeforePath);
const contractAfter = await loadContract(effectiveAfterPath);
Expand All @@ -57,35 +59,21 @@ export const diffComponentContractHandler = createHandler<
const diffData = {
before: effectiveBeforePath,
after: effectiveAfterPath,
dsComponentName: params.dsComponentName,
dsComponentName,
timestamp: new Date().toISOString(),
domPathDictionary: domPathDict.paths,
changes: groupedChanges,
summary: generateDiffSummary(processedResult, groupedChanges),
};

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

// Create component-specific diffs directory
const componentKebab = componentNameToKebabCase(params.dsComponentName);
const diffDir = resolveCrossPlatformPath(
workspaceRoot,
`.cursor/tmp/contracts/${componentKebab}/diffs`,
);
await mkdir(diffDir, { recursive: true });
const effectiveSaveLocation = resolveCrossPlatformPath(cwd, saveLocation);

const { dirname } = await import('node:path');
await mkdir(dirname(effectiveSaveLocation), { recursive: true });

// Generate simplified diff filename: diff-{componentName}-{timestamp}.json
const componentBaseName = basename(
effectiveBeforePath,
'.contract.json',
).split('-')[0]; // Extract component name before timestamp
const timestamp = new Date()
.toISOString()
.replace(/[-:]/g, '')
.replace(/\.\d+Z$/, 'Z');
const diffFileName = `diff-${componentBaseName}-${timestamp}.json`;
const diffFilePath = resolveCrossPlatformPath(diffDir, diffFileName);
const diffFilePath = effectiveSaveLocation;

const formattedJson = JSON.stringify(normalizedDiffData, null, 2);
await writeFile(diffFilePath, formattedJson, 'utf-8');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { ToolSchemaOptions } from '@push-based/models';
import {
createProjectAnalysisSchema,
COMMON_ANNOTATIONS,
} from '../../../shared';
import { COMMON_ANNOTATIONS } from '../../../shared';

/**
* Schema for diffing component contracts
Expand All @@ -12,26 +9,30 @@ export const diffComponentContractSchema: ToolSchemaOptions = {
description:
'Compare before/after contracts for parity and surface breaking changes.',
inputSchema: {
...createProjectAnalysisSchema({
type: 'object',
properties: {
saveLocation: {
type: 'string',
description:
'Path where to save the diff result file. Supports both absolute and relative paths.',
},
contractBeforePath: {
type: 'string',
description: 'Path to the contract file before refactoring',
description:
'Path to the contract file before refactoring. Supports both absolute and relative paths.',
},
contractAfterPath: {
type: 'string',
description: 'Path to the contract file after refactoring',
description:
'Path to the contract file after refactoring. Supports both absolute and relative paths.',
},
dsComponentName: {
type: 'string',
description: 'The name of the design system component being used',
default: '',
},
}),
required: [
'directory',
'contractBeforePath',
'contractAfterPath',
'dsComponentName',
],
},
required: ['saveLocation', 'contractBeforePath', 'contractAfterPath'],
},
annotations: {
title: 'Diff Component Contract',
Expand Down