forge-workflow 0.0.9 → 0.0.10
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/commands/dev.md +2 -2
- package/.claude/commands/plan.md +2 -2
- package/.claude/commands/ship.md +2 -2
- package/.claude/commands/status.md +4 -4
- package/.cline/workflows/dev.md +2 -2
- package/.cline/workflows/plan.md +2 -2
- package/.cline/workflows/ship.md +2 -2
- package/.cline/workflows/status.md +4 -4
- package/.codex/skills/dev/SKILL.md +2 -2
- package/.codex/skills/plan/SKILL.md +2 -2
- package/.codex/skills/ship/SKILL.md +2 -2
- package/.codex/skills/status/SKILL.md +4 -4
- package/.cursor/commands/dev.md +2 -2
- package/.cursor/commands/plan.md +2 -2
- package/.cursor/commands/ship.md +2 -2
- package/.cursor/commands/status.md +4 -4
- package/.github/prompts/dev.prompt.md +2 -2
- package/.github/prompts/plan.prompt.md +2 -2
- package/.github/prompts/ship.prompt.md +2 -2
- package/.github/prompts/status.prompt.md +4 -4
- package/.github/workflows/beads-to-github.yml +43 -10
- package/.github/workflows/github-to-beads.yml +10 -7
- package/.kilocode/workflows/dev.md +2 -2
- package/.kilocode/workflows/plan.md +2 -2
- package/.kilocode/workflows/ship.md +2 -2
- package/.kilocode/workflows/status.md +4 -4
- package/.opencode/commands/dev.md +2 -2
- package/.opencode/commands/plan.md +2 -2
- package/.opencode/commands/ship.md +2 -2
- package/.opencode/commands/status.md +4 -4
- package/.roo/commands/dev.md +2 -2
- package/.roo/commands/plan.md +2 -2
- package/.roo/commands/ship.md +2 -2
- package/.roo/commands/status.md +4 -4
- package/AGENTS.md +1 -0
- package/CLAUDE.md +12 -0
- package/bin/forge.js +10 -5
- package/docs/BEADS_GITHUB_SYNC.md +26 -0
- package/docs/TOOLCHAIN.md +130 -148
- package/lib/beads-bootstrap.js +225 -0
- package/lib/beads-health-check.js +55 -10
- package/lib/beads-setup.js +104 -28
- package/lib/beads-sync-scaffold.js +11 -6
- package/lib/commands/_issue.js +11 -1
- package/lib/commands/issues.js +49 -0
- package/lib/commands/recommend.js +22 -1
- package/lib/commands/setup.js +16 -10
- package/lib/commands/status.js +181 -0
- package/lib/commands/team.js +11 -1
- package/lib/commands/test.js +37 -2
- package/lib/commands/validate.js +14 -8
- package/lib/commands/worktree.js +27 -54
- package/lib/dep-guard/keyword-ripple.js +184 -0
- package/lib/detect-worktree.js +9 -10
- package/lib/forge-issues.js +326 -0
- package/lib/issue-sync/authority.js +100 -0
- package/lib/issue-sync/github-pull.js +184 -0
- package/lib/issue-sync/import-primitives.js +98 -0
- package/lib/issue-sync/legacy-link-bridge.js +436 -0
- package/lib/issue-sync/link-store.js +292 -0
- package/lib/issue-sync/project-github.js +123 -0
- package/lib/issue-sync/reconcile.js +195 -0
- package/lib/issue-sync/schema.js +126 -0
- package/lib/lefthook-check.js +5 -2
- package/lib/project-memory.js +564 -0
- package/lib/runtime-health.js +100 -12
- package/lib/smart-status/conflicts.js +205 -0
- package/lib/smart-status/scoring.js +177 -0
- package/lib/status/beads-snapshot.js +102 -0
- package/lib/status/presenter.js +65 -0
- package/lib/workflow/enforce-stage.js +3 -1
- package/lib/workflow/state-manager.js +164 -8
- package/package.json +12 -4
- package/scripts/beads-context.sh +124 -5
- package/scripts/beads-context.test.js +21 -4
- package/scripts/beads-migrate-to-dolt.sh +7 -0
- package/scripts/beads-upgrade-smoke.sh +263 -0
- package/scripts/behavioral-judge.sh +115 -11
- package/scripts/benchmark.js +349 -63
- package/scripts/dep-guard-analyze.js +52 -17
- package/scripts/dep-guard-keyword-ripple.js +29 -0
- package/scripts/dep-guard-render-review.js +86 -0
- package/scripts/dep-guard.sh +45 -232
- package/scripts/forge-team/lib/sync-github.sh +160 -28
- package/scripts/forge-team/tests/sync-github.test.sh +195 -58
- package/scripts/github-beads-sync/index.mjs +122 -98
- package/scripts/github-beads-sync/mapping.mjs +54 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +2 -2
- package/scripts/github-beads-sync/reverse-sync.mjs +31 -7
- package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
- package/scripts/preflight.sh +181 -0
- package/scripts/smart-status-score.js +31 -0
- package/scripts/smart-status-sessions.js +51 -0
- package/scripts/smart-status.sh +74 -329
- package/scripts/sync-agentic-workflow.js +48 -0
- package/scripts/test-ci-shard.js +244 -0
- package/scripts/test-dashboard.js +188 -52
- package/scripts/test-full-suite.js +186 -0
- package/scripts/test-profile.js +278 -0
- package/scripts/test.js +219 -28
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
package/scripts/benchmark.js
CHANGED
|
@@ -1,85 +1,371 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
* Performance benchmark script for Forge CLI.
|
|
5
|
-
* Measures CLI startup time, autoDetect, and detectFramework performance.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* node scripts/benchmark.js # Human-readable output
|
|
9
|
-
* node scripts/benchmark.js --json # JSON output (for CI/tests)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const { execFileSync } = require('node:child_process');
|
|
13
|
-
const { performance } = require('node:perf_hooks');
|
|
14
|
-
const path = require('node:path');
|
|
3
|
+
const { spawnSync: defaultSpawnSync } = require('node:child_process');
|
|
15
4
|
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { performance } = require('node:perf_hooks');
|
|
7
|
+
|
|
8
|
+
const { buildProfile, parseJUnitFiles, walk } = require('./test-profile');
|
|
16
9
|
|
|
17
10
|
const rootDir = path.join(__dirname, '..');
|
|
18
|
-
const
|
|
11
|
+
const DEFAULT_OUTPUT = path.join(rootDir, 'test-results', 'benchmark-results.json');
|
|
12
|
+
const DEFAULT_PROFILE_DIR = path.join(rootDir, 'test-results', 'benchmark-profiles');
|
|
13
|
+
const DEFAULT_SAMPLES = 3;
|
|
14
|
+
const DEFAULT_JUNIT_PATH = path.join(rootDir, 'test-results', 'test-results.xml');
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
const BENCHMARK_GROUPS = [
|
|
17
|
+
{
|
|
18
|
+
id: 'full-suite',
|
|
19
|
+
label: 'Whole suite',
|
|
20
|
+
command: ['bun', 'test'],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'pre-push-runner',
|
|
24
|
+
label: 'Pre-push runner slice',
|
|
25
|
+
command: ['bun', 'test', 'test/scripts/test-runner.test.js'],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'validation-core',
|
|
29
|
+
label: 'Validation core slice',
|
|
30
|
+
command: [
|
|
31
|
+
'bun',
|
|
32
|
+
'test',
|
|
33
|
+
'test/scripts/test-profile.test.js',
|
|
34
|
+
'test/scripts/test-ci-shard.test.js',
|
|
35
|
+
'test/scripts/test-runner.test.js',
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'hotspot-shell',
|
|
40
|
+
label: 'Hotspot shell slice',
|
|
41
|
+
command: [
|
|
42
|
+
'bun',
|
|
43
|
+
'test',
|
|
44
|
+
'test/scripts/smart-status.conflicts.files.test.js',
|
|
45
|
+
'test/scripts/smart-status.conflicts.merge-tree.test.js',
|
|
46
|
+
'test/scripts/dep-guard.check-ripple.basic.test.js',
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'validate',
|
|
51
|
+
label: 'Validate command',
|
|
52
|
+
command: ['bash', 'scripts/validate.sh'],
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
function parseArgs(argv) {
|
|
57
|
+
const args = {
|
|
58
|
+
groups: [],
|
|
59
|
+
json: false,
|
|
60
|
+
output: DEFAULT_OUTPUT,
|
|
61
|
+
profileDir: DEFAULT_PROFILE_DIR,
|
|
62
|
+
samples: DEFAULT_SAMPLES,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
66
|
+
const current = argv[index];
|
|
67
|
+
const next = argv[index + 1];
|
|
68
|
+
|
|
69
|
+
if (current === '--json') args.json = true;
|
|
70
|
+
if (current === '--output') args.output = next;
|
|
71
|
+
if (current === '--profile-dir') args.profileDir = next;
|
|
72
|
+
if (current === '--samples') args.samples = Number.parseInt(next, 10) || DEFAULT_SAMPLES;
|
|
73
|
+
if (current === '--group') args.groups.push(next);
|
|
31
74
|
}
|
|
32
|
-
|
|
33
|
-
return
|
|
75
|
+
|
|
76
|
+
return args;
|
|
34
77
|
}
|
|
35
78
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
79
|
+
function resolveGroups(groupIds = []) {
|
|
80
|
+
if (groupIds.length === 0) {
|
|
81
|
+
return BENCHMARK_GROUPS.map((group) => ({ ...group, command: [...group.command] }));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const byId = new Map(BENCHMARK_GROUPS.map((group) => [group.id, group]));
|
|
85
|
+
const resolved = [];
|
|
86
|
+
for (const id of groupIds) {
|
|
87
|
+
const match = byId.get(id);
|
|
88
|
+
if (!match) {
|
|
89
|
+
throw new Error(`Unknown benchmark group: ${id}`);
|
|
90
|
+
}
|
|
91
|
+
resolved.push({ ...match, command: [...match.command] });
|
|
92
|
+
}
|
|
93
|
+
return resolved;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function roundMs(value) {
|
|
97
|
+
return Math.round(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function calculateMedian(values) {
|
|
101
|
+
if (values.length === 0) return 0;
|
|
102
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
103
|
+
const middle = Math.floor(sorted.length / 2);
|
|
104
|
+
if (sorted.length % 2 === 0) {
|
|
105
|
+
return roundMs((sorted[middle - 1] + sorted[middle]) / 2);
|
|
106
|
+
}
|
|
107
|
+
return sorted[middle];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function summarizeSamples(samplesMs) {
|
|
111
|
+
if (samplesMs.length === 0) {
|
|
112
|
+
return {
|
|
113
|
+
maxMs: 0,
|
|
114
|
+
meanMs: 0,
|
|
115
|
+
medianMs: 0,
|
|
116
|
+
minMs: 0,
|
|
117
|
+
samplesMs: [],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const total = samplesMs.reduce((sum, value) => sum + value, 0);
|
|
121
|
+
return {
|
|
122
|
+
maxMs: Math.max(...samplesMs),
|
|
123
|
+
meanMs: roundMs(total / samplesMs.length),
|
|
124
|
+
medianMs: calculateMedian(samplesMs),
|
|
125
|
+
minMs: Math.min(...samplesMs),
|
|
126
|
+
samplesMs,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildJUnitCommand(command, junitPath) {
|
|
131
|
+
const [binary, subcommand, ...rest] = command;
|
|
132
|
+
if (binary === 'bun' && subcommand === 'test') {
|
|
133
|
+
return [
|
|
134
|
+
binary,
|
|
135
|
+
subcommand,
|
|
136
|
+
...rest,
|
|
137
|
+
'--reporter=junit',
|
|
138
|
+
'--reporter-outfile',
|
|
139
|
+
junitPath,
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
return [...command];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function runCommand(command, options = {}, spawnSync = defaultSpawnSync) {
|
|
146
|
+
const [binary, ...args] = command;
|
|
147
|
+
const result = spawnSync(binary, args, {
|
|
148
|
+
cwd: rootDir,
|
|
149
|
+
encoding: 'utf8',
|
|
150
|
+
stdio: 'pipe',
|
|
151
|
+
...options,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (result.error) {
|
|
155
|
+
throw result.error;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if ((result.status ?? 1) !== 0) {
|
|
159
|
+
throw new Error(result.stderr || `Command failed: ${command.join(' ')}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function materializeJUnitFile(expectedPath) {
|
|
166
|
+
if (fs.existsSync(expectedPath)) {
|
|
167
|
+
return expectedPath;
|
|
43
168
|
}
|
|
44
|
-
|
|
45
|
-
|
|
169
|
+
|
|
170
|
+
if (fs.existsSync(DEFAULT_JUNIT_PATH)) {
|
|
171
|
+
fs.mkdirSync(path.dirname(expectedPath), { recursive: true });
|
|
172
|
+
fs.copyFileSync(DEFAULT_JUNIT_PATH, expectedPath);
|
|
173
|
+
return expectedPath;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
throw new Error(`Benchmark run did not produce JUnit output at ${expectedPath}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function removeMaterializedOutputs(expectedPath) {
|
|
180
|
+
const directory = path.dirname(expectedPath);
|
|
181
|
+
const extension = path.extname(expectedPath);
|
|
182
|
+
const basename = path.basename(expectedPath, extension);
|
|
183
|
+
|
|
184
|
+
if (!fs.existsSync(directory)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
189
|
+
if (!entry.isFile() || !entry.name.endsWith(extension)) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (entry.name === `${basename}${extension}` || entry.name.startsWith(`${basename}.`)) {
|
|
194
|
+
fs.rmSync(path.join(directory, entry.name), { force: true });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function indexXmlFiles(directory) {
|
|
200
|
+
return new Map(
|
|
201
|
+
walk(directory, '.xml').map((filePath) => {
|
|
202
|
+
const stats = fs.statSync(filePath);
|
|
203
|
+
return [filePath, `${stats.size}:${stats.mtimeMs}`];
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function listChangedXmlFiles(directory, beforeIndex) {
|
|
209
|
+
return walk(directory, '.xml')
|
|
210
|
+
.filter((filePath) => {
|
|
211
|
+
const stats = fs.statSync(filePath);
|
|
212
|
+
const fingerprint = `${stats.size}:${stats.mtimeMs}`;
|
|
213
|
+
return beforeIndex.get(filePath) !== fingerprint;
|
|
214
|
+
})
|
|
215
|
+
.sort((left, right) => left.localeCompare(right));
|
|
46
216
|
}
|
|
47
217
|
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
218
|
+
function materializeJUnitFiles(expectedPath, sourceFiles) {
|
|
219
|
+
const uniqueSources = [];
|
|
220
|
+
const seen = new Set();
|
|
221
|
+
|
|
222
|
+
for (const sourceFile of sourceFiles) {
|
|
223
|
+
if (typeof sourceFile !== 'string' || seen.has(sourceFile) || !fs.existsSync(sourceFile)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
seen.add(sourceFile);
|
|
227
|
+
uniqueSources.push(sourceFile);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (uniqueSources.length === 0) {
|
|
231
|
+
return [materializeJUnitFile(expectedPath)];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return uniqueSources.map((sourceFile, index) => {
|
|
235
|
+
const destination = index === 0
|
|
236
|
+
? expectedPath
|
|
237
|
+
: expectedPath.replace(/\.xml$/, `.${index + 1}.xml`);
|
|
238
|
+
|
|
239
|
+
if (sourceFile === destination) {
|
|
240
|
+
return destination;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
244
|
+
fs.copyFileSync(sourceFile, destination);
|
|
245
|
+
return destination;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function buildGroupProfile(group, xmlFiles) {
|
|
250
|
+
const metrics = parseJUnitFiles(xmlFiles);
|
|
251
|
+
return buildProfile({
|
|
252
|
+
integrationSkipped: true,
|
|
253
|
+
label: `benchmark-${group.id}`,
|
|
254
|
+
}, metrics);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function runBenchmarkGroup(group, options = {}) {
|
|
258
|
+
const profileDir = path.resolve(rootDir, options.profileDir || DEFAULT_PROFILE_DIR);
|
|
259
|
+
const samples = options.samples || DEFAULT_SAMPLES;
|
|
260
|
+
const spawnSync = options.spawnSync || defaultSpawnSync;
|
|
261
|
+
|
|
262
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
263
|
+
|
|
264
|
+
const xmlFiles = [];
|
|
265
|
+
const samplesMs = [];
|
|
266
|
+
|
|
267
|
+
for (let sampleIndex = 0; sampleIndex < samples; sampleIndex += 1) {
|
|
268
|
+
const junitPath = path.join(profileDir, `${group.id}.sample-${sampleIndex + 1}.xml`);
|
|
269
|
+
removeMaterializedOutputs(junitPath);
|
|
270
|
+
fs.rmSync(DEFAULT_JUNIT_PATH, { force: true });
|
|
271
|
+
const beforeXmlFiles = indexXmlFiles(path.join(rootDir, 'test-results'));
|
|
272
|
+
const command = buildJUnitCommand(group.command, junitPath);
|
|
52
273
|
const start = performance.now();
|
|
53
|
-
|
|
54
|
-
|
|
274
|
+
runCommand(command, {}, spawnSync);
|
|
275
|
+
samplesMs.push(roundMs(performance.now() - start));
|
|
276
|
+
const changedXmlFiles = listChangedXmlFiles(path.join(rootDir, 'test-results'), beforeXmlFiles);
|
|
277
|
+
xmlFiles.push(...materializeJUnitFiles(junitPath, [
|
|
278
|
+
fs.existsSync(junitPath) ? junitPath : null,
|
|
279
|
+
...changedXmlFiles,
|
|
280
|
+
]));
|
|
55
281
|
}
|
|
56
|
-
|
|
57
|
-
|
|
282
|
+
|
|
283
|
+
const summary = summarizeSamples(samplesMs);
|
|
284
|
+
const profile = buildGroupProfile(group, xmlFiles);
|
|
285
|
+
const profilePath = path.join(profileDir, `${group.id}.profile.json`);
|
|
286
|
+
fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
command: group.command.join(' '),
|
|
290
|
+
groupId: group.id,
|
|
291
|
+
groupLabel: group.label,
|
|
292
|
+
profilePath: path.relative(rootDir, profilePath).replace(/\\/g, '/'),
|
|
293
|
+
samples,
|
|
294
|
+
...summary,
|
|
295
|
+
};
|
|
58
296
|
}
|
|
59
297
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
];
|
|
298
|
+
function buildBenchmarkResults(groups, results, timestamp = new Date().toISOString()) {
|
|
299
|
+
const totalMedianMs = results.reduce((sum, result) => sum + result.medianMs, 0);
|
|
300
|
+
const slowestGroup = [...results]
|
|
301
|
+
.sort((left, right) => right.medianMs - left.medianMs || left.groupId.localeCompare(right.groupId))[0] || null;
|
|
65
302
|
|
|
66
|
-
|
|
303
|
+
return {
|
|
304
|
+
groups: results,
|
|
305
|
+
requestedGroups: groups.map((group) => group.id),
|
|
306
|
+
samples: results[0]?.samples || DEFAULT_SAMPLES,
|
|
307
|
+
slowestGroup: slowestGroup
|
|
308
|
+
? { groupId: slowestGroup.groupId, groupLabel: slowestGroup.groupLabel, medianMs: slowestGroup.medianMs }
|
|
309
|
+
: null,
|
|
310
|
+
timestamp,
|
|
311
|
+
totalMedianMs,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function formatResultLine(result) {
|
|
316
|
+
return ` ${result.groupLabel}: median ${result.medianMs}ms (min ${result.minMs}ms, max ${result.maxMs}ms)`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function main(argv = process.argv.slice(2), deps = {}) {
|
|
320
|
+
const args = parseArgs(argv);
|
|
321
|
+
const groups = resolveGroups(args.groups);
|
|
322
|
+
const results = groups.map((group) => runBenchmarkGroup(group, {
|
|
323
|
+
profileDir: args.profileDir,
|
|
324
|
+
samples: args.samples,
|
|
325
|
+
spawnSync: deps.spawnSync,
|
|
326
|
+
}));
|
|
67
327
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
328
|
+
const payload = buildBenchmarkResults(groups, results);
|
|
329
|
+
const outputPath = path.resolve(rootDir, args.output || DEFAULT_OUTPUT);
|
|
330
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
331
|
+
fs.writeFileSync(outputPath, JSON.stringify(payload, null, 2));
|
|
332
|
+
|
|
333
|
+
if (args.json) {
|
|
334
|
+
process.stdout.write(JSON.stringify(payload));
|
|
335
|
+
return payload;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
console.log('\n Forge Test Benchmark Baselines');
|
|
339
|
+
console.log(' =============================\n');
|
|
340
|
+
for (const result of results) {
|
|
341
|
+
console.log(formatResultLine(result));
|
|
76
342
|
}
|
|
77
|
-
console.log(
|
|
343
|
+
console.log(`\n Total median: ${payload.totalMedianMs}ms`);
|
|
344
|
+
console.log(` Results saved to: ${path.relative(rootDir, outputPath).replace(/\\/g, '/')}\n`);
|
|
345
|
+
return payload;
|
|
346
|
+
}
|
|
78
347
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
|
|
82
|
-
console.log(` Results saved to: benchmark-results.json\n`);
|
|
348
|
+
if (require.main === module) {
|
|
349
|
+
main();
|
|
83
350
|
}
|
|
84
351
|
|
|
85
|
-
module.exports =
|
|
352
|
+
module.exports = {
|
|
353
|
+
BENCHMARK_GROUPS,
|
|
354
|
+
DEFAULT_OUTPUT,
|
|
355
|
+
DEFAULT_PROFILE_DIR,
|
|
356
|
+
DEFAULT_SAMPLES,
|
|
357
|
+
buildBenchmarkResults,
|
|
358
|
+
buildGroupProfile,
|
|
359
|
+
buildJUnitCommand,
|
|
360
|
+
calculateMedian,
|
|
361
|
+
formatResultLine,
|
|
362
|
+
main,
|
|
363
|
+
parseArgs,
|
|
364
|
+
resolveGroups,
|
|
365
|
+
roundMs,
|
|
366
|
+
materializeJUnitFile,
|
|
367
|
+
materializeJUnitFiles,
|
|
368
|
+
runBenchmarkGroup,
|
|
369
|
+
runCommand,
|
|
370
|
+
summarizeSamples,
|
|
371
|
+
};
|
|
@@ -32,26 +32,61 @@ function normalizeSingleIssuePayload(payload) {
|
|
|
32
32
|
return {};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function readStdinPayload() {
|
|
36
|
+
const raw = fs.readFileSync(0, 'utf8');
|
|
37
|
+
if (!raw.trim()) {
|
|
38
|
+
throw new Error('Expected JSON payload on stdin');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const payload = JSON.parse(raw);
|
|
42
|
+
return {
|
|
43
|
+
currentIssue: normalizeSingleIssuePayload(payload.currentIssue),
|
|
44
|
+
openIssues: normalizeListPayload(payload.openIssues),
|
|
45
|
+
inProgressIssues: normalizeListPayload(payload.inProgressIssues),
|
|
46
|
+
taskFile: payload.taskFile,
|
|
47
|
+
repositoryRoot: payload.repositoryRoot || process.cwd(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
35
51
|
function main() {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
repositoryRoot = process.cwd(),
|
|
42
|
-
] = process.argv.slice(2);
|
|
52
|
+
const args = process.argv.slice(2);
|
|
53
|
+
let currentIssue;
|
|
54
|
+
let openIssues;
|
|
55
|
+
let taskFile;
|
|
56
|
+
let repositoryRoot;
|
|
43
57
|
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
if (args[0] === '--stdin') {
|
|
59
|
+
const payload = readStdinPayload();
|
|
60
|
+
currentIssue = payload.currentIssue;
|
|
61
|
+
openIssues = [
|
|
62
|
+
...payload.openIssues,
|
|
63
|
+
...payload.inProgressIssues,
|
|
64
|
+
].filter((issue) => issue && issue.id && issue.id !== currentIssue.id);
|
|
65
|
+
taskFile = payload.taskFile;
|
|
66
|
+
repositoryRoot = payload.repositoryRoot;
|
|
67
|
+
} else {
|
|
68
|
+
const [
|
|
69
|
+
currentIssueFile,
|
|
70
|
+
openIssuesFile,
|
|
71
|
+
inProgressIssuesFile,
|
|
72
|
+
taskFileArg,
|
|
73
|
+
repositoryRootArg = process.cwd(),
|
|
74
|
+
] = args;
|
|
49
75
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
76
|
+
if (!currentIssueFile || !openIssuesFile || !inProgressIssuesFile || !taskFileArg) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
'Usage: dep-guard-analyze.js <current-issue.json> <open-issues.json> <in-progress-issues.json> <task-file> [repository-root]',
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
currentIssue = normalizeSingleIssuePayload(readJsonFile(currentIssueFile));
|
|
83
|
+
openIssues = [
|
|
84
|
+
...normalizeListPayload(readJsonFile(openIssuesFile)),
|
|
85
|
+
...normalizeListPayload(readJsonFile(inProgressIssuesFile)),
|
|
86
|
+
].filter((issue) => issue && issue.id && issue.id !== currentIssue.id);
|
|
87
|
+
taskFile = taskFileArg;
|
|
88
|
+
repositoryRoot = repositoryRootArg;
|
|
89
|
+
}
|
|
55
90
|
|
|
56
91
|
const result = analyzePhase3Dependencies({
|
|
57
92
|
currentIssue,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
parseActiveIssueLines,
|
|
6
|
+
renderKeywordRippleReport,
|
|
7
|
+
} = require('../lib/dep-guard/keyword-ripple.js');
|
|
8
|
+
|
|
9
|
+
function main() {
|
|
10
|
+
try {
|
|
11
|
+
const report = renderKeywordRippleReport({
|
|
12
|
+
issueId: process.env.ISSUE_ID || '',
|
|
13
|
+
sourceTitle: process.env.SOURCE_TITLE || '',
|
|
14
|
+
activeIssues: parseActiveIssueLines(process.env.LIST_OUTPUT || ''),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (report.overlapCount > 0) {
|
|
18
|
+
process.stdout.write(`⚠️ ${report.output}\n`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
process.stdout.write(`✅ ${report.output}\n`);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error(`dep-guard-keyword-ripple: ${error.message}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
main();
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
|
|
6
|
+
function readAnalysis() {
|
|
7
|
+
const raw = fs.readFileSync(0, 'utf8');
|
|
8
|
+
if (!raw.trim()) {
|
|
9
|
+
throw new Error('Expected JSON analysis payload on stdin');
|
|
10
|
+
}
|
|
11
|
+
return JSON.parse(raw);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function renderPhase3Review(analysis) {
|
|
15
|
+
const detectorCategories = Object.entries(analysis.scores ?? {})
|
|
16
|
+
.filter(([name, score]) => name !== 'rubric' && Number(score) > 0)
|
|
17
|
+
.map(([name]) => name);
|
|
18
|
+
|
|
19
|
+
const lines = [
|
|
20
|
+
'',
|
|
21
|
+
`Structured dependency review for ${analysis.currentIssue.id}...`,
|
|
22
|
+
'',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
if (!Array.isArray(analysis.issues) || analysis.issues.length === 0) {
|
|
26
|
+
lines.push('No conflicts detected');
|
|
27
|
+
return `${lines.join('\n')}\n`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const seenPairs = new Set();
|
|
31
|
+
for (const finding of analysis.issues) {
|
|
32
|
+
const key = `${analysis.currentIssue.id}->${finding.targetIssueId}`;
|
|
33
|
+
if (seenPairs.has(key)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
seenPairs.add(key);
|
|
37
|
+
lines.push(`Issue pair: ${analysis.currentIssue.id} -> ${finding.targetIssueId}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
lines.push(`Rubric score: ${analysis.scores?.rubric ?? 0}`);
|
|
41
|
+
lines.push(
|
|
42
|
+
`Confidence: ${(analysis.confidence?.score ?? 0).toFixed(2)}${
|
|
43
|
+
analysis.confidence?.belowThreshold ? ' (below 70% threshold)' : ''
|
|
44
|
+
}`,
|
|
45
|
+
);
|
|
46
|
+
lines.push(`Detector categories: ${detectorCategories.join(', ') || 'none'}`);
|
|
47
|
+
lines.push(`Needs user decision: ${analysis.needsUserDecision ? 'yes' : 'no'}`);
|
|
48
|
+
|
|
49
|
+
if (Array.isArray(analysis.detectorConflicts) && analysis.detectorConflicts.length > 0) {
|
|
50
|
+
lines.push('Conflicts:');
|
|
51
|
+
for (const conflict of analysis.detectorConflicts) {
|
|
52
|
+
lines.push(` - ${conflict}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (Array.isArray(analysis.proposals) && analysis.proposals.length > 0) {
|
|
57
|
+
lines.push('');
|
|
58
|
+
lines.push('Proposed dependency updates:');
|
|
59
|
+
for (const proposal of analysis.proposals) {
|
|
60
|
+
lines.push(` - ${proposal.dependentIssueId} depends on ${proposal.dependsOnIssueId}`);
|
|
61
|
+
if (Array.isArray(proposal.pros) && proposal.pros.length > 0) {
|
|
62
|
+
lines.push(` Pros: ${proposal.pros.join('; ')}`);
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(proposal.cons) && proposal.cons.length > 0) {
|
|
65
|
+
lines.push(` Cons: ${proposal.cons.join('; ')}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return `${lines.join('\n')}\n`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function main() {
|
|
74
|
+
process.stdout.write(renderPhase3Review(readAnalysis()));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
main();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
process.stderr.write(`${error.message}\n`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
renderPhase3Review,
|
|
86
|
+
};
|