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
2 changes: 0 additions & 2 deletions .claude-adr.log

This file was deleted.

10 changes: 7 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Environment variables override values from .eroderc.json.
# Use .eroderc.json for project settings and .env for secrets.

# AI provider: "gemini" (default) or "anthropic"
# AI provider: "gemini" (default), "openai", or "anthropic"
ERODE_AI_PROVIDER=gemini

# API keys (required for the selected provider)
ERODE_GEMINI_API_KEY=
ERODE_OPENAI_API_KEY=
ERODE_ANTHROPIC_API_KEY=

# GitHub / GitLab tokens
Expand All @@ -16,8 +17,10 @@ ERODE_GITHUB_TOKEN=
# Model overrides — FAST for extraction stages (1, 2), ADVANCED for analysis stages (3, 4)
# ERODE_GEMINI_FAST_MODEL=gemini-2.5-flash # Default
# ERODE_GEMINI_ADVANCED_MODEL=gemini-2.5-pro # Default
# ERODE_ANTHROPIC_FAST_MODEL=claude-haiku-4-5-20251001
# ERODE_ANTHROPIC_ADVANCED_MODEL=claude-sonnet-4-5-20250929
# ERODE_OPENAI_FAST_MODEL=gpt-5-mini # Default
# ERODE_OPENAI_ADVANCED_MODEL=gpt-5 # Default
# ERODE_ANTHROPIC_FAST_MODEL=claude-haiku-4-5
# ERODE_ANTHROPIC_ADVANCED_MODEL=claude-sonnet-4-6

# Architecture model format
# ERODE_MODEL_FORMAT=likec4
Expand All @@ -29,6 +32,7 @@ ERODE_GITHUB_TOKEN=

# Timeouts (ms)
# ERODE_GEMINI_TIMEOUT=60000
# ERODE_OPENAI_TIMEOUT=60000
# ERODE_ANTHROPIC_TIMEOUT=60000
# ERODE_GITHUB_TIMEOUT=30000

Expand Down
28 changes: 1 addition & 27 deletions .githooks/post-commit
Original file line number Diff line number Diff line change
@@ -1,29 +1,3 @@
#!/bin/sh

# Post-commit hook: Auto-generate ADR for architectural changes
# Runs Claude Code headlessly in background to analyze commits

COMMIT_MSG=$(git log -1 --pretty=%B)

# Opt-out checks
if echo "$COMMIT_MSG" | grep -qiE "\[(skip-adr|no-adr)\]"; then
exit 0
fi
[ "$SKIP_ADR" = "1" ] && exit 0

# Check for Claude CLI
command -v claude >/dev/null 2>&1 || exit 0

# Check for architectural indicators
if echo "$COMMIT_MSG" | grep -qiE "(refactor|architecture|migrate|introduce|domain layer|api version)"; then
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " Architectural change detected"
echo " Running ADR generation in background..."
echo " Check .claude-adr.log for output"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

nohup "$(dirname "$0")/adr-generator.sh" >> .claude-adr.log 2>&1 &
fi

echo "Post-commit checks passed!"
exit 0
8 changes: 4 additions & 4 deletions packages/core/schemas/eroderc.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,11 @@
"maximum": 300000
},
"fastModel": {
"default": "claude-haiku-4-5-20251001",
"default": "claude-haiku-4-5",
"type": "string"
},
"advancedModel": {
"default": "claude-sonnet-4-5-20250929",
"default": "claude-sonnet-4-6",
"type": "string"
Comment thread
parse marked this conversation as resolved.
}
},
Expand Down Expand Up @@ -210,11 +210,11 @@
"maximum": 300000
},
"fastModel": {
"default": "gpt-4.1-mini",
"default": "gpt-5-mini",
"type": "string"
},
"advancedModel": {
"default": "gpt-4.1",
"default": "gpt-5",
"type": "string"
}
},
Expand Down
60 changes: 60 additions & 0 deletions packages/core/src/analysis/__tests__/prompt-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,66 @@ describe('PromptBuilder', () => {
expect(result).toContain('REMOVED');
expect(result).toContain('memcached');
});

it('should preserve dependency evidence for drift analysis', () => {
const result = PromptBuilder.buildDriftAnalysisPrompt({
changeRequest: {
number: 1,
title: 'Add service dependency',
description: null,
repository: 'org/repo',
author: { login: 'dev' },
base: { ref: 'main', sha: 'base' },
head: { ref: 'feature', sha: 'head' },
stats: { commits: 1, additions: 10, deletions: 0, files_changed: 2 },
commits: [{ sha: 'head', message: 'Test', author: 'dev' }],
},
component: { id: 'api_gateway', name: 'API Gateway', type: 'service', tags: [] },
dependencies: {
dependencies: [
{
type: 'added',
file: 'packages/api-gateway/src/index.ts',
dependency: 'Order Service',
description: 'Existing component calls newly introduced service',
code: 'const ORDER_SERVICE = "http://order-service:3005";',
},
{
type: 'added',
file: 'packages/order-service/src/index.ts',
dependency: 'Product Service',
description: 'Newly introduced service calls existing component',
code: 'const PRODUCT_SERVICE = "http://product-service:3002";',
},
{
type: 'added',
file: 'packages/product-service/src/index.ts',
dependency: 'User Service',
description: 'Existing component calls existing component',
code: 'const USER_SERVICE = "http://user-service:3001";',
},
],
summary: 'Added service relationships',
},
architectural: { dependencies: [], dependents: [], relationships: [] },
allRelationships: [],
});

const vars = JSON.parse(result) as DriftAnalysisPromptVars;
expect(vars.dependencyChangesSection).toContain('Evidence:\n const ORDER_SERVICE');
expect(vars.dependencyChangesSection).toContain('Evidence:\n const PRODUCT_SERVICE');
expect(vars.dependencyChangesSection).toContain('Evidence:\n const USER_SERVICE');
});

it('should instruct drift analysis to account for every added dependency', () => {
const templateDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'prompts');
const template = readFileSync(join(templateDir, 'drift-analysis.md'), 'utf-8');

expect(template).toContain('For every ADDED dependency');
expect(template).toContain('Classify each dependency');
expect(template).toContain('New component plus relationship to add');
expect(template).toContain('Do not let one dependency that created a new component');
});
});

describe('buildModelPatchPrompt', () => {
Expand Down
43 changes: 43 additions & 0 deletions packages/core/src/analysis/__tests__/section-formatters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,49 @@ describe('section-formatters', () => {
expect(result).toContain('Added Redis');
});

it('should include code evidence when present', () => {
const result = formatDependencyChanges({
dependencies: [
{
type: 'added',
file: 'src/gateway.ts',
dependency: 'Order Service',
description: 'External order service via HTTP',
code: 'const ORDER_SERVICE = "http://order-service:3005";',
},
],
summary: '',
});

expect(result).toContain('Evidence:\n const ORDER_SERVICE');
expect(result).toContain('http://order-service:3005');
});

it('should keep multiline code evidence indented under the dependency bullet', () => {
const result = formatDependencyChanges({
dependencies: [
{
type: 'added',
file: 'src/gateway.ts',
dependency: 'Order Service',
description: 'External order service via HTTP',
code: 'const ORDER_SERVICE = "http://order-service:3005";\nawait fetch(ORDER_SERVICE);',
},
],
summary: '',
});

expect(result).toContain(
[
'- Order Service (src/gateway.ts)',
' External order service via HTTP',
' Evidence:',
' const ORDER_SERVICE = "http://order-service:3005";',
' await fetch(ORDER_SERVICE);',
].join('\n')
);
});

it('should format modified dependencies', () => {
const result = formatDependencyChanges({
dependencies: [
Expand Down
17 changes: 17 additions & 0 deletions packages/core/src/analysis/prompts/drift-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ These are ALL relationships currently declared in the architecture model:

Cross-reference the dependency changes above against the architecture model and assess:

### Dependency Coverage

For every ADDED dependency in the DEPENDENCY CHANGES DETECTED section, account for it
explicitly. Classify each dependency as one of:

- Already declared in the model
- New relationship to add
- New component plus relationship to add
- External package or third-party dependency that should not be modeled
- Ignored with a brief reason

If an existing modeled component gains a dependency on a newly introduced component,
include both the new component and that relationship. If a newly introduced component
depends on an existing modeled component, include both the new component and that
relationship. Do not let one dependency that created a new component hide other
relationships to or from that component.

### 1. New Dependencies NOT in Model (Potential Drift)

- Are there new dependencies that aren't in the allowed dependencies list?
Expand Down
11 changes: 10 additions & 1 deletion packages/core/src/analysis/section-formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,16 @@ export function formatDependencyChanges(dependencies: DependencyExtractionResult
if (items && items.length > 0) {
section += `**${label} Dependencies:**\n`;
section += items
.map((dep) => `- ${dep.dependency} (${dep.file})\n ${dep.description}`)
.map((dep) => {
const trimmedCode = dep.code.trim();
const evidence = trimmedCode
? `\n Evidence:\n${trimmedCode
.split('\n')
.map((line) => ` ${line}`)
.join('\n')}`
: '';
return `- ${dep.dependency} (${dep.file})\n ${dep.description}${evidence}`;
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
.join('\n');
section += '\n\n';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest';
import { AnalysisPhase } from '../analysis-phase.js';
import {
getGenerationProfileForModelPatch,
getGenerationProfileForPhase,
resolveOutputTokenLimit,
} from '../generation-profile.js';

describe('getGenerationProfileForPhase', () => {
it('uses small low-effort generation for simple phases', () => {
expect(getGenerationProfileForPhase(AnalysisPhase.COMPONENT_RESOLUTION)).toEqual({
outputSize: 'small',
reasoningEffort: 'low',
});
expect(getGenerationProfileForPhase(AnalysisPhase.DEPENDENCY_SCAN)).toEqual({
outputSize: 'small',
reasoningEffort: 'low',
});
});

it('uses large low-effort generation for drift analysis', () => {
expect(getGenerationProfileForPhase(AnalysisPhase.CHANGE_ANALYSIS)).toEqual({
outputSize: 'large',
reasoningEffort: 'low',
});
});

it('uses medium medium-effort generation for model updates', () => {
expect(getGenerationProfileForPhase(AnalysisPhase.MODEL_UPDATE)).toEqual({
outputSize: 'medium',
reasoningEffort: 'medium',
});
});

it('adds a dynamic output content hint for model patches', () => {
const profile = getGenerationProfileForModelPatch('x'.repeat(40_000), [' comp.a -> comp.b']);

expect(profile).toMatchObject({
outputSize: 'medium',
reasoningEffort: 'medium',
});
expect(profile.outputContentHint?.characters).toBeGreaterThan(16_384);
});

it('resolves output token limits from profile size and content hints', () => {
expect(
resolveOutputTokenLimit(
{ outputSize: 'medium', outputContentHint: { characters: 40_000 } },
{ small: 600, medium: 1500, large: 3000 }
)
).toBe(10_000);

expect(
resolveOutputTokenLimit({ outputSize: 'medium' }, { small: 600, medium: 1500, large: 3000 })
).toBe(1500);
});
});
12 changes: 6 additions & 6 deletions packages/core/src/providers/__tests__/provider-factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ const {
},
openai: {
apiKey: 'test-openai-key',
fastModel: 'gpt-4.1-mini',
advancedModel: 'gpt-4.1',
fastModel: 'gpt-5-mini',
advancedModel: 'gpt-5',
},
anthropic: {
apiKey: 'test-anthropic-key',
Expand Down Expand Up @@ -94,8 +94,8 @@ describe('createAIProvider', () => {
mockConfig.gemini.fastModel = 'gemini-flash';
mockConfig.gemini.advancedModel = 'gemini-pro';
mockConfig.openai.apiKey = 'test-openai-key';
mockConfig.openai.fastModel = 'gpt-4.1-mini';
mockConfig.openai.advancedModel = 'gpt-4.1';
mockConfig.openai.fastModel = 'gpt-5-mini';
mockConfig.openai.advancedModel = 'gpt-5';
mockConfig.anthropic.apiKey = 'test-anthropic-key';
mockConfig.anthropic.fastModel = 'claude-haiku';
mockConfig.anthropic.advancedModel = 'claude-sonnet';
Expand All @@ -122,8 +122,8 @@ describe('createAIProvider', () => {
expect(result).toBe(mockOpenAIInstance);
expect(OpenAIProvider).toHaveBeenCalledWith({
apiKey: 'test-openai-key',
fastModel: 'gpt-4.1-mini',
advancedModel: 'gpt-4.1',
fastModel: 'gpt-5-mini',
advancedModel: 'gpt-5',
});
});

Expand Down
Loading
Loading