edsger 0.45.1 → 0.47.0
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.
- package/.claude/settings.local.json +3 -23
- package/dist/api/__tests__/app-store.test.d.ts +7 -0
- package/dist/api/__tests__/app-store.test.js +60 -0
- package/dist/api/__tests__/intelligence.test.d.ts +11 -0
- package/dist/api/__tests__/intelligence.test.js +315 -0
- package/dist/api/features/__tests__/feature-utils.test.d.ts +4 -0
- package/dist/api/features/__tests__/feature-utils.test.js +370 -0
- package/dist/api/features/__tests__/status-updater.test.d.ts +4 -0
- package/dist/api/features/__tests__/status-updater.test.js +88 -0
- package/dist/commands/build/__tests__/build.test.d.ts +5 -0
- package/dist/commands/build/__tests__/build.test.js +206 -0
- package/dist/commands/build/__tests__/detect-project.test.d.ts +6 -0
- package/dist/commands/build/__tests__/detect-project.test.js +160 -0
- package/dist/commands/build/__tests__/run-build.test.d.ts +6 -0
- package/dist/commands/build/__tests__/run-build.test.js +433 -0
- package/dist/commands/intelligence/__tests__/command.test.d.ts +4 -0
- package/dist/commands/intelligence/__tests__/command.test.js +48 -0
- package/dist/commands/run-sheet/index.js +6 -0
- package/dist/commands/workflow/core/__tests__/feature-filter.test.d.ts +5 -0
- package/dist/commands/workflow/core/__tests__/feature-filter.test.js +316 -0
- package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.d.ts +4 -0
- package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.js +397 -0
- package/dist/commands/workflow/core/__tests__/state-manager.test.d.ts +4 -0
- package/dist/commands/workflow/core/__tests__/state-manager.test.js +384 -0
- package/dist/commands/workflow/executors/phase-executor.js +3 -1
- package/dist/commands/workflow/phase-orchestrator.js +1 -2
- package/dist/config/__tests__/config.test.d.ts +4 -0
- package/dist/config/__tests__/config.test.js +286 -0
- package/dist/config/__tests__/feature-status.test.d.ts +4 -0
- package/dist/config/__tests__/feature-status.test.js +111 -0
- package/dist/errors/__tests__/index.test.d.ts +4 -0
- package/dist/errors/__tests__/index.test.js +349 -0
- package/dist/index.js +0 -0
- package/dist/phases/app-store-generation/__tests__/agent.test.d.ts +5 -0
- package/dist/phases/app-store-generation/__tests__/agent.test.js +142 -0
- package/dist/phases/app-store-generation/__tests__/context.test.d.ts +4 -0
- package/dist/phases/app-store-generation/__tests__/context.test.js +284 -0
- package/dist/phases/app-store-generation/__tests__/prompts.test.d.ts +4 -0
- package/dist/phases/app-store-generation/__tests__/prompts.test.js +122 -0
- package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.d.ts +5 -0
- package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.js +826 -0
- package/dist/phases/app-store-generation/index.js +1 -2
- package/dist/phases/branch-planning/index.js +1 -2
- package/dist/phases/bug-fixing/analyzer.js +1 -2
- package/dist/phases/code-implementation/index.js +1 -2
- package/dist/phases/code-refine/index.js +1 -2
- package/dist/phases/code-review/__tests__/diff-utils.test.d.ts +1 -0
- package/dist/phases/code-review/__tests__/diff-utils.test.js +101 -0
- package/dist/phases/code-review/index.js +1 -2
- package/dist/phases/code-testing/analyzer.js +1 -2
- package/dist/phases/feature-analysis/index.js +1 -2
- package/dist/phases/functional-testing/analyzer.js +1 -2
- package/dist/phases/growth-analysis/index.js +1 -2
- package/dist/phases/intelligence-analysis/__tests__/context.test.d.ts +4 -0
- package/dist/phases/intelligence-analysis/__tests__/context.test.js +192 -0
- package/dist/phases/intelligence-analysis/__tests__/matching.test.d.ts +13 -0
- package/dist/phases/intelligence-analysis/__tests__/matching.test.js +154 -0
- package/dist/phases/intelligence-analysis/__tests__/orchestration.test.d.ts +5 -0
- package/dist/phases/intelligence-analysis/__tests__/orchestration.test.js +378 -0
- package/dist/phases/intelligence-analysis/__tests__/prompts.test.d.ts +4 -0
- package/dist/phases/intelligence-analysis/__tests__/prompts.test.js +33 -0
- package/dist/phases/pr-execution/__tests__/file-assigner.test.d.ts +1 -0
- package/dist/phases/pr-execution/__tests__/file-assigner.test.js +303 -0
- package/dist/phases/pr-execution/index.js +1 -0
- package/dist/phases/pr-resolve/__tests__/checklist-learner.test.d.ts +1 -0
- package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +157 -0
- package/dist/phases/pr-resolve/__tests__/prompts.test.d.ts +1 -0
- package/dist/phases/pr-resolve/__tests__/prompts.test.js +116 -0
- package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.d.ts +1 -0
- package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.js +138 -0
- package/dist/phases/pr-resolve/__tests__/types.test.d.ts +1 -0
- package/dist/phases/pr-resolve/__tests__/types.test.js +43 -0
- package/dist/phases/pr-resolve/__tests__/workspace.test.d.ts +1 -0
- package/dist/phases/pr-resolve/__tests__/workspace.test.js +111 -0
- package/dist/phases/pr-review/__tests__/prompts.test.d.ts +1 -0
- package/dist/phases/pr-review/__tests__/prompts.test.js +49 -0
- package/dist/phases/pr-review/__tests__/review-comments.test.d.ts +1 -0
- package/dist/phases/pr-review/__tests__/review-comments.test.js +110 -0
- package/dist/phases/pr-shared/__tests__/agent-utils.test.d.ts +1 -0
- package/dist/phases/pr-shared/__tests__/agent-utils.test.js +91 -0
- package/dist/phases/pr-shared/__tests__/context.test.d.ts +1 -0
- package/dist/phases/pr-shared/__tests__/context.test.js +94 -0
- package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.d.ts +1 -0
- package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.js +331 -0
- package/dist/phases/pr-splitting/index.js +1 -2
- package/dist/phases/release-sync/github.d.ts +12 -0
- package/dist/phases/release-sync/github.js +39 -0
- package/dist/phases/release-sync/snapshot.js +0 -1
- package/dist/phases/run-sheet/index.d.ts +15 -0
- package/dist/phases/run-sheet/index.js +161 -29
- package/dist/phases/run-sheet/render.d.ts +23 -5
- package/dist/phases/run-sheet/render.js +195 -31
- package/dist/phases/smoke-test/__tests__/agent.test.d.ts +4 -0
- package/dist/phases/smoke-test/__tests__/agent.test.js +84 -0
- package/dist/phases/smoke-test/__tests__/github.test.d.ts +9 -0
- package/dist/phases/smoke-test/__tests__/github.test.js +120 -0
- package/dist/phases/smoke-test/__tests__/snapshot.test.d.ts +8 -0
- package/dist/phases/smoke-test/__tests__/snapshot.test.js +93 -0
- package/dist/phases/smoke-test/agent.js +2 -4
- package/dist/phases/smoke-test/github.d.ts +54 -0
- package/dist/phases/smoke-test/github.js +101 -0
- package/dist/phases/smoke-test/index.js +11 -6
- package/dist/phases/smoke-test/snapshot.d.ts +27 -0
- package/dist/phases/smoke-test/snapshot.js +157 -0
- package/dist/phases/technical-design/index.js +1 -2
- package/dist/phases/test-cases-analysis/index.js +1 -2
- package/dist/phases/user-stories-analysis/index.js +1 -2
- package/dist/services/coaching/__tests__/coaching-agent.test.d.ts +1 -0
- package/dist/services/coaching/__tests__/coaching-agent.test.js +74 -0
- package/dist/services/coaching/__tests__/coaching-loop.test.d.ts +1 -0
- package/dist/services/coaching/__tests__/coaching-loop.test.js +59 -0
- package/dist/services/coaching/__tests__/self-rating.test.d.ts +1 -0
- package/dist/services/coaching/__tests__/self-rating.test.js +188 -0
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +4 -0
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +133 -0
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +4 -0
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +336 -0
- package/dist/services/lifecycle-agent/index.d.ts +24 -0
- package/dist/services/lifecycle-agent/index.js +25 -0
- package/dist/services/lifecycle-agent/phase-criteria.d.ts +57 -0
- package/dist/services/lifecycle-agent/phase-criteria.js +335 -0
- package/dist/services/lifecycle-agent/transition-rules.d.ts +60 -0
- package/dist/services/lifecycle-agent/transition-rules.js +184 -0
- package/dist/services/lifecycle-agent/types.d.ts +190 -0
- package/dist/services/lifecycle-agent/types.js +12 -0
- package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.d.ts +1 -0
- package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.js +122 -0
- package/dist/services/phase-hooks/__tests__/hook-executor.test.d.ts +1 -0
- package/dist/services/phase-hooks/__tests__/hook-executor.test.js +321 -0
- package/dist/services/phase-hooks/__tests__/hook-runner.test.d.ts +1 -0
- package/dist/services/phase-hooks/__tests__/hook-runner.test.js +261 -0
- package/dist/services/phase-hooks/__tests__/plugin-loader.test.d.ts +1 -0
- package/dist/services/phase-hooks/__tests__/plugin-loader.test.js +158 -0
- package/dist/services/video/__tests__/video-pipeline.test.d.ts +6 -0
- package/dist/services/video/__tests__/video-pipeline.test.js +249 -0
- package/dist/workspace/__tests__/workspace-manager.test.d.ts +7 -0
- package/dist/workspace/__tests__/workspace-manager.test.js +52 -0
- package/dist/workspace/workspace-manager.js +17 -4
- package/package.json +1 -1
- package/.env.local +0 -12
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { autoFixPROrdering, getTransitiveDependencies, parseImportSpecifiers, resolveImportToChangedFile, } from '../import-dep-validator.js';
|
|
4
|
+
// ============================================================
|
|
5
|
+
// parseImportSpecifiers
|
|
6
|
+
// ============================================================
|
|
7
|
+
void describe('parseImportSpecifiers', () => {
|
|
8
|
+
void it('parses named imports', () => {
|
|
9
|
+
const result = parseImportSpecifiers(`import { foo } from './utils'`);
|
|
10
|
+
assert.deepStrictEqual(result, ['./utils']);
|
|
11
|
+
});
|
|
12
|
+
void it('parses default imports', () => {
|
|
13
|
+
const result = parseImportSpecifiers(`import foo from './utils'`);
|
|
14
|
+
assert.deepStrictEqual(result, ['./utils']);
|
|
15
|
+
});
|
|
16
|
+
void it('parses namespace imports', () => {
|
|
17
|
+
const result = parseImportSpecifiers(`import * as foo from './utils'`);
|
|
18
|
+
assert.deepStrictEqual(result, ['./utils']);
|
|
19
|
+
});
|
|
20
|
+
void it('parses side-effect imports', () => {
|
|
21
|
+
const result = parseImportSpecifiers(`import './polyfill'`);
|
|
22
|
+
assert.deepStrictEqual(result, ['./polyfill']);
|
|
23
|
+
});
|
|
24
|
+
void it('parses type-only imports', () => {
|
|
25
|
+
const result = parseImportSpecifiers(`import type { Foo } from './types'`);
|
|
26
|
+
assert.deepStrictEqual(result, ['./types']);
|
|
27
|
+
});
|
|
28
|
+
void it('parses re-exports', () => {
|
|
29
|
+
const result = parseImportSpecifiers(`export { foo } from './utils'`);
|
|
30
|
+
assert.deepStrictEqual(result, ['./utils']);
|
|
31
|
+
});
|
|
32
|
+
void it('parses star re-exports', () => {
|
|
33
|
+
const result = parseImportSpecifiers(`export * from './utils'`);
|
|
34
|
+
assert.deepStrictEqual(result, ['./utils']);
|
|
35
|
+
});
|
|
36
|
+
void it('parses dynamic imports', () => {
|
|
37
|
+
const result = parseImportSpecifiers(`const m = import('./lazy')`);
|
|
38
|
+
assert.deepStrictEqual(result, ['./lazy']);
|
|
39
|
+
});
|
|
40
|
+
void it('parses multiple imports', () => {
|
|
41
|
+
const source = `
|
|
42
|
+
import { foo } from './utils'
|
|
43
|
+
import Bar from '../components/Bar'
|
|
44
|
+
import type { Baz } from './types'
|
|
45
|
+
`;
|
|
46
|
+
const result = parseImportSpecifiers(source);
|
|
47
|
+
assert.deepStrictEqual(result, ['./utils', '../components/Bar', './types']);
|
|
48
|
+
});
|
|
49
|
+
void it('filters out non-relative imports', () => {
|
|
50
|
+
const source = `
|
|
51
|
+
import { X } from 'lodash'
|
|
52
|
+
import React from 'react'
|
|
53
|
+
import { Y } from './local'
|
|
54
|
+
import { Z } from '@scope/package'
|
|
55
|
+
`;
|
|
56
|
+
const result = parseImportSpecifiers(source);
|
|
57
|
+
assert.deepStrictEqual(result, ['./local']);
|
|
58
|
+
});
|
|
59
|
+
void it('parses multiline imports', () => {
|
|
60
|
+
const source = `import {
|
|
61
|
+
foo,
|
|
62
|
+
bar,
|
|
63
|
+
baz
|
|
64
|
+
} from './utils'`;
|
|
65
|
+
const result = parseImportSpecifiers(source);
|
|
66
|
+
assert.deepStrictEqual(result, ['./utils']);
|
|
67
|
+
});
|
|
68
|
+
void it('deduplicates specifiers', () => {
|
|
69
|
+
const source = `
|
|
70
|
+
import { foo } from './utils'
|
|
71
|
+
import { bar } from './utils'
|
|
72
|
+
`;
|
|
73
|
+
const result = parseImportSpecifiers(source);
|
|
74
|
+
assert.deepStrictEqual(result, ['./utils']);
|
|
75
|
+
});
|
|
76
|
+
void it('handles .js extension in specifier', () => {
|
|
77
|
+
const result = parseImportSpecifiers(`import { X } from './utils.js'`);
|
|
78
|
+
assert.deepStrictEqual(result, ['./utils.js']);
|
|
79
|
+
});
|
|
80
|
+
void it('returns empty for empty source', () => {
|
|
81
|
+
assert.deepStrictEqual(parseImportSpecifiers(''), []);
|
|
82
|
+
});
|
|
83
|
+
void it('returns empty for non-relative only', () => {
|
|
84
|
+
const source = `import express from 'express'`;
|
|
85
|
+
assert.deepStrictEqual(parseImportSpecifiers(source), []);
|
|
86
|
+
});
|
|
87
|
+
void it('parses export default from', () => {
|
|
88
|
+
const result = parseImportSpecifiers(`export { default as X } from './module'`);
|
|
89
|
+
assert.deepStrictEqual(result, ['./module']);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
// ============================================================
|
|
93
|
+
// resolveImportToChangedFile
|
|
94
|
+
// ============================================================
|
|
95
|
+
void describe('resolveImportToChangedFile', () => {
|
|
96
|
+
void it('resolves exact match with extension', () => {
|
|
97
|
+
const changed = new Set(['src/utils.ts']);
|
|
98
|
+
const result = resolveImportToChangedFile('./utils.ts', 'src/app.ts', changed);
|
|
99
|
+
assert.strictEqual(result, 'src/utils.ts');
|
|
100
|
+
});
|
|
101
|
+
void it('infers .ts extension', () => {
|
|
102
|
+
const changed = new Set(['src/utils.ts']);
|
|
103
|
+
const result = resolveImportToChangedFile('./utils', 'src/app.ts', changed);
|
|
104
|
+
assert.strictEqual(result, 'src/utils.ts');
|
|
105
|
+
});
|
|
106
|
+
void it('infers .tsx extension', () => {
|
|
107
|
+
const changed = new Set(['src/Component.tsx']);
|
|
108
|
+
const result = resolveImportToChangedFile('./Component', 'src/app.ts', changed);
|
|
109
|
+
assert.strictEqual(result, 'src/Component.tsx');
|
|
110
|
+
});
|
|
111
|
+
void it('resolves index file', () => {
|
|
112
|
+
const changed = new Set(['src/types/index.ts']);
|
|
113
|
+
const result = resolveImportToChangedFile('./types', 'src/app.ts', changed);
|
|
114
|
+
assert.strictEqual(result, 'src/types/index.ts');
|
|
115
|
+
});
|
|
116
|
+
void it('resolves .js to .ts (ESM convention)', () => {
|
|
117
|
+
const changed = new Set(['src/utils.ts']);
|
|
118
|
+
const result = resolveImportToChangedFile('./utils.js', 'src/app.ts', changed);
|
|
119
|
+
assert.strictEqual(result, 'src/utils.ts');
|
|
120
|
+
});
|
|
121
|
+
void it('resolves parent directory traversal', () => {
|
|
122
|
+
const changed = new Set(['src/shared/helpers.ts']);
|
|
123
|
+
const result = resolveImportToChangedFile('../shared/helpers', 'src/components/Button.ts', changed);
|
|
124
|
+
assert.strictEqual(result, 'src/shared/helpers.ts');
|
|
125
|
+
});
|
|
126
|
+
void it('resolves double parent traversal', () => {
|
|
127
|
+
const changed = new Set(['src/utils.ts']);
|
|
128
|
+
const result = resolveImportToChangedFile('../../utils', 'src/features/explore/App.ts', changed);
|
|
129
|
+
assert.strictEqual(result, 'src/utils.ts');
|
|
130
|
+
});
|
|
131
|
+
void it('returns null for non-changed file', () => {
|
|
132
|
+
const changed = new Set(['src/other.ts']);
|
|
133
|
+
const result = resolveImportToChangedFile('./utils', 'src/app.ts', changed);
|
|
134
|
+
assert.strictEqual(result, null);
|
|
135
|
+
});
|
|
136
|
+
void it('returns null for external package import path', () => {
|
|
137
|
+
// This shouldn't happen since parseImportSpecifiers filters these,
|
|
138
|
+
// but test defensive behavior
|
|
139
|
+
const changed = new Set(['src/utils.ts']);
|
|
140
|
+
const result = resolveImportToChangedFile('./nonexistent', 'src/app.ts', changed);
|
|
141
|
+
assert.strictEqual(result, null);
|
|
142
|
+
});
|
|
143
|
+
void it('prefers .ts over .tsx when both exist', () => {
|
|
144
|
+
const changed = new Set(['src/utils.ts', 'src/utils.tsx']);
|
|
145
|
+
const result = resolveImportToChangedFile('./utils', 'src/app.ts', changed);
|
|
146
|
+
assert.strictEqual(result, 'src/utils.ts');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
// ============================================================
|
|
150
|
+
// getTransitiveDependencies
|
|
151
|
+
// ============================================================
|
|
152
|
+
void describe('getTransitiveDependencies', () => {
|
|
153
|
+
void it('returns direct dependency', () => {
|
|
154
|
+
const graph = new Map([
|
|
155
|
+
['A', new Set(['B'])],
|
|
156
|
+
['B', new Set()],
|
|
157
|
+
]);
|
|
158
|
+
const result = getTransitiveDependencies('A', graph);
|
|
159
|
+
assert.deepStrictEqual(result, new Set(['B']));
|
|
160
|
+
});
|
|
161
|
+
void it('returns transitive dependencies', () => {
|
|
162
|
+
const graph = new Map([
|
|
163
|
+
['A', new Set(['B'])],
|
|
164
|
+
['B', new Set(['C'])],
|
|
165
|
+
['C', new Set()],
|
|
166
|
+
]);
|
|
167
|
+
const result = getTransitiveDependencies('A', graph);
|
|
168
|
+
assert.deepStrictEqual(result, new Set(['B', 'C']));
|
|
169
|
+
});
|
|
170
|
+
void it('handles diamond dependency', () => {
|
|
171
|
+
const graph = new Map([
|
|
172
|
+
['A', new Set(['B', 'C'])],
|
|
173
|
+
['B', new Set(['D'])],
|
|
174
|
+
['C', new Set(['D'])],
|
|
175
|
+
['D', new Set()],
|
|
176
|
+
]);
|
|
177
|
+
const result = getTransitiveDependencies('A', graph);
|
|
178
|
+
assert.deepStrictEqual(result, new Set(['B', 'C', 'D']));
|
|
179
|
+
});
|
|
180
|
+
void it('handles cycles without infinite loop', () => {
|
|
181
|
+
const graph = new Map([
|
|
182
|
+
['A', new Set(['B'])],
|
|
183
|
+
['B', new Set(['A'])],
|
|
184
|
+
]);
|
|
185
|
+
const result = getTransitiveDependencies('A', graph);
|
|
186
|
+
assert.deepStrictEqual(result, new Set(['B', 'A']));
|
|
187
|
+
});
|
|
188
|
+
void it('returns empty set for no dependencies', () => {
|
|
189
|
+
const graph = new Map([['A', new Set()]]);
|
|
190
|
+
const result = getTransitiveDependencies('A', graph);
|
|
191
|
+
assert.deepStrictEqual(result, new Set());
|
|
192
|
+
});
|
|
193
|
+
void it('returns empty set for unknown file', () => {
|
|
194
|
+
const graph = new Map();
|
|
195
|
+
const result = getTransitiveDependencies('unknown', graph);
|
|
196
|
+
assert.deepStrictEqual(result, new Set());
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
// ============================================================
|
|
200
|
+
// autoFixPROrdering
|
|
201
|
+
// ============================================================
|
|
202
|
+
function makePR(sequence, name, files, dependsOn) {
|
|
203
|
+
return {
|
|
204
|
+
sequence,
|
|
205
|
+
name,
|
|
206
|
+
description: name,
|
|
207
|
+
branch_name: `pr/feat/${sequence}-${name.toLowerCase().replace(/\s/g, '-')}`,
|
|
208
|
+
depends_on_branch_name: dependsOn ?? null,
|
|
209
|
+
files: files.map((f) => ({ path: f, change_type: 'modified' })),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
void describe('autoFixPROrdering', () => {
|
|
213
|
+
void it('returns unchanged when no violations', () => {
|
|
214
|
+
const prs = [
|
|
215
|
+
makePR(1, 'Foundation', ['src/utils.ts']),
|
|
216
|
+
makePR(2, 'Components', ['src/app.ts'], 'pr/feat/1-foundation'),
|
|
217
|
+
];
|
|
218
|
+
// app.ts depends on utils.ts, and utils.ts is in PR 1 (earlier) — OK
|
|
219
|
+
const graph = new Map([
|
|
220
|
+
['src/app.ts', new Set(['src/utils.ts'])],
|
|
221
|
+
['src/utils.ts', new Set()],
|
|
222
|
+
]);
|
|
223
|
+
const result = autoFixPROrdering(prs, graph);
|
|
224
|
+
assert.strictEqual(result.movedFiles.length, 0);
|
|
225
|
+
assert.strictEqual(result.pullRequests[0].files?.length, 1);
|
|
226
|
+
assert.strictEqual(result.pullRequests[1].files?.length, 1);
|
|
227
|
+
});
|
|
228
|
+
void it('moves dependency from later PR to earlier PR', () => {
|
|
229
|
+
const prs = [
|
|
230
|
+
makePR(1, 'Components', ['src/app.ts']),
|
|
231
|
+
makePR(2, 'Utils', ['src/utils.ts'], 'pr/feat/1-components'),
|
|
232
|
+
];
|
|
233
|
+
// app.ts (PR 1) depends on utils.ts (PR 2) — violation!
|
|
234
|
+
const graph = new Map([
|
|
235
|
+
['src/app.ts', new Set(['src/utils.ts'])],
|
|
236
|
+
['src/utils.ts', new Set()],
|
|
237
|
+
]);
|
|
238
|
+
const result = autoFixPROrdering(prs, graph);
|
|
239
|
+
assert.strictEqual(result.movedFiles.length, 1);
|
|
240
|
+
assert.strictEqual(result.movedFiles[0].file, 'src/utils.ts');
|
|
241
|
+
// utils.ts should now be in PR 1
|
|
242
|
+
const pr1Files = result.pullRequests[0].files?.map((f) => f.path) ?? [];
|
|
243
|
+
assert.ok(pr1Files.includes('src/utils.ts'));
|
|
244
|
+
assert.ok(pr1Files.includes('src/app.ts'));
|
|
245
|
+
// PR 2 should be empty
|
|
246
|
+
assert.strictEqual(result.pullRequests[1].files?.length, 0);
|
|
247
|
+
});
|
|
248
|
+
void it('handles transitive dependency moves', () => {
|
|
249
|
+
const prs = [
|
|
250
|
+
makePR(1, 'App', ['src/app.ts']),
|
|
251
|
+
makePR(2, 'Service', ['src/service.ts'], 'pr/feat/1-app'),
|
|
252
|
+
makePR(3, 'Utils', ['src/utils.ts'], 'pr/feat/2-service'),
|
|
253
|
+
];
|
|
254
|
+
// app.ts → service.ts → utils.ts (chain across 3 PRs)
|
|
255
|
+
const graph = new Map([
|
|
256
|
+
['src/app.ts', new Set(['src/service.ts'])],
|
|
257
|
+
['src/service.ts', new Set(['src/utils.ts'])],
|
|
258
|
+
['src/utils.ts', new Set()],
|
|
259
|
+
]);
|
|
260
|
+
const result = autoFixPROrdering(prs, graph);
|
|
261
|
+
// Both service.ts and utils.ts should move to PR 1
|
|
262
|
+
const pr1Files = result.pullRequests[0].files?.map((f) => f.path) ?? [];
|
|
263
|
+
assert.ok(pr1Files.includes('src/app.ts'));
|
|
264
|
+
assert.ok(pr1Files.includes('src/service.ts'));
|
|
265
|
+
assert.ok(pr1Files.includes('src/utils.ts'));
|
|
266
|
+
});
|
|
267
|
+
void it('moves dep to earliest PR that needs it', () => {
|
|
268
|
+
const prs = [
|
|
269
|
+
makePR(1, 'PR1', ['src/a.ts']),
|
|
270
|
+
makePR(2, 'PR2', ['src/b.ts'], 'pr/feat/1-pr1'),
|
|
271
|
+
makePR(3, 'PR3', ['src/utils.ts'], 'pr/feat/2-pr2'),
|
|
272
|
+
];
|
|
273
|
+
// Both a.ts (PR 1) and b.ts (PR 2) import utils.ts (PR 3)
|
|
274
|
+
const graph = new Map([
|
|
275
|
+
['src/a.ts', new Set(['src/utils.ts'])],
|
|
276
|
+
['src/b.ts', new Set(['src/utils.ts'])],
|
|
277
|
+
['src/utils.ts', new Set()],
|
|
278
|
+
]);
|
|
279
|
+
const result = autoFixPROrdering(prs, graph);
|
|
280
|
+
// utils.ts should move to PR 1 (earliest needer)
|
|
281
|
+
const pr1Files = result.pullRequests[0].files?.map((f) => f.path) ?? [];
|
|
282
|
+
assert.ok(pr1Files.includes('src/utils.ts'));
|
|
283
|
+
});
|
|
284
|
+
void it('handles no dependencies at all', () => {
|
|
285
|
+
const prs = [
|
|
286
|
+
makePR(1, 'PR1', ['src/a.ts']),
|
|
287
|
+
makePR(2, 'PR2', ['src/b.ts'], 'pr/feat/1-pr1'),
|
|
288
|
+
];
|
|
289
|
+
const graph = new Map([
|
|
290
|
+
['src/a.ts', new Set()],
|
|
291
|
+
['src/b.ts', new Set()],
|
|
292
|
+
]);
|
|
293
|
+
const result = autoFixPROrdering(prs, graph);
|
|
294
|
+
assert.strictEqual(result.movedFiles.length, 0);
|
|
295
|
+
});
|
|
296
|
+
void it('handles single PR with all files', () => {
|
|
297
|
+
const prs = [makePR(1, 'Everything', ['src/a.ts', 'src/b.ts', 'src/c.ts'])];
|
|
298
|
+
const graph = new Map([
|
|
299
|
+
['src/a.ts', new Set(['src/b.ts'])],
|
|
300
|
+
['src/b.ts', new Set(['src/c.ts'])],
|
|
301
|
+
['src/c.ts', new Set()],
|
|
302
|
+
]);
|
|
303
|
+
const result = autoFixPROrdering(prs, graph);
|
|
304
|
+
assert.strictEqual(result.movedFiles.length, 0);
|
|
305
|
+
});
|
|
306
|
+
void it('does not mutate original PR files arrays', () => {
|
|
307
|
+
const originalFiles = [{ path: 'src/app.ts', change_type: 'modified' }];
|
|
308
|
+
const prs = [
|
|
309
|
+
{
|
|
310
|
+
sequence: 1,
|
|
311
|
+
name: 'PR1',
|
|
312
|
+
description: 'PR1',
|
|
313
|
+
files: originalFiles,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
sequence: 2,
|
|
317
|
+
name: 'PR2',
|
|
318
|
+
description: 'PR2',
|
|
319
|
+
files: [{ path: 'src/utils.ts', change_type: 'modified' }],
|
|
320
|
+
},
|
|
321
|
+
];
|
|
322
|
+
const graph = new Map([
|
|
323
|
+
['src/app.ts', new Set(['src/utils.ts'])],
|
|
324
|
+
['src/utils.ts', new Set()],
|
|
325
|
+
]);
|
|
326
|
+
autoFixPROrdering(prs, graph);
|
|
327
|
+
// Original files array should not be modified
|
|
328
|
+
assert.strictEqual(originalFiles.length, 1);
|
|
329
|
+
assert.strictEqual(originalFiles[0].path, 'src/app.ts');
|
|
330
|
+
});
|
|
331
|
+
});
|
|
@@ -27,9 +27,8 @@ async function* prompt(analysisPrompt) {
|
|
|
27
27
|
* then uses AI to produce a PR split plan saved to the database.
|
|
28
28
|
* Human review is expected before running the pr-execution phase.
|
|
29
29
|
*/
|
|
30
|
-
export const splitFeatureIntoPRs = async (options, config
|
|
31
30
|
// eslint-disable-next-line complexity
|
|
32
|
-
) => {
|
|
31
|
+
export const splitFeatureIntoPRs = async (options, config) => {
|
|
33
32
|
const { featureId, verbose, replaceExisting } = options;
|
|
34
33
|
if (verbose) {
|
|
35
34
|
logInfo(`Starting PR splitting for feature ID: ${featureId}`);
|
|
@@ -23,6 +23,7 @@ export interface CompareFile {
|
|
|
23
23
|
}
|
|
24
24
|
export interface CompareCommit {
|
|
25
25
|
sha: string;
|
|
26
|
+
html_url?: string;
|
|
26
27
|
commit: {
|
|
27
28
|
message: string;
|
|
28
29
|
author?: {
|
|
@@ -45,6 +46,17 @@ export declare function getDefaultBranchHead(owner: string, repo: string, token:
|
|
|
45
46
|
sha: string;
|
|
46
47
|
}>;
|
|
47
48
|
export declare function fetchCompare(owner: string, repo: string, base: string, head: string, token: string): Promise<CompareResponse>;
|
|
49
|
+
/**
|
|
50
|
+
* Paginated commit list for base...head. Unlike `fetchCompare` (single
|
|
51
|
+
* page, capped at ~250 by GitHub), this loops pages until exhausted or
|
|
52
|
+
* the caller-specified cap is hit. We don't need file patches here —
|
|
53
|
+
* run-sheet just wants the commit list — so we ignore `data.files`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function fetchAllCompareCommits(owner: string, repo: string, base: string, head: string, token: string, maxPages?: number): Promise<{
|
|
56
|
+
commits: CompareCommit[];
|
|
57
|
+
truncated: boolean;
|
|
58
|
+
totalCommits: number;
|
|
59
|
+
}>;
|
|
48
60
|
export declare function buildDiffDigest(compare: CompareResponse): string;
|
|
49
61
|
export declare function summariseStats(compare: CompareResponse): {
|
|
50
62
|
files_changed: number;
|
|
@@ -50,6 +50,7 @@ export async function fetchCompare(owner, repo, base, head, token) {
|
|
|
50
50
|
})),
|
|
51
51
|
commits: (data.commits ?? []).map((c) => ({
|
|
52
52
|
sha: c.sha,
|
|
53
|
+
html_url: c.html_url ?? undefined,
|
|
53
54
|
commit: {
|
|
54
55
|
message: c.commit?.message ?? '',
|
|
55
56
|
author: c.commit?.author
|
|
@@ -59,6 +60,44 @@ export async function fetchCompare(owner, repo, base, head, token) {
|
|
|
59
60
|
})),
|
|
60
61
|
};
|
|
61
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Paginated commit list for base...head. Unlike `fetchCompare` (single
|
|
65
|
+
* page, capped at ~250 by GitHub), this loops pages until exhausted or
|
|
66
|
+
* the caller-specified cap is hit. We don't need file patches here —
|
|
67
|
+
* run-sheet just wants the commit list — so we ignore `data.files`.
|
|
68
|
+
*/
|
|
69
|
+
export async function fetchAllCompareCommits(owner, repo, base, head, token, maxPages = 8) {
|
|
70
|
+
const gh = octokit(token);
|
|
71
|
+
const perPage = 100;
|
|
72
|
+
const out = [];
|
|
73
|
+
let totalCommits = 0;
|
|
74
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
75
|
+
const { data } = await gh.repos.compareCommits({
|
|
76
|
+
owner,
|
|
77
|
+
repo,
|
|
78
|
+
base,
|
|
79
|
+
head,
|
|
80
|
+
per_page: perPage,
|
|
81
|
+
page,
|
|
82
|
+
});
|
|
83
|
+
totalCommits = data.total_commits ?? totalCommits;
|
|
84
|
+
const batch = (data.commits ?? []).map((c) => ({
|
|
85
|
+
sha: c.sha,
|
|
86
|
+
html_url: c.html_url ?? undefined,
|
|
87
|
+
commit: {
|
|
88
|
+
message: c.commit?.message ?? '',
|
|
89
|
+
author: c.commit?.author
|
|
90
|
+
? { name: c.commit.author.name ?? undefined }
|
|
91
|
+
: null,
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
out.push(...batch);
|
|
95
|
+
if (batch.length < perPage) {
|
|
96
|
+
return { commits: out, truncated: false, totalCommits };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { commits: out, truncated: out.length < totalCommits, totalCommits };
|
|
100
|
+
}
|
|
62
101
|
export function buildDiffDigest(compare) {
|
|
63
102
|
const lines = [];
|
|
64
103
|
lines.push(`Total commits: ${compare.total_commits}`);
|
|
@@ -96,7 +96,6 @@ function userMessage(content) {
|
|
|
96
96
|
async function* makePrompt(text) {
|
|
97
97
|
yield userMessage(text);
|
|
98
98
|
}
|
|
99
|
-
// eslint-disable-next-line complexity -- agent loop with message-type handling
|
|
100
99
|
export async function detectSnapshotVersion(options) {
|
|
101
100
|
const { cwd, latestReleaseTag, verbose } = options;
|
|
102
101
|
if (verbose) {
|
|
@@ -23,7 +23,9 @@ export interface RunSheetResult {
|
|
|
23
23
|
runSheetId?: string;
|
|
24
24
|
missingPlaceholders?: string[];
|
|
25
25
|
cloneError?: string | null;
|
|
26
|
+
commitsError?: string | null;
|
|
26
27
|
commitsTruncated?: boolean;
|
|
28
|
+
isDraft?: boolean;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
31
|
* Atomic-create a `.lock` file alongside the workspace dir. Returns
|
|
@@ -36,4 +38,17 @@ export interface RunSheetResult {
|
|
|
36
38
|
export declare function tryAcquireFileLock(lockPath: string): boolean;
|
|
37
39
|
/** Exported for unit tests; not part of the public CLI surface. */
|
|
38
40
|
export declare function releaseFileLock(lockPath: string): void;
|
|
41
|
+
/**
|
|
42
|
+
* Delete `run-sheet-*` workspaces that are both:
|
|
43
|
+
* - not currently locked (lock file missing or stale), AND
|
|
44
|
+
* - older than WORKSPACE_MAX_AGE_MS by mtime.
|
|
45
|
+
*
|
|
46
|
+
* Best-effort: any error on a single entry is logged and skipped so a
|
|
47
|
+
* corrupt dir doesn't block the current generation.
|
|
48
|
+
*
|
|
49
|
+
* Exported for unit tests.
|
|
50
|
+
*/
|
|
51
|
+
export declare function pruneStaleRunSheetWorkspaces(workspaceRoot: string, now?: number, maxAgeMs?: number): {
|
|
52
|
+
removed: string[];
|
|
53
|
+
};
|
|
39
54
|
export declare function runRunSheet(options: RunSheetOptions): Promise<RunSheetResult>;
|