@planu/cli 4.3.9 → 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/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,3 +1,6 @@
|
|
|
1
|
+
function technicalSectionRef(spec) {
|
|
2
|
+
return `${spec.specPath}#technical`;
|
|
3
|
+
}
|
|
1
4
|
// ---------------------------------------------------------------------------
|
|
2
5
|
// Title generation
|
|
3
6
|
// ---------------------------------------------------------------------------
|
|
@@ -46,7 +49,7 @@ function buildGithubMarkdown(spec, audience, includeChecklist) {
|
|
|
46
49
|
sections.push('');
|
|
47
50
|
sections.push(`## Changes`);
|
|
48
51
|
sections.push(`- Spec: \`${spec.specPath}\``);
|
|
49
|
-
sections.push(`- Technical: \`${spec
|
|
52
|
+
sections.push(`- Technical design: \`${technicalSectionRef(spec)}\``);
|
|
50
53
|
if (spec.tags.length > 0) {
|
|
51
54
|
sections.push(`- Tags: ${spec.tags.join(', ')}`);
|
|
52
55
|
}
|
|
@@ -86,7 +89,7 @@ function buildGitlabMarkdown(spec, audience, includeChecklist) {
|
|
|
86
89
|
sections.push(`Required by Planu spec ${spec.id}. Risk level: **${spec.risk}**. Difficulty: **${spec.difficulty}/5**.`);
|
|
87
90
|
sections.push('');
|
|
88
91
|
sections.push(`## How`);
|
|
89
|
-
sections.push(`See
|
|
92
|
+
sections.push(`See \`${technicalSectionRef(spec)}\` for implementation details.`);
|
|
90
93
|
if (spec.tags.length > 0) {
|
|
91
94
|
sections.push(`Tags: ${spec.tags.join(', ')}`);
|
|
92
95
|
}
|
|
@@ -29,7 +29,7 @@ function buildCompletenessItems(spec) {
|
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
id: 'cl-comp-2',
|
|
32
|
-
question: 'Is the technical design documented in
|
|
32
|
+
question: 'Is the technical design documented in spec.md under ## Technical?',
|
|
33
33
|
category: 'completeness',
|
|
34
34
|
answer: spec.technicalPath ? 'pending' : 'no',
|
|
35
35
|
autoChecked: false,
|
|
@@ -39,7 +39,7 @@ function getSddProcess() {
|
|
|
39
39
|
id: 'creating-spec',
|
|
40
40
|
name: '3. Create Spec',
|
|
41
41
|
tools: ['create_spec', 'reverse_engineer', 'estimate'],
|
|
42
|
-
description: 'Create a full SDD spec
|
|
42
|
+
description: 'Create a full SDD spec.md from requirements or ' +
|
|
43
43
|
'reverse-engineer from existing code. Estimate effort, cost, and tokens.',
|
|
44
44
|
},
|
|
45
45
|
{
|
|
@@ -11,7 +11,7 @@ function getBuiltInTemplates() {
|
|
|
11
11
|
{
|
|
12
12
|
id: 'feature',
|
|
13
13
|
name: 'Feature Spec',
|
|
14
|
-
description: 'Full feature specification
|
|
14
|
+
description: 'Full feature specification using unified spec.md sections',
|
|
15
15
|
type: 'feature',
|
|
16
16
|
sections: [
|
|
17
17
|
'metadata',
|
package/dist/types/index.d.ts
CHANGED
|
@@ -228,7 +228,6 @@ export * from './spec-from-issue.js';
|
|
|
228
228
|
export * from './plan-mode.js';
|
|
229
229
|
export * from './self-healing.js';
|
|
230
230
|
export * from './deploy.js';
|
|
231
|
-
export * from './legacy.js';
|
|
232
231
|
export * from './multi-agent-review.js';
|
|
233
232
|
export * from './delete-first.js';
|
|
234
233
|
export * from './auto-checkpoint.js';
|
package/dist/types/index.js
CHANGED
|
@@ -225,7 +225,6 @@ export * from './spec-from-issue.js';
|
|
|
225
225
|
export * from './plan-mode.js';
|
|
226
226
|
export * from './self-healing.js';
|
|
227
227
|
export * from './deploy.js';
|
|
228
|
-
export * from './legacy.js';
|
|
229
228
|
export * from './multi-agent-review.js';
|
|
230
229
|
export * from './delete-first.js';
|
|
231
230
|
export * from './auto-checkpoint.js';
|
|
@@ -32,7 +32,7 @@ export interface LeanSpecInput {
|
|
|
32
32
|
/** SPEC-481: Acceptance criteria format. Defaults to 'checkbox'. */
|
|
33
33
|
acFormat?: 'checkbox' | 'bdd';
|
|
34
34
|
}
|
|
35
|
-
/** A file entry in the
|
|
35
|
+
/** A file entry in the unified spec.md Technical section. */
|
|
36
36
|
export interface LeanFileEntry {
|
|
37
37
|
path: string;
|
|
38
38
|
status: 'pending' | 'done';
|
|
@@ -102,7 +102,7 @@ export interface ReplaceSectionResult {
|
|
|
102
102
|
replaced: boolean;
|
|
103
103
|
appended: boolean;
|
|
104
104
|
}
|
|
105
|
-
/** Input for lean
|
|
105
|
+
/** Input for lean spec.md Technical section generation. */
|
|
106
106
|
export interface LeanTechnicalInput {
|
|
107
107
|
specId: string;
|
|
108
108
|
filesToCreate?: LeanFileEntry[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.10",
|
|
4
4
|
"description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"packageName": "@planu/core"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
|
-
"@planu/core-darwin-arm64": "4.3.
|
|
36
|
-
"@planu/core-darwin-x64": "4.3.
|
|
37
|
-
"@planu/core-linux-arm64-gnu": "4.3.
|
|
38
|
-
"@planu/core-linux-arm64-musl": "4.3.
|
|
39
|
-
"@planu/core-linux-x64-gnu": "4.3.
|
|
40
|
-
"@planu/core-linux-x64-musl": "4.3.
|
|
41
|
-
"@planu/core-win32-arm64-msvc": "4.3.
|
|
42
|
-
"@planu/core-win32-x64-msvc": "4.3.
|
|
35
|
+
"@planu/core-darwin-arm64": "4.3.10",
|
|
36
|
+
"@planu/core-darwin-x64": "4.3.10",
|
|
37
|
+
"@planu/core-linux-arm64-gnu": "4.3.10",
|
|
38
|
+
"@planu/core-linux-arm64-musl": "4.3.10",
|
|
39
|
+
"@planu/core-linux-x64-gnu": "4.3.10",
|
|
40
|
+
"@planu/core-linux-x64-musl": "4.3.10",
|
|
41
|
+
"@planu/core-win32-arm64-msvc": "4.3.10",
|
|
42
|
+
"@planu/core-win32-x64-msvc": "4.3.10"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=24.0.0"
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { PlanuContextGraphProvider, createContextGraphProvider } from './context-graph-provider.js';
|
|
2
|
-
export { getSensitivePathRefusal, collectPreservedFragments, verifyPreservedFragments, shouldBypassLeanMode, } from './compression-guards.js';
|
|
3
|
-
export { compressSafeContext } from './safe-context-compressor.js';
|
|
4
|
-
export { evaluateContextModes, summarizeContextEval } from './eval-harness.js';
|
|
5
|
-
export { emptyImpactMap, estimateImpactBudget, mergeImpactMaps } from './impact-map.js';
|
|
6
|
-
export type { ContextEvalCase, ContextEvalMode, ContextEvalResult, ContextGraphInput, ContextGraphProvider, ContextImpactEdge, ContextImpactFile, ContextImpactMap, ContextImpactSymbol, SafeCompressionResult, SafeContextCompressionInput, } from '../../types/context-intelligence.js';
|
|
7
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { PlanuContextGraphProvider, createContextGraphProvider } from './context-graph-provider.js';
|
|
2
|
-
export { getSensitivePathRefusal, collectPreservedFragments, verifyPreservedFragments, shouldBypassLeanMode, } from './compression-guards.js';
|
|
3
|
-
export { compressSafeContext } from './safe-context-compressor.js';
|
|
4
|
-
export { evaluateContextModes, summarizeContextEval } from './eval-harness.js';
|
|
5
|
-
export { emptyImpactMap, estimateImpactBudget, mergeImpactMaps } from './impact-map.js';
|
|
6
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { LegacyFunctionInfo, LegacyFileAnalysis } from '../../types/index.js';
|
|
2
|
-
export type { LegacyFunctionInfo, LegacyFileAnalysis };
|
|
3
|
-
/**
|
|
4
|
-
* Checks if the given file extension is a supported TypeScript/JavaScript file.
|
|
5
|
-
*/
|
|
6
|
-
export declare function isSupportedFile(filePath: string): boolean;
|
|
7
|
-
/**
|
|
8
|
-
* Extracts a short snippet of lines around a given line number.
|
|
9
|
-
*/
|
|
10
|
-
export declare function extractSnippet(lines: string[], lineIndex: number, context?: number): string;
|
|
11
|
-
/**
|
|
12
|
-
* Parses function names and their line numbers from source code.
|
|
13
|
-
* Uses regex-based detection to avoid external AST dependencies.
|
|
14
|
-
*/
|
|
15
|
-
export declare function extractFunctions(content: string): LegacyFunctionInfo[];
|
|
16
|
-
/**
|
|
17
|
-
* Reads and analyzes a TypeScript/JavaScript source file.
|
|
18
|
-
* Returns null if the file is not supported or cannot be read.
|
|
19
|
-
*/
|
|
20
|
-
export declare function analyzeFile(filePath: string): Promise<LegacyFileAnalysis | null>;
|
|
21
|
-
//# sourceMappingURL=ast-analyzer.d.ts.map
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
// Planu — engine/legacy/ast-analyzer.ts
|
|
2
|
-
// Minimal TypeScript/JavaScript AST analyzer using regex-based patterns.
|
|
3
|
-
// No external dependencies beyond Node built-ins.
|
|
4
|
-
import { readFile } from 'node:fs/promises';
|
|
5
|
-
import { extname } from 'node:path';
|
|
6
|
-
// Regex patterns for function detection
|
|
7
|
-
const FUNCTION_PATTERNS = [
|
|
8
|
-
// export function foo(
|
|
9
|
-
/^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/m,
|
|
10
|
-
// const foo = (
|
|
11
|
-
/(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*(?::\s*\S+\s*)?=>/m,
|
|
12
|
-
// foo(
|
|
13
|
-
/^(?:export\s+)?(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*\S+\s*)?{/m,
|
|
14
|
-
];
|
|
15
|
-
/**
|
|
16
|
-
* Checks if the given file extension is a supported TypeScript/JavaScript file.
|
|
17
|
-
*/
|
|
18
|
-
export function isSupportedFile(filePath) {
|
|
19
|
-
const ext = extname(filePath).toLowerCase();
|
|
20
|
-
return ['.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs'].includes(ext);
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Extracts a short snippet of lines around a given line number.
|
|
24
|
-
*/
|
|
25
|
-
export function extractSnippet(lines, lineIndex, context = 3) {
|
|
26
|
-
const start = Math.max(0, lineIndex - 1);
|
|
27
|
-
const end = Math.min(lines.length, lineIndex + context);
|
|
28
|
-
return lines
|
|
29
|
-
.slice(start, end)
|
|
30
|
-
.map((l) => l.trimEnd())
|
|
31
|
-
.join('\n');
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Parses function names and their line numbers from source code.
|
|
35
|
-
* Uses regex-based detection to avoid external AST dependencies.
|
|
36
|
-
*/
|
|
37
|
-
export function extractFunctions(content) {
|
|
38
|
-
const lines = content.split('\n');
|
|
39
|
-
const functions = [];
|
|
40
|
-
const seen = new Set();
|
|
41
|
-
for (let i = 0; i < lines.length; i++) {
|
|
42
|
-
const line = lines[i] ?? '';
|
|
43
|
-
const trimmed = line.trim();
|
|
44
|
-
// Skip comments and empty lines
|
|
45
|
-
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed === '') {
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
for (const pattern of FUNCTION_PATTERNS) {
|
|
49
|
-
const match = trimmed.match(pattern);
|
|
50
|
-
if (match) {
|
|
51
|
-
const name = match[1];
|
|
52
|
-
const paramsRaw = match[2] ?? '';
|
|
53
|
-
if (name === undefined || name === '' || seen.has(`${name}:${i}`)) {
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
seen.add(`${name}:${i}`);
|
|
57
|
-
const params = paramsRaw
|
|
58
|
-
.split(',')
|
|
59
|
-
.map((p) => p.trim().split(':')[0]?.trim() ?? '')
|
|
60
|
-
.filter((p) => p.length > 0);
|
|
61
|
-
// Collect function body (up to closing brace or end of arrow fn)
|
|
62
|
-
const bodyLines = [];
|
|
63
|
-
let braceCount = 0;
|
|
64
|
-
let bodyStarted = false;
|
|
65
|
-
for (let j = i; j < Math.min(lines.length, i + 80); j++) {
|
|
66
|
-
const bl = lines[j] ?? '';
|
|
67
|
-
bodyLines.push(bl);
|
|
68
|
-
for (const ch of bl) {
|
|
69
|
-
if (ch === '{') {
|
|
70
|
-
braceCount++;
|
|
71
|
-
bodyStarted = true;
|
|
72
|
-
}
|
|
73
|
-
else if (ch === '}') {
|
|
74
|
-
braceCount--;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
if (bodyStarted && braceCount === 0) {
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
functions.push({
|
|
82
|
-
name,
|
|
83
|
-
line: i + 1,
|
|
84
|
-
params,
|
|
85
|
-
body: bodyLines.join('\n'),
|
|
86
|
-
snippet: extractSnippet(lines, i + 1),
|
|
87
|
-
});
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return functions;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Reads and analyzes a TypeScript/JavaScript source file.
|
|
96
|
-
* Returns null if the file is not supported or cannot be read.
|
|
97
|
-
*/
|
|
98
|
-
export async function analyzeFile(filePath) {
|
|
99
|
-
if (!isSupportedFile(filePath)) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
let content;
|
|
103
|
-
try {
|
|
104
|
-
content = await readFile(filePath, 'utf-8');
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
const lines = content.split('\n');
|
|
110
|
-
const functions = extractFunctions(content);
|
|
111
|
-
return { filePath, functions, lines };
|
|
112
|
-
}
|
|
113
|
-
//# sourceMappingURL=ast-analyzer.js.map
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { CharacterizationResult } from '../../types/index.js';
|
|
2
|
-
/**
|
|
3
|
-
* Generates characterization test stubs for functions in the given files.
|
|
4
|
-
*/
|
|
5
|
-
export declare function generateCharacterizationTests(projectPath: string, files?: string[], outputFile?: string): Promise<CharacterizationResult>;
|
|
6
|
-
//# sourceMappingURL=characterization-generator.d.ts.map
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
// Planu — engine/legacy/characterization-generator.ts
|
|
2
|
-
// Generates characterization test stubs for existing functions.
|
|
3
|
-
// The stubs call the function with detected sample inputs and leave
|
|
4
|
-
// `// TODO: assert current output` placeholders for the developer to fill in.
|
|
5
|
-
import { glob } from 'glob';
|
|
6
|
-
import { join, relative, basename } from 'node:path';
|
|
7
|
-
import { analyzeFile } from './ast-analyzer.js';
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Sample input inference
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Keyword groups for sample value inference
|
|
12
|
-
const PATH_KEYWORDS = ['path', 'dir', 'file'];
|
|
13
|
-
const URL_KEYWORDS = ['url', 'endpoint'];
|
|
14
|
-
const ID_KEYWORDS = ['id', 'key'];
|
|
15
|
-
const NUM_KEYWORDS = ['count', 'size', 'limit', 'max', 'min', 'num', 'index'];
|
|
16
|
-
const BOOL_KEYWORDS = ['flag', 'enabled', 'active', 'is', 'has'];
|
|
17
|
-
const ARRAY_KEYWORDS = ['list', 'items', 'arr'];
|
|
18
|
-
const OBJ_KEYWORDS = ['opts', 'options', 'config', 'args'];
|
|
19
|
-
function matchesAny(lower, keywords) {
|
|
20
|
-
return keywords.some((k) => lower.includes(k));
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Infers a plausible sample value for a parameter based on its name/type hints.
|
|
24
|
-
*/
|
|
25
|
-
function inferSampleValue(paramName) {
|
|
26
|
-
const lower = paramName.toLowerCase();
|
|
27
|
-
if (matchesAny(lower, PATH_KEYWORDS)) {
|
|
28
|
-
return { name: paramName, value: '"/tmp/sample"', type: 'string' };
|
|
29
|
-
}
|
|
30
|
-
if (matchesAny(lower, URL_KEYWORDS)) {
|
|
31
|
-
return { name: paramName, value: '"https://example.com"', type: 'string' };
|
|
32
|
-
}
|
|
33
|
-
if (matchesAny(lower, ID_KEYWORDS)) {
|
|
34
|
-
return { name: paramName, value: '"test-id-001"', type: 'string' };
|
|
35
|
-
}
|
|
36
|
-
if (matchesAny(lower, NUM_KEYWORDS)) {
|
|
37
|
-
return { name: paramName, value: '10', type: 'number' };
|
|
38
|
-
}
|
|
39
|
-
if (matchesAny(lower, BOOL_KEYWORDS)) {
|
|
40
|
-
return { name: paramName, value: 'true', type: 'boolean' };
|
|
41
|
-
}
|
|
42
|
-
if (matchesAny(lower, ARRAY_KEYWORDS)) {
|
|
43
|
-
return { name: paramName, value: '[]', type: 'unknown[]' };
|
|
44
|
-
}
|
|
45
|
-
if (matchesAny(lower, OBJ_KEYWORDS)) {
|
|
46
|
-
return { name: paramName, value: '{}', type: 'Record<string, unknown>' };
|
|
47
|
-
}
|
|
48
|
-
return { name: paramName, value: `"${paramName}-value"`, type: 'string' };
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Generates a test stub for a single function.
|
|
52
|
-
*/
|
|
53
|
-
function generateTestStub(fn, sourceFile, importPath, testId) {
|
|
54
|
-
const params = fn.params;
|
|
55
|
-
const sampleInputs = params.map(inferSampleValue);
|
|
56
|
-
const paramList = sampleInputs.map((s) => s.value).join(', ');
|
|
57
|
-
const isAsync = fn.body.includes('await') || fn.body.includes('async');
|
|
58
|
-
const awaitKeyword = isAsync ? 'await ' : '';
|
|
59
|
-
const testModifier = isAsync ? 'async ' : '';
|
|
60
|
-
const stubLines = [
|
|
61
|
-
`// Characterization test for ${fn.name}`,
|
|
62
|
-
`// File: ${sourceFile}`,
|
|
63
|
-
`// Generated by Planu — fill in the expected values.`,
|
|
64
|
-
``,
|
|
65
|
-
`import { ${fn.name} } from '${importPath}';`,
|
|
66
|
-
``,
|
|
67
|
-
`describe('${fn.name} (characterization)', () => {`,
|
|
68
|
-
` it('should behave as currently observed', ${testModifier}() => {`,
|
|
69
|
-
...sampleInputs.map((s) => ` const ${s.name} = ${s.value}; // ${s.type}`),
|
|
70
|
-
``,
|
|
71
|
-
` const result = ${awaitKeyword}${fn.name}(${paramList});`,
|
|
72
|
-
``,
|
|
73
|
-
` // TODO: Replace with actual observed output:`,
|
|
74
|
-
` // expect(result).toEqual(/* observed value */);`,
|
|
75
|
-
` expect(result).toBeDefined();`,
|
|
76
|
-
` });`,
|
|
77
|
-
`});`,
|
|
78
|
-
];
|
|
79
|
-
const testCode = stubLines.join('\n');
|
|
80
|
-
return {
|
|
81
|
-
id: testId,
|
|
82
|
-
sourceFile,
|
|
83
|
-
functionName: fn.name,
|
|
84
|
-
testCode,
|
|
85
|
-
sampleInputs,
|
|
86
|
-
developerNote: 'Run the function manually with these inputs, observe the output, then replace the TODO assertion with the actual expected value.',
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
// File resolution
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
|
-
async function resolveFiles(projectPath, files) {
|
|
93
|
-
if (files !== undefined && files.length > 0) {
|
|
94
|
-
return files;
|
|
95
|
-
}
|
|
96
|
-
return glob('src/**/*.{ts,tsx,js,jsx}', {
|
|
97
|
-
cwd: projectPath,
|
|
98
|
-
absolute: true,
|
|
99
|
-
ignore: ['**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*', '**/index.ts'],
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
// Main entry point
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
let testCounter = 0;
|
|
106
|
-
function makeTestId() {
|
|
107
|
-
testCounter++;
|
|
108
|
-
return `CT-${String(testCounter).padStart(4, '0')}`;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Generates characterization test stubs for functions in the given files.
|
|
112
|
-
*/
|
|
113
|
-
export async function generateCharacterizationTests(projectPath, files, outputFile) {
|
|
114
|
-
testCounter = 0;
|
|
115
|
-
const resolvedFiles = await resolveFiles(projectPath, files);
|
|
116
|
-
const allTests = [];
|
|
117
|
-
for (const filePath of resolvedFiles) {
|
|
118
|
-
const absPath = filePath.startsWith('/') ? filePath : join(projectPath, filePath);
|
|
119
|
-
const analysis = await analyzeFile(absPath);
|
|
120
|
-
if (analysis === null || analysis.functions.length === 0) {
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
const relPath = relative(projectPath, absPath);
|
|
124
|
-
// Convert to a relative import path usable in tests/
|
|
125
|
-
const importBase = basename(absPath).replace(/\.(ts|tsx|js|jsx)$/, '.js');
|
|
126
|
-
const importDir = relative(join(projectPath, 'tests'), join(projectPath, relPath.replace(/\/[^/]+$/, '')));
|
|
127
|
-
const importPath = `${importDir}/${importBase}`;
|
|
128
|
-
for (const fn of analysis.functions) {
|
|
129
|
-
// Skip private-looking functions (underscore prefix)
|
|
130
|
-
if (fn.name.startsWith('_')) {
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
allTests.push(generateTestStub(fn, relPath, importPath, makeTestId()));
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const defaultOutput = join(projectPath, 'tests', 'characterization', 'characterization.test.ts');
|
|
137
|
-
const outFile = outputFile ?? defaultOutput;
|
|
138
|
-
return {
|
|
139
|
-
projectPath,
|
|
140
|
-
filesAnalyzed: resolvedFiles.length,
|
|
141
|
-
tests: allTests,
|
|
142
|
-
outputFile: outFile,
|
|
143
|
-
summary: `Analyzed ${resolvedFiles.length} files. Generated ${allTests.length} characterization test stubs.`,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
//# sourceMappingURL=characterization-generator.js.map
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { HyrumScanResult } from '../../types/index.js';
|
|
2
|
-
/**
|
|
3
|
-
* Scans the given files (or project) for Hyrum's Law risks.
|
|
4
|
-
*/
|
|
5
|
-
export declare function scanHyrumRisks(projectPath: string, files?: string[]): Promise<HyrumScanResult>;
|
|
6
|
-
//# sourceMappingURL=hyrum-scanner.d.ts.map
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
// Planu — engine/legacy/hyrum-scanner.ts
|
|
2
|
-
// Detects Hyrum's Law risks in public API surfaces:
|
|
3
|
-
// observable behaviors that callers may depend on even if undocumented.
|
|
4
|
-
import { glob } from 'glob';
|
|
5
|
-
import { analyzeFile, extractSnippet } from './ast-analyzer.js';
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Risk detection patterns
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
const RISK_PATTERNS = [
|
|
10
|
-
{
|
|
11
|
-
category: 'exception-type',
|
|
12
|
-
regex: /throw\s+new\s+(\w+Error|Error)\s*\(/,
|
|
13
|
-
severity: 'high',
|
|
14
|
-
describe: (m) => `Throws ${m[1] ?? 'Error'} — callers may catch this specific type`,
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
category: 'return-shape',
|
|
18
|
-
regex: /return\s*\{\s*(\w+)\s*:/,
|
|
19
|
-
severity: 'medium',
|
|
20
|
-
describe: (m) => `Returns object with key "${m[1] ?? '?'}" — shape may be depended on`,
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
category: 'null-vs-undefined',
|
|
24
|
-
regex: /return\s+(null|undefined)\b/,
|
|
25
|
-
severity: 'high',
|
|
26
|
-
describe: (m) => `Returns ${m[1] ?? 'null/undefined'} — callers may distinguish null vs undefined`,
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
category: 'ordering-guarantee',
|
|
30
|
-
regex: /\.sort\s*\(/,
|
|
31
|
-
severity: 'medium',
|
|
32
|
-
describe: () => 'Array.sort() used — output ordering may be depended on by callers',
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
category: 'error-message-text',
|
|
36
|
-
regex: /throw\s+new\s+\w+\s*\(\s*[`'"]/,
|
|
37
|
-
severity: 'medium',
|
|
38
|
-
describe: () => 'Error with string literal — error message text may be relied upon by callers',
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
category: 'implicit-return',
|
|
42
|
-
regex: /=>\s*[^{(\n][^;\n]*$/m,
|
|
43
|
-
severity: 'low',
|
|
44
|
-
describe: () => 'Arrow function with implicit return — return value shape may be an implicit contract',
|
|
45
|
-
},
|
|
46
|
-
];
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
// Scanner
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
let riskCounter = 0;
|
|
51
|
-
function makeRiskId() {
|
|
52
|
-
riskCounter++;
|
|
53
|
-
return `HR-${String(riskCounter).padStart(4, '0')}`;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Scans a single file for Hyrum risks.
|
|
57
|
-
*/
|
|
58
|
-
async function scanFile(filePath) {
|
|
59
|
-
const analysis = await analyzeFile(filePath);
|
|
60
|
-
if (analysis === null) {
|
|
61
|
-
return [];
|
|
62
|
-
}
|
|
63
|
-
const risks = [];
|
|
64
|
-
for (const fn of analysis.functions) {
|
|
65
|
-
const bodyLines = fn.body.split('\n');
|
|
66
|
-
for (const pattern of RISK_PATTERNS) {
|
|
67
|
-
for (let i = 0; i < bodyLines.length; i++) {
|
|
68
|
-
const line = bodyLines[i] ?? '';
|
|
69
|
-
const match = line.match(pattern.regex);
|
|
70
|
-
if (match) {
|
|
71
|
-
const absoluteLine = fn.line + i;
|
|
72
|
-
risks.push({
|
|
73
|
-
id: makeRiskId(),
|
|
74
|
-
category: pattern.category,
|
|
75
|
-
file: filePath,
|
|
76
|
-
line: absoluteLine,
|
|
77
|
-
functionName: fn.name,
|
|
78
|
-
description: pattern.describe(match),
|
|
79
|
-
severity: pattern.severity,
|
|
80
|
-
snippet: extractSnippet(analysis.lines, absoluteLine),
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return risks;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Resolves the list of files to scan.
|
|
90
|
-
* If `files` is provided, uses that list. Otherwise globs for TS/JS files.
|
|
91
|
-
*/
|
|
92
|
-
async function resolveFiles(projectPath, files) {
|
|
93
|
-
if (files !== undefined && files.length > 0) {
|
|
94
|
-
return files;
|
|
95
|
-
}
|
|
96
|
-
return glob('src/**/*.{ts,tsx,js,jsx}', {
|
|
97
|
-
cwd: projectPath,
|
|
98
|
-
absolute: true,
|
|
99
|
-
ignore: ['**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*'],
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Scans the given files (or project) for Hyrum's Law risks.
|
|
104
|
-
*/
|
|
105
|
-
export async function scanHyrumRisks(projectPath, files) {
|
|
106
|
-
riskCounter = 0;
|
|
107
|
-
const resolvedFiles = await resolveFiles(projectPath, files);
|
|
108
|
-
// Process in parallel with a concurrency limit
|
|
109
|
-
const BATCH = 20;
|
|
110
|
-
const allRisks = [];
|
|
111
|
-
for (let i = 0; i < resolvedFiles.length; i += BATCH) {
|
|
112
|
-
const batch = resolvedFiles.slice(i, i + BATCH);
|
|
113
|
-
const batchResults = await Promise.all(batch.map((f) => scanFile(f)));
|
|
114
|
-
for (const r of batchResults) {
|
|
115
|
-
allRisks.push(...r);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
// Sort by severity then file
|
|
119
|
-
const severityOrder = { high: 0, medium: 1, low: 2 };
|
|
120
|
-
allRisks.sort((a, b) => {
|
|
121
|
-
const sv = severityOrder[a.severity] - severityOrder[b.severity];
|
|
122
|
-
if (sv !== 0) {
|
|
123
|
-
return sv;
|
|
124
|
-
}
|
|
125
|
-
return a.file.localeCompare(b.file);
|
|
126
|
-
});
|
|
127
|
-
const highCount = allRisks.filter((r) => r.severity === 'high').length;
|
|
128
|
-
const medCount = allRisks.filter((r) => r.severity === 'medium').length;
|
|
129
|
-
const lowCount = allRisks.filter((r) => r.severity === 'low').length;
|
|
130
|
-
return {
|
|
131
|
-
projectPath,
|
|
132
|
-
filesScanned: resolvedFiles.length,
|
|
133
|
-
risks: allRisks,
|
|
134
|
-
summary: `Scanned ${resolvedFiles.length} files. Found ${allRisks.length} Hyrum risks: ${highCount} high, ${medCount} medium, ${lowCount} low.`,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
//# sourceMappingURL=hyrum-scanner.js.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { SafetyNetPlan } from '../../types/index.js';
|
|
2
|
-
/**
|
|
3
|
-
* Builds a safety-net refactoring plan for the given project and target files.
|
|
4
|
-
* Pure function — returns a plan without executing anything.
|
|
5
|
-
*/
|
|
6
|
-
export declare function buildSafetyNetPlan(projectPath: string, targetFiles?: string[], coverageThreshold?: number): Promise<SafetyNetPlan>;
|
|
7
|
-
//# sourceMappingURL=safety-net-orchestrator.d.ts.map
|
|
@@ -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
|