@planu/cli 4.3.8 → 4.3.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/CHANGELOG.md +12 -1
- package/dist/engine/hooks/file-watcher.js +1 -2
- package/dist/engine/pr-description-generator.js +5 -2
- package/dist/engine/validator/checklist.js +1 -1
- package/dist/resources/process.js +1 -1
- package/dist/resources/templates.js +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types/spec-format.d.ts +2 -2
- package/package.json +9 -9
- package/dist/engine/context-intelligence/index.d.ts +0 -7
- package/dist/engine/context-intelligence/index.js +0 -6
- package/dist/engine/legacy/ast-analyzer.d.ts +0 -21
- package/dist/engine/legacy/ast-analyzer.js +0 -113
- package/dist/engine/legacy/characterization-generator.d.ts +0 -6
- package/dist/engine/legacy/characterization-generator.js +0 -146
- package/dist/engine/legacy/hyrum-scanner.d.ts +0 -6
- package/dist/engine/legacy/hyrum-scanner.js +0 -137
- package/dist/engine/legacy/safety-net-orchestrator.d.ts +0 -7
- package/dist/engine/legacy/safety-net-orchestrator.js +0 -114
- package/dist/engine/legacy/seam-finder.d.ts +0 -6
- package/dist/engine/legacy/seam-finder.js +0 -139
- package/dist/tools/legacy/characterize-legacy-code.d.ts +0 -9
- package/dist/tools/legacy/characterize-legacy-code.js +0 -63
- package/dist/tools/legacy/detect-hyrum-risks.d.ts +0 -8
- package/dist/tools/legacy/detect-hyrum-risks.js +0 -47
- package/dist/tools/legacy/refactor-with-safety-net.d.ts +0 -10
- package/dist/tools/legacy/refactor-with-safety-net.js +0 -55
- package/dist/tools/legacy/seams-detector.d.ts +0 -8
- package/dist/tools/legacy/seams-detector.js +0 -47
- package/dist/types/legacy.d.ts +0 -151
- package/dist/types/legacy.js +0 -5
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// Planu — engine/legacy/safety-net-orchestrator.ts
|
|
2
|
-
// Pure orchestrator: given a project and target files, produces a sequenced
|
|
3
|
-
// safety-net plan for refactoring. No real execution — returns the plan.
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { findSeams } from './seam-finder.js';
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Step builders
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
function buildStep(order, label, description, estimatedMinutes) {
|
|
10
|
-
return {
|
|
11
|
-
order,
|
|
12
|
-
label,
|
|
13
|
-
description,
|
|
14
|
-
status: 'pending',
|
|
15
|
-
estimatedMinutes,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
const BASELINE_STEPS = [
|
|
19
|
-
{
|
|
20
|
-
label: 'Measure coverage baseline',
|
|
21
|
-
description: 'Run the test suite with coverage enabled (e.g., `pnpm test:coverage`) and record the current line/branch coverage percentage for the target files.',
|
|
22
|
-
status: 'pending',
|
|
23
|
-
estimatedMinutes: 5,
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
label: 'Add characterization tests',
|
|
27
|
-
description: 'Use `characterize_legacy_code` to generate test stubs. Fill in expected values by running the code manually. These tests lock in existing behavior before any refactoring.',
|
|
28
|
-
status: 'pending',
|
|
29
|
-
estimatedMinutes: 30,
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
label: 'Verify safety net',
|
|
33
|
-
description: 'Run the test suite again. All characterization tests must pass before proceeding. If any fail, fix the expected values — do not change the production code yet.',
|
|
34
|
-
status: 'pending',
|
|
35
|
-
estimatedMinutes: 5,
|
|
36
|
-
},
|
|
37
|
-
];
|
|
38
|
-
const REFACTOR_STEPS_AFTER_SAFETY_NET = [
|
|
39
|
-
{
|
|
40
|
-
label: 'Break seams incrementally',
|
|
41
|
-
description: 'Use `seams_detector` output to identify injection points. Break one seam at a time. Run tests after each change.',
|
|
42
|
-
status: 'pending',
|
|
43
|
-
estimatedMinutes: 60,
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
label: 'Validate behavior unchanged',
|
|
47
|
-
description: 'After each refactor step, run the full test suite. All characterization tests must still pass. If any break, roll back the last change.',
|
|
48
|
-
status: 'pending',
|
|
49
|
-
estimatedMinutes: 10,
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
label: 'Remove characterization scaffolding',
|
|
53
|
-
description: 'Once the refactored code has proper unit tests covering the same behaviors, remove the temporary characterization test stubs.',
|
|
54
|
-
status: 'pending',
|
|
55
|
-
estimatedMinutes: 15,
|
|
56
|
-
},
|
|
57
|
-
];
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// Refactor steps from seams
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
function seamToRefactorStep(seam) {
|
|
62
|
-
const patternMap = {
|
|
63
|
-
'static-call': 'inject-dependency',
|
|
64
|
-
'global-env': 'inject-dependency',
|
|
65
|
-
'inline-new': 'inject-dependency',
|
|
66
|
-
'module-singleton': 'replace-singleton',
|
|
67
|
-
'hardcoded-path': 'inject-dependency',
|
|
68
|
-
'hardcoded-url': 'inject-dependency',
|
|
69
|
-
};
|
|
70
|
-
const pattern = patternMap[seam.category] ?? 'introduce-seam';
|
|
71
|
-
const risk = seam.breakability === 'hard' ? 'high' : seam.breakability === 'moderate' ? 'medium' : 'low';
|
|
72
|
-
return {
|
|
73
|
-
file: seam.file,
|
|
74
|
-
change: `In function \`${seam.functionName}\`: ${seam.suggestion}`,
|
|
75
|
-
pattern,
|
|
76
|
-
requiresTest: true,
|
|
77
|
-
risk,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
// Main orchestrator
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
/**
|
|
84
|
-
* Builds a safety-net refactoring plan for the given project and target files.
|
|
85
|
-
* Pure function — returns a plan without executing anything.
|
|
86
|
-
*/
|
|
87
|
-
export async function buildSafetyNetPlan(projectPath, targetFiles, coverageThreshold = 80) {
|
|
88
|
-
// Detect seams to populate refactor steps
|
|
89
|
-
const seamResult = await findSeams(projectPath, targetFiles);
|
|
90
|
-
// Build ordered steps
|
|
91
|
-
const steps = [
|
|
92
|
-
...BASELINE_STEPS.map((s, i) => buildStep(i + 1, s.label, s.description, s.estimatedMinutes)),
|
|
93
|
-
...REFACTOR_STEPS_AFTER_SAFETY_NET.map((s, i) => buildStep(BASELINE_STEPS.length + i + 1, s.label, s.description, s.estimatedMinutes)),
|
|
94
|
-
];
|
|
95
|
-
// Build refactor steps from detected seams (max 20 to keep plan readable)
|
|
96
|
-
const refactorSteps = seamResult.seams
|
|
97
|
-
.slice(0, 20)
|
|
98
|
-
.map((seam) => seamToRefactorStep(seam));
|
|
99
|
-
const resolvedFiles = targetFiles !== undefined && targetFiles.length > 0
|
|
100
|
-
? targetFiles
|
|
101
|
-
: [`${join(projectPath, 'src')}/**/*.{ts,js}`];
|
|
102
|
-
const totalMinutes = steps.reduce((sum, s) => sum + s.estimatedMinutes, 0);
|
|
103
|
-
return {
|
|
104
|
-
projectPath,
|
|
105
|
-
targetFiles: resolvedFiles,
|
|
106
|
-
coverageBaseline: coverageThreshold,
|
|
107
|
-
steps,
|
|
108
|
-
refactorSteps,
|
|
109
|
-
summary: `Safety-net plan: ${steps.length} steps (~${totalMinutes} min total). ` +
|
|
110
|
-
`${refactorSteps.length} refactor actions identified from ${seamResult.seams.length} seams. ` +
|
|
111
|
-
`Coverage target: ≥${coverageThreshold}%.`,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
//# sourceMappingURL=safety-net-orchestrator.js.map
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
// Planu — engine/legacy/seam-finder.ts
|
|
2
|
-
// Finds dependency break points (seams) in TypeScript/JavaScript files.
|
|
3
|
-
// Patterns: static calls, globals, new() inline, hardcoded paths/URLs.
|
|
4
|
-
import { glob } from 'glob';
|
|
5
|
-
import { analyzeFile } from './ast-analyzer.js';
|
|
6
|
-
const SEAM_PATTERNS = [
|
|
7
|
-
{
|
|
8
|
-
category: 'static-call',
|
|
9
|
-
regex: /\b([A-Z]\w+)\.(\w+)\s*\(/,
|
|
10
|
-
breakability: 'moderate',
|
|
11
|
-
describe: (m) => `Static call to ${m[1] ?? '?'}.${m[2] ?? '?'}() — hard to mock/replace`,
|
|
12
|
-
suggest: () => 'Inject an interface/dependency instead of calling static method directly',
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
category: 'global-env',
|
|
16
|
-
regex: /process\.env\.(\w+)/,
|
|
17
|
-
breakability: 'easy',
|
|
18
|
-
describe: (m) => `Direct process.env.${m[1] ?? '?'} access — not injectable`,
|
|
19
|
-
suggest: (m) => `Extract to a config object or inject \`${m[1] ?? 'CONFIG'}\` via parameter`,
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
category: 'inline-new',
|
|
23
|
-
regex: /=\s*new\s+([A-Z]\w+)\s*\(/,
|
|
24
|
-
breakability: 'hard',
|
|
25
|
-
describe: (m) => `Inline \`new ${m[1] ?? '?'}()\` inside function — cannot be replaced`,
|
|
26
|
-
suggest: (m) => `Inject a factory or pass \`${m[1] ?? 'Dep'}Factory\` as a parameter`,
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
category: 'hardcoded-path',
|
|
30
|
-
regex: /['"`](\/[a-zA-Z0-9_\-/]+\.[a-zA-Z]{1,5})['"`]/,
|
|
31
|
-
breakability: 'easy',
|
|
32
|
-
describe: (m) => `Hardcoded file path: ${m[1] ?? '?'}`,
|
|
33
|
-
suggest: () => 'Move path to a config constant or inject via parameter',
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
category: 'hardcoded-url',
|
|
37
|
-
regex: /['"`](https?:\/\/[^'"`\s]+)['"`]/,
|
|
38
|
-
breakability: 'easy',
|
|
39
|
-
describe: (m) => `Hardcoded URL: ${(m[1] ?? '').slice(0, 60)}`,
|
|
40
|
-
suggest: () => 'Move URL to a config object or environment variable',
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
category: 'module-singleton',
|
|
44
|
-
regex: /^(?:import\s+\{[^}]+\}\s+from|const\s+\w+\s*=\s*require\s*\()/m,
|
|
45
|
-
breakability: 'moderate',
|
|
46
|
-
describe: () => 'Module-level singleton import — creates hidden dependency',
|
|
47
|
-
suggest: () => 'Pass the dependency via constructor or function parameter for testability',
|
|
48
|
-
},
|
|
49
|
-
];
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// Scanner
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
let seamCounter = 0;
|
|
54
|
-
function makeSeamId() {
|
|
55
|
-
seamCounter++;
|
|
56
|
-
return `SM-${String(seamCounter).padStart(4, '0')}`;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Scans a single file for seams.
|
|
60
|
-
*/
|
|
61
|
-
async function scanFile(filePath) {
|
|
62
|
-
const analysis = await analyzeFile(filePath);
|
|
63
|
-
if (analysis === null) {
|
|
64
|
-
return [];
|
|
65
|
-
}
|
|
66
|
-
const seams = [];
|
|
67
|
-
for (const fn of analysis.functions) {
|
|
68
|
-
const bodyLines = fn.body.split('\n');
|
|
69
|
-
for (const pattern of SEAM_PATTERNS) {
|
|
70
|
-
for (let i = 0; i < bodyLines.length; i++) {
|
|
71
|
-
const line = bodyLines[i] ?? '';
|
|
72
|
-
const match = line.match(pattern.regex);
|
|
73
|
-
if (match) {
|
|
74
|
-
const absoluteLine = fn.line + i;
|
|
75
|
-
seams.push({
|
|
76
|
-
id: makeSeamId(),
|
|
77
|
-
category: pattern.category,
|
|
78
|
-
file: filePath,
|
|
79
|
-
line: absoluteLine,
|
|
80
|
-
functionName: fn.name,
|
|
81
|
-
description: pattern.describe(match),
|
|
82
|
-
pattern: match[0],
|
|
83
|
-
suggestion: pattern.suggest(match),
|
|
84
|
-
breakability: pattern.breakability,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return seams;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Resolves the list of files to scan.
|
|
94
|
-
*/
|
|
95
|
-
async function resolveFiles(projectPath, files) {
|
|
96
|
-
if (files !== undefined && files.length > 0) {
|
|
97
|
-
return files;
|
|
98
|
-
}
|
|
99
|
-
return glob('src/**/*.{ts,tsx,js,jsx}', {
|
|
100
|
-
cwd: projectPath,
|
|
101
|
-
absolute: true,
|
|
102
|
-
ignore: ['**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*'],
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Scans the given files (or project) for seams.
|
|
107
|
-
*/
|
|
108
|
-
export async function findSeams(projectPath, files) {
|
|
109
|
-
seamCounter = 0;
|
|
110
|
-
const resolvedFiles = await resolveFiles(projectPath, files);
|
|
111
|
-
const BATCH = 20;
|
|
112
|
-
const allSeams = [];
|
|
113
|
-
for (let i = 0; i < resolvedFiles.length; i += BATCH) {
|
|
114
|
-
const batch = resolvedFiles.slice(i, i + BATCH);
|
|
115
|
-
const batchResults = await Promise.all(batch.map((f) => scanFile(f)));
|
|
116
|
-
for (const r of batchResults) {
|
|
117
|
-
allSeams.push(...r);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// Sort by breakability (hard first = most valuable to break), then file
|
|
121
|
-
const breakOrder = { hard: 0, moderate: 1, easy: 2 };
|
|
122
|
-
allSeams.sort((a, b) => {
|
|
123
|
-
const bk = breakOrder[a.breakability] - breakOrder[b.breakability];
|
|
124
|
-
if (bk !== 0) {
|
|
125
|
-
return bk;
|
|
126
|
-
}
|
|
127
|
-
return a.file.localeCompare(b.file);
|
|
128
|
-
});
|
|
129
|
-
const hardCount = allSeams.filter((s) => s.breakability === 'hard').length;
|
|
130
|
-
const modCount = allSeams.filter((s) => s.breakability === 'moderate').length;
|
|
131
|
-
const easyCount = allSeams.filter((s) => s.breakability === 'easy').length;
|
|
132
|
-
return {
|
|
133
|
-
projectPath,
|
|
134
|
-
filesScanned: resolvedFiles.length,
|
|
135
|
-
seams: allSeams,
|
|
136
|
-
summary: `Scanned ${resolvedFiles.length} files. Found ${allSeams.length} seams: ${hardCount} hard, ${modCount} moderate, ${easyCount} easy to break.`,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
//# sourceMappingURL=seam-finder.js.map
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import type { ToolResult, CharacterizeLegacyInput } from '../../types/index.js';
|
|
3
|
-
export declare const CharacterizeLegacyCodeInputSchema: {
|
|
4
|
-
projectPath: z.ZodString;
|
|
5
|
-
files: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
6
|
-
outputFile: z.ZodOptional<z.ZodString>;
|
|
7
|
-
};
|
|
8
|
-
export declare function handleCharacterizeLegacyCode(args: CharacterizeLegacyInput): Promise<ToolResult>;
|
|
9
|
-
//# sourceMappingURL=characterize-legacy-code.d.ts.map
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
// Planu — tools/legacy/characterize-legacy-code.ts
|
|
2
|
-
// Tool handler for characterize_legacy_code.
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import { writeFile, mkdir } from 'node:fs/promises';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { generateCharacterizationTests } from '../../engine/legacy/characterization-generator.js';
|
|
7
|
-
import { compactResult } from '../output-formatter.js';
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Input schema
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
export const CharacterizeLegacyCodeInputSchema = {
|
|
12
|
-
projectPath: z.string().max(4096).describe('Absolute path to the project root to analyze'),
|
|
13
|
-
files: z
|
|
14
|
-
.array(z.string())
|
|
15
|
-
.optional()
|
|
16
|
-
.describe('Optional list of absolute file paths to analyze. If omitted, scans src/**/*.{ts,js}'),
|
|
17
|
-
outputFile: z
|
|
18
|
-
.string()
|
|
19
|
-
.optional()
|
|
20
|
-
.describe('Absolute path where the generated test file will be written. Defaults to tests/characterization/characterization.test.ts'),
|
|
21
|
-
};
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Handler
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
export async function handleCharacterizeLegacyCode(args) {
|
|
26
|
-
const { projectPath, files, outputFile } = args;
|
|
27
|
-
const result = await generateCharacterizationTests(projectPath, files, outputFile);
|
|
28
|
-
if (result.tests.length === 0) {
|
|
29
|
-
return compactResult(`## Characterization Tests\n\nNo functions found to characterize in ${result.filesAnalyzed} file(s).\n\nTip: ensure the target files export named functions.`);
|
|
30
|
-
}
|
|
31
|
-
// Write the generated test file
|
|
32
|
-
const outPath = result.outputFile;
|
|
33
|
-
const allTestCode = [
|
|
34
|
-
'// AUTO-GENERATED by Planu characterize_legacy_code',
|
|
35
|
-
'// Fill in the TODO assertions with observed output values.',
|
|
36
|
-
'// DO NOT delete these tests until proper unit tests replace them.',
|
|
37
|
-
'',
|
|
38
|
-
...result.tests.map((t) => t.testCode),
|
|
39
|
-
].join('\n\n');
|
|
40
|
-
await mkdir(dirname(outPath), { recursive: true });
|
|
41
|
-
await writeFile(outPath, allTestCode, 'utf-8');
|
|
42
|
-
const outRelative = outPath.startsWith(join(projectPath, ''))
|
|
43
|
-
? outPath.slice(projectPath.length + 1)
|
|
44
|
-
: outPath;
|
|
45
|
-
const lines = [
|
|
46
|
-
`## Characterization Tests Generated`,
|
|
47
|
-
``,
|
|
48
|
-
`**Files analyzed**: ${result.filesAnalyzed}`,
|
|
49
|
-
`**Test stubs generated**: ${result.tests.length}`,
|
|
50
|
-
`**Output**: \`${outRelative}\``,
|
|
51
|
-
``,
|
|
52
|
-
`### Functions characterized`,
|
|
53
|
-
];
|
|
54
|
-
for (const test of result.tests.slice(0, 20)) {
|
|
55
|
-
lines.push(`- \`${test.functionName}\` in \`${test.sourceFile}\``);
|
|
56
|
-
}
|
|
57
|
-
if (result.tests.length > 20) {
|
|
58
|
-
lines.push(`- _...and ${result.tests.length - 20} more_`);
|
|
59
|
-
}
|
|
60
|
-
lines.push(``, `### Next steps`, `1. Open \`${outRelative}\``, `2. Run each function with the sample inputs shown`, `3. Replace \`// TODO: assert current output\` with the observed value`, `4. Run the test suite — all characterization tests must pass before refactoring`);
|
|
61
|
-
return compactResult(lines.join('\n'));
|
|
62
|
-
}
|
|
63
|
-
//# sourceMappingURL=characterize-legacy-code.js.map
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import type { ToolResult, LegacyAnalysisInput } from '../../types/index.js';
|
|
3
|
-
export declare const DetectHyrumRisksInputSchema: {
|
|
4
|
-
projectPath: z.ZodString;
|
|
5
|
-
files: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
6
|
-
};
|
|
7
|
-
export declare function handleDetectHyrumRisks(args: LegacyAnalysisInput): Promise<ToolResult>;
|
|
8
|
-
//# sourceMappingURL=detect-hyrum-risks.d.ts.map
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// Planu — tools/legacy/detect-hyrum-risks.ts
|
|
2
|
-
// Tool handler for detect_hyrum_risks.
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import { scanHyrumRisks } from '../../engine/legacy/hyrum-scanner.js';
|
|
5
|
-
import { compactResult, formatTable } from '../output-formatter.js';
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Input schema
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
export const DetectHyrumRisksInputSchema = {
|
|
10
|
-
projectPath: z.string().max(4096).describe('Absolute path to the project root to scan'),
|
|
11
|
-
files: z
|
|
12
|
-
.array(z.string())
|
|
13
|
-
.optional()
|
|
14
|
-
.describe('Optional list of absolute file paths to scan. If omitted, scans src/**/*.{ts,js}'),
|
|
15
|
-
};
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Handler
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
export async function handleDetectHyrumRisks(args) {
|
|
20
|
-
const { projectPath, files } = args;
|
|
21
|
-
const result = await scanHyrumRisks(projectPath, files);
|
|
22
|
-
if (result.risks.length === 0) {
|
|
23
|
-
return compactResult(`## Hyrum Risk Scan\n\n${result.summary}\n\nNo observable behavior risks detected.`);
|
|
24
|
-
}
|
|
25
|
-
const tableRows = result.risks.slice(0, 30).map((r) => ({
|
|
26
|
-
ID: r.id,
|
|
27
|
-
Severity: r.severity,
|
|
28
|
-
Category: r.category,
|
|
29
|
-
Function: r.functionName,
|
|
30
|
-
File: r.file.split('/').slice(-2).join('/'),
|
|
31
|
-
Description: r.description.slice(0, 80),
|
|
32
|
-
}));
|
|
33
|
-
const table = formatTable(tableRows, ['ID', 'Severity', 'Category', 'Function', 'File', 'Description'], `Hyrum Risks (${result.risks.length})`);
|
|
34
|
-
const highRisks = result.risks.filter((r) => r.severity === 'high');
|
|
35
|
-
const highSection = highRisks.length > 0
|
|
36
|
-
? [
|
|
37
|
-
``,
|
|
38
|
-
`### High-Severity Risks (action required)`,
|
|
39
|
-
...highRisks
|
|
40
|
-
.slice(0, 5)
|
|
41
|
-
.map((r) => `**${r.id}** \`${r.functionName}\` — ${r.description}\n` +
|
|
42
|
-
`\`\`\`\n${r.snippet}\n\`\`\``),
|
|
43
|
-
].join('\n')
|
|
44
|
-
: '';
|
|
45
|
-
return compactResult([`## Hyrum Risk Detection`, ``, result.summary, ``, table, highSection].join('\n'));
|
|
46
|
-
}
|
|
47
|
-
//# sourceMappingURL=detect-hyrum-risks.js.map
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import type { ToolResult, RefactorSafetyNetInput } from '../../types/index.js';
|
|
3
|
-
export declare const RefactorWithSafetyNetInputSchema: {
|
|
4
|
-
projectPath: z.ZodString;
|
|
5
|
-
files: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
6
|
-
coverageThreshold: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
7
|
-
dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
8
|
-
};
|
|
9
|
-
export declare function handleRefactorWithSafetyNet(args: RefactorSafetyNetInput): Promise<ToolResult>;
|
|
10
|
-
//# sourceMappingURL=refactor-with-safety-net.d.ts.map
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
// Planu — tools/legacy/refactor-with-safety-net.ts
|
|
2
|
-
// Tool handler for refactor_with_safety_net.
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import { buildSafetyNetPlan } from '../../engine/legacy/safety-net-orchestrator.js';
|
|
5
|
-
import { compactResult } from '../output-formatter.js';
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Input schema
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
export const RefactorWithSafetyNetInputSchema = {
|
|
10
|
-
projectPath: z
|
|
11
|
-
.string()
|
|
12
|
-
.max(4096)
|
|
13
|
-
.describe('Absolute path to the project root to analyze and plan'),
|
|
14
|
-
files: z
|
|
15
|
-
.array(z.string())
|
|
16
|
-
.optional()
|
|
17
|
-
.describe('Optional list of absolute file paths to target. If omitted, scans src/**/*.{ts,js}'),
|
|
18
|
-
coverageThreshold: z
|
|
19
|
-
.number()
|
|
20
|
-
.min(0)
|
|
21
|
-
.max(100)
|
|
22
|
-
.optional()
|
|
23
|
-
.default(80)
|
|
24
|
-
.describe('Minimum coverage percentage required before refactoring. Default: 80'),
|
|
25
|
-
dryRun: z
|
|
26
|
-
.boolean()
|
|
27
|
-
.optional()
|
|
28
|
-
.default(false)
|
|
29
|
-
.describe('If true, show the plan without executing any changes. Default: false (plan is always returned — this flag is reserved for future execution mode)'),
|
|
30
|
-
};
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Handler
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
export async function handleRefactorWithSafetyNet(args) {
|
|
35
|
-
const { projectPath, files, coverageThreshold = 80 } = args;
|
|
36
|
-
const plan = await buildSafetyNetPlan(projectPath, files, coverageThreshold);
|
|
37
|
-
const lines = [`## Refactor with Safety Net`, ``, plan.summary, ``, `### Steps`];
|
|
38
|
-
for (const step of plan.steps) {
|
|
39
|
-
lines.push(`**${step.order}. ${step.label}** (~${step.estimatedMinutes} min)`);
|
|
40
|
-
lines.push(` ${step.description}`);
|
|
41
|
-
}
|
|
42
|
-
if (plan.refactorSteps.length > 0) {
|
|
43
|
-
lines.push(``, `### Identified Refactor Actions`);
|
|
44
|
-
for (const rs of plan.refactorSteps.slice(0, 15)) {
|
|
45
|
-
const riskBadge = rs.risk === 'high' ? ' ⚠️ high risk' : rs.risk === 'medium' ? ' medium risk' : '';
|
|
46
|
-
lines.push(`- **${rs.pattern}**${riskBadge}: ${rs.change}`);
|
|
47
|
-
}
|
|
48
|
-
if (plan.refactorSteps.length > 15) {
|
|
49
|
-
lines.push(`- _...and ${plan.refactorSteps.length - 15} more_`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
lines.push(``, `### Coverage target`, `≥ ${plan.coverageBaseline}% line coverage required before starting refactoring.`, ``, `### Automated tools`, `- \`characterize_legacy_code\` — generates test stubs (Step 2)`, `- \`seams_detector\` — identifies injection points (Step 4)`, `- \`detect_hyrum_risks\` — flags observable behaviors to preserve`);
|
|
53
|
-
return compactResult(lines.join('\n'));
|
|
54
|
-
}
|
|
55
|
-
//# sourceMappingURL=refactor-with-safety-net.js.map
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import type { ToolResult, LegacyAnalysisInput } from '../../types/index.js';
|
|
3
|
-
export declare const SeamsDetectorInputSchema: {
|
|
4
|
-
projectPath: z.ZodString;
|
|
5
|
-
files: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
6
|
-
};
|
|
7
|
-
export declare function handleSeamsDetector(args: LegacyAnalysisInput): Promise<ToolResult>;
|
|
8
|
-
//# sourceMappingURL=seams-detector.d.ts.map
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// Planu — tools/legacy/seams-detector.ts
|
|
2
|
-
// Tool handler for seams_detector.
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import { findSeams } from '../../engine/legacy/seam-finder.js';
|
|
5
|
-
import { compactResult, formatTable } from '../output-formatter.js';
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Input schema
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
export const SeamsDetectorInputSchema = {
|
|
10
|
-
projectPath: z.string().max(4096).describe('Absolute path to the project root to scan'),
|
|
11
|
-
files: z
|
|
12
|
-
.array(z.string())
|
|
13
|
-
.optional()
|
|
14
|
-
.describe('Optional list of absolute file paths to scan. If omitted, scans src/**/*.{ts,js}'),
|
|
15
|
-
};
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Handler
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
export async function handleSeamsDetector(args) {
|
|
20
|
-
const { projectPath, files } = args;
|
|
21
|
-
const result = await findSeams(projectPath, files);
|
|
22
|
-
if (result.seams.length === 0) {
|
|
23
|
-
return compactResult(`## Seams Detector\n\n${result.summary}\n\nNo dependency break points detected.`);
|
|
24
|
-
}
|
|
25
|
-
const tableRows = result.seams.slice(0, 30).map((s) => ({
|
|
26
|
-
ID: s.id,
|
|
27
|
-
Breakability: s.breakability,
|
|
28
|
-
Category: s.category,
|
|
29
|
-
Function: s.functionName,
|
|
30
|
-
File: s.file.split('/').slice(-2).join('/'),
|
|
31
|
-
Suggestion: s.suggestion.slice(0, 80),
|
|
32
|
-
}));
|
|
33
|
-
const table = formatTable(tableRows, ['ID', 'Breakability', 'Category', 'Function', 'File', 'Suggestion'], `Seams Found (${result.seams.length})`);
|
|
34
|
-
const hardSeams = result.seams.filter((s) => s.breakability === 'hard');
|
|
35
|
-
const hardSection = hardSeams.length > 0
|
|
36
|
-
? [
|
|
37
|
-
``,
|
|
38
|
-
`### Hard-to-Break Seams (high priority)`,
|
|
39
|
-
...hardSeams
|
|
40
|
-
.slice(0, 5)
|
|
41
|
-
.map((s) => `**${s.id}** \`${s.functionName}\` — ${s.description}\n` +
|
|
42
|
-
`_Suggestion_: ${s.suggestion}`),
|
|
43
|
-
].join('\n')
|
|
44
|
-
: '';
|
|
45
|
-
return compactResult([`## Seams Detector`, ``, result.summary, ``, table, hardSection].join('\n'));
|
|
46
|
-
}
|
|
47
|
-
//# sourceMappingURL=seams-detector.js.map
|
package/dist/types/legacy.d.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
export type HyrumRiskCategory = 'exception-type' | 'return-shape' | 'ordering-guarantee' | 'null-vs-undefined' | 'error-message-text' | 'implicit-return';
|
|
2
|
-
export interface HyrumRisk {
|
|
3
|
-
/** Unique identifier for the risk */
|
|
4
|
-
id: string;
|
|
5
|
-
/** Category of observable behavior risk */
|
|
6
|
-
category: HyrumRiskCategory;
|
|
7
|
-
/** File where the risk was detected */
|
|
8
|
-
file: string;
|
|
9
|
-
/** Line number (1-based) */
|
|
10
|
-
line: number;
|
|
11
|
-
/** Name of the function/method containing the risk */
|
|
12
|
-
functionName: string;
|
|
13
|
-
/** Human-readable description of the observable behavior */
|
|
14
|
-
description: string;
|
|
15
|
-
/** Severity: high = likely depended on, medium = possibly, low = unlikely */
|
|
16
|
-
severity: 'high' | 'medium' | 'low';
|
|
17
|
-
/** Code snippet showing the problematic pattern */
|
|
18
|
-
snippet: string;
|
|
19
|
-
}
|
|
20
|
-
export interface HyrumScanResult {
|
|
21
|
-
projectPath: string;
|
|
22
|
-
filesScanned: number;
|
|
23
|
-
risks: HyrumRisk[];
|
|
24
|
-
summary: string;
|
|
25
|
-
}
|
|
26
|
-
export type SeamCategory = 'static-call' | 'global-env' | 'inline-new' | 'hardcoded-path' | 'hardcoded-url' | 'module-singleton';
|
|
27
|
-
export interface Seam {
|
|
28
|
-
/** Unique identifier for the seam */
|
|
29
|
-
id: string;
|
|
30
|
-
/** Category of the seam */
|
|
31
|
-
category: SeamCategory;
|
|
32
|
-
/** File where the seam was detected */
|
|
33
|
-
file: string;
|
|
34
|
-
/** Line number (1-based) */
|
|
35
|
-
line: number;
|
|
36
|
-
/** Name of the enclosing function/method */
|
|
37
|
-
functionName: string;
|
|
38
|
-
/** Human-readable description of the seam */
|
|
39
|
-
description: string;
|
|
40
|
-
/** The pattern that was matched */
|
|
41
|
-
pattern: string;
|
|
42
|
-
/** Suggested refactoring approach */
|
|
43
|
-
suggestion: string;
|
|
44
|
-
/** How easy it is to break this seam: easy | moderate | hard */
|
|
45
|
-
breakability: 'easy' | 'moderate' | 'hard';
|
|
46
|
-
}
|
|
47
|
-
export interface SeamScanResult {
|
|
48
|
-
projectPath: string;
|
|
49
|
-
filesScanned: number;
|
|
50
|
-
seams: Seam[];
|
|
51
|
-
summary: string;
|
|
52
|
-
}
|
|
53
|
-
export interface SampleInput {
|
|
54
|
-
/** Parameter name */
|
|
55
|
-
name: string;
|
|
56
|
-
/** Suggested sample value (as source code string) */
|
|
57
|
-
value: string;
|
|
58
|
-
/** Inferred TypeScript type */
|
|
59
|
-
type: string;
|
|
60
|
-
}
|
|
61
|
-
export interface CharacterizationTest {
|
|
62
|
-
/** Unique identifier */
|
|
63
|
-
id: string;
|
|
64
|
-
/** File being characterized */
|
|
65
|
-
sourceFile: string;
|
|
66
|
-
/** Function/method to characterize */
|
|
67
|
-
functionName: string;
|
|
68
|
-
/** Generated test stub code */
|
|
69
|
-
testCode: string;
|
|
70
|
-
/** Detected sample inputs for the function */
|
|
71
|
-
sampleInputs: SampleInput[];
|
|
72
|
-
/** Note to the developer about filling in expected values */
|
|
73
|
-
developerNote: string;
|
|
74
|
-
}
|
|
75
|
-
export interface CharacterizationResult {
|
|
76
|
-
projectPath: string;
|
|
77
|
-
filesAnalyzed: number;
|
|
78
|
-
tests: CharacterizationTest[];
|
|
79
|
-
outputFile: string;
|
|
80
|
-
summary: string;
|
|
81
|
-
}
|
|
82
|
-
export type SafetyNetStepStatus = 'pending' | 'in-progress' | 'done' | 'skipped' | 'failed';
|
|
83
|
-
export interface SafetyNetStep {
|
|
84
|
-
/** Step number (1-based) */
|
|
85
|
-
order: number;
|
|
86
|
-
/** Short label for the step */
|
|
87
|
-
label: string;
|
|
88
|
-
/** Detailed description of what to do */
|
|
89
|
-
description: string;
|
|
90
|
-
/** Status of the step */
|
|
91
|
-
status: SafetyNetStepStatus;
|
|
92
|
-
/** Estimated effort in minutes */
|
|
93
|
-
estimatedMinutes: number;
|
|
94
|
-
}
|
|
95
|
-
export interface RefactorStep {
|
|
96
|
-
/** Target file to refactor */
|
|
97
|
-
file: string;
|
|
98
|
-
/** Description of what to change */
|
|
99
|
-
change: string;
|
|
100
|
-
/** Pattern: extract-function | inject-dependency | replace-singleton | introduce-seam */
|
|
101
|
-
pattern: 'extract-function' | 'inject-dependency' | 'replace-singleton' | 'introduce-seam';
|
|
102
|
-
/** Whether a test must exist before this step */
|
|
103
|
-
requiresTest: boolean;
|
|
104
|
-
/** Risk level for this step */
|
|
105
|
-
risk: 'low' | 'medium' | 'high';
|
|
106
|
-
}
|
|
107
|
-
export interface SafetyNetPlan {
|
|
108
|
-
projectPath: string;
|
|
109
|
-
targetFiles: string[];
|
|
110
|
-
coverageBaseline: number;
|
|
111
|
-
steps: SafetyNetStep[];
|
|
112
|
-
refactorSteps: RefactorStep[];
|
|
113
|
-
summary: string;
|
|
114
|
-
}
|
|
115
|
-
export interface LegacyFunctionInfo {
|
|
116
|
-
name: string;
|
|
117
|
-
line: number;
|
|
118
|
-
params: string[];
|
|
119
|
-
body: string;
|
|
120
|
-
snippet: string;
|
|
121
|
-
}
|
|
122
|
-
export interface LegacyFileAnalysis {
|
|
123
|
-
filePath: string;
|
|
124
|
-
functions: LegacyFunctionInfo[];
|
|
125
|
-
lines: string[];
|
|
126
|
-
}
|
|
127
|
-
export interface HyrumRiskPattern {
|
|
128
|
-
category: HyrumRiskCategory;
|
|
129
|
-
regex: RegExp;
|
|
130
|
-
severity: HyrumRisk['severity'];
|
|
131
|
-
describe: (match: RegExpMatchArray) => string;
|
|
132
|
-
}
|
|
133
|
-
export interface LegacySeamPattern {
|
|
134
|
-
category: SeamCategory;
|
|
135
|
-
regex: RegExp;
|
|
136
|
-
breakability: Seam['breakability'];
|
|
137
|
-
describe: (match: RegExpMatchArray) => string;
|
|
138
|
-
suggest: (match: RegExpMatchArray) => string;
|
|
139
|
-
}
|
|
140
|
-
export interface LegacyAnalysisInput {
|
|
141
|
-
projectPath: string;
|
|
142
|
-
files?: string[];
|
|
143
|
-
}
|
|
144
|
-
export interface CharacterizeLegacyInput extends LegacyAnalysisInput {
|
|
145
|
-
outputFile?: string;
|
|
146
|
-
}
|
|
147
|
-
export interface RefactorSafetyNetInput extends LegacyAnalysisInput {
|
|
148
|
-
coverageThreshold?: number;
|
|
149
|
-
dryRun?: boolean;
|
|
150
|
-
}
|
|
151
|
-
//# sourceMappingURL=legacy.d.ts.map
|