@planu/cli 4.3.9 → 4.3.11
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 +8 -0
- package/dist/cli/commands/status.js +79 -46
- package/dist/engine/pr-description-generator.js +5 -2
- package/dist/engine/spec-generator/api-key-resolver.d.ts +4 -0
- package/dist/engine/spec-generator/api-key-resolver.js +31 -0
- package/dist/engine/spec-generator/index.d.ts +3 -0
- package/dist/engine/spec-generator/index.js +3 -0
- package/dist/engine/spec-generator/opus-generator.d.ts +12 -0
- package/dist/engine/spec-generator/opus-generator.js +97 -0
- package/dist/engine/spec-generator/quality-validator.d.ts +5 -0
- package/dist/engine/spec-generator/quality-validator.js +22 -0
- package/dist/engine/spec-migrator/planu-canonical-policy.js +9 -1
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +11 -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/tools/create-spec.js +5 -2
- package/dist/types/cli.d.ts +17 -0
- 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/dist/types/spec-generator.d.ts +38 -0
- package/package.json +11 -10
- 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,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
|