@planu/cli 4.3.21 → 4.3.22
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 +6 -0
- package/dist/engine/spec-format/lean-technical-generator.js +5 -7
- package/dist/engine/spec-format/unified-spec-builder.js +1 -1
- package/dist/engine/test-generators/contract-test-generator.d.ts +3 -2
- package/dist/engine/test-generators/contract-test-generator.js +8 -43
- package/dist/engine/test-generators/mock-generator.d.ts +1 -1
- package/dist/engine/test-generators/mock-generator.js +13 -31
- package/dist/tools/generate-tests/adapters/contract-testing-adapter.js +9 -3
- package/dist/tools/heal-spec-docs.js +96 -40
- package/dist/tools/migrate-tech/core-handlers.js +24 -72
- package/package.json +9 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
/** Generate lean ## Technical content: YAML-like metadata + files section only. */
|
|
4
4
|
export function generateLeanTechnicalContent(input) {
|
|
5
5
|
const { specId, filesToCreate = [], filesToModify = [], filesToTest = [] } = input;
|
|
6
|
-
const lines = ['---', `spec: ${specId}`, '---'
|
|
6
|
+
const lines = ['---', `spec: ${specId}`, '---'];
|
|
7
|
+
if (filesToCreate.length === 0 && filesToModify.length === 0 && filesToTest.length === 0) {
|
|
8
|
+
return lines.join('\n');
|
|
9
|
+
}
|
|
10
|
+
lines.push('', '## Files', '');
|
|
7
11
|
if (filesToCreate.length > 0) {
|
|
8
12
|
lines.push('### Create', '');
|
|
9
13
|
for (const f of filesToCreate) {
|
|
@@ -25,12 +29,6 @@ export function generateLeanTechnicalContent(input) {
|
|
|
25
29
|
}
|
|
26
30
|
lines.push('');
|
|
27
31
|
}
|
|
28
|
-
// If no files specified, add placeholder
|
|
29
|
-
if (filesToCreate.length === 0 && filesToModify.length === 0 && filesToTest.length === 0) {
|
|
30
|
-
lines.push('### Create', '', '- (to be determined)', '');
|
|
31
|
-
lines.push('### Modify', '', '- (to be determined)', '');
|
|
32
|
-
lines.push('### Test', '', '- (to be determined)', '');
|
|
33
|
-
}
|
|
34
32
|
return lines.join('\n');
|
|
35
33
|
}
|
|
36
34
|
//# sourceMappingURL=lean-technical-generator.js.map
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* inject sections the user did not author themselves.
|
|
14
14
|
*/
|
|
15
15
|
export function buildUnifiedSpecContent(leanSpecBody, leanTechnicalBody) {
|
|
16
|
-
const technicalBodyRaw = leanTechnicalBody.replace(/^---\n[\s\S]*?\n
|
|
16
|
+
const technicalBodyRaw = leanTechnicalBody.replace(/^---\n[\s\S]*?\n---(?:\n|$)/, '').trim();
|
|
17
17
|
if (technicalBodyRaw.length === 0) {
|
|
18
18
|
return leanSpecBody.endsWith('\n') ? leanSpecBody : `${leanSpecBody}\n`;
|
|
19
19
|
}
|
|
@@ -5,9 +5,10 @@ import type { ContractFormat, ContractSpec, ContractSpecEndpoint, ProjectKnowled
|
|
|
5
5
|
*/
|
|
6
6
|
export declare function detectContractFormat(knowledge: ProjectKnowledge): ContractFormat;
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Legacy helper retained for API compatibility. Contract generation must not
|
|
9
|
+
* infer endpoints from a title; callers must pass observed endpoints instead.
|
|
9
10
|
*/
|
|
10
|
-
export declare function buildSampleEndpoints(
|
|
11
|
+
export declare function buildSampleEndpoints(_specTitle: string): ContractSpecEndpoint[];
|
|
11
12
|
/**
|
|
12
13
|
* Generate a contract spec for the given spec title and knowledge.
|
|
13
14
|
* Format is auto-detected from project stack.
|
|
@@ -21,49 +21,11 @@ export function detectContractFormat(knowledge) {
|
|
|
21
21
|
}
|
|
22
22
|
// === Contract generation functions ===
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* Legacy helper retained for API compatibility. Contract generation must not
|
|
25
|
+
* infer endpoints from a title; callers must pass observed endpoints instead.
|
|
25
26
|
*/
|
|
26
|
-
export function buildSampleEndpoints(
|
|
27
|
-
|
|
28
|
-
.toLowerCase()
|
|
29
|
-
.replace(/[^a-z0-9]/g, '-')
|
|
30
|
-
.replace(/-+/g, '-');
|
|
31
|
-
return [
|
|
32
|
-
{
|
|
33
|
-
path: `/api/${resource}`,
|
|
34
|
-
method: 'GET',
|
|
35
|
-
requestSchema: {},
|
|
36
|
-
responseSchema: {
|
|
37
|
-
type: 'array',
|
|
38
|
-
items: { type: 'object', properties: { id: { type: 'string' } } },
|
|
39
|
-
},
|
|
40
|
-
statusCodes: [200, 401, 500],
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
path: `/api/${resource}`,
|
|
44
|
-
method: 'POST',
|
|
45
|
-
requestSchema: {
|
|
46
|
-
type: 'object',
|
|
47
|
-
required: ['name'],
|
|
48
|
-
properties: { name: { type: 'string' } },
|
|
49
|
-
},
|
|
50
|
-
responseSchema: {
|
|
51
|
-
type: 'object',
|
|
52
|
-
properties: { id: { type: 'string' }, name: { type: 'string' } },
|
|
53
|
-
},
|
|
54
|
-
statusCodes: [201, 400, 401, 422, 500],
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
path: `/api/${resource}/{id}`,
|
|
58
|
-
method: 'GET',
|
|
59
|
-
requestSchema: {},
|
|
60
|
-
responseSchema: {
|
|
61
|
-
type: 'object',
|
|
62
|
-
properties: { id: { type: 'string' }, name: { type: 'string' } },
|
|
63
|
-
},
|
|
64
|
-
statusCodes: [200, 401, 404, 500],
|
|
65
|
-
},
|
|
66
|
-
];
|
|
27
|
+
export function buildSampleEndpoints(_specTitle) {
|
|
28
|
+
return [];
|
|
67
29
|
}
|
|
68
30
|
// === OpenAPI contract generation ===
|
|
69
31
|
function buildOpenApiYaml(specTitle, endpoints) {
|
|
@@ -244,7 +206,10 @@ export const ${name}Router = router({
|
|
|
244
206
|
*/
|
|
245
207
|
export function generateContractSpec(specTitle, knowledge) {
|
|
246
208
|
const format = detectContractFormat(knowledge);
|
|
247
|
-
const endpoints =
|
|
209
|
+
const endpoints = [];
|
|
210
|
+
if (endpoints.length === 0) {
|
|
211
|
+
return { format, content: '', endpoints };
|
|
212
|
+
}
|
|
248
213
|
let content;
|
|
249
214
|
switch (format) {
|
|
250
215
|
case 'pact':
|
|
@@ -2,7 +2,7 @@ import type { ApiMockDefinition, ContractSpecEndpoint, MockServerTool } from '..
|
|
|
2
2
|
/**
|
|
3
3
|
* Generate mock definitions from a list of endpoints.
|
|
4
4
|
*/
|
|
5
|
-
export declare function generateMockDefinitions(endpoints: ContractSpecEndpoint[],
|
|
5
|
+
export declare function generateMockDefinitions(endpoints: ContractSpecEndpoint[], _specTitle: string): ApiMockDefinition[];
|
|
6
6
|
/**
|
|
7
7
|
* Generate mock server content for the given tool.
|
|
8
8
|
*/
|
|
@@ -1,23 +1,5 @@
|
|
|
1
1
|
// engine/test-generators/mock-generator.ts
|
|
2
2
|
// SPEC-018: Mock generation for API endpoints (realistic data, delays, error responses)
|
|
3
|
-
// === Realistic sample data generators ===
|
|
4
|
-
function generateSampleId() {
|
|
5
|
-
return '550e8400-e29b-41d4-a716-446655440000';
|
|
6
|
-
}
|
|
7
|
-
function generateSampleData(resource) {
|
|
8
|
-
return {
|
|
9
|
-
id: generateSampleId(),
|
|
10
|
-
name: `Sample ${resource}`,
|
|
11
|
-
description: `Auto-generated sample for ${resource} (Planu SPEC-018)`,
|
|
12
|
-
status: 'active',
|
|
13
|
-
createdAt: '2025-01-15T10:30:00.000Z',
|
|
14
|
-
updatedAt: '2025-01-15T10:30:00.000Z',
|
|
15
|
-
email: `user@example.com`,
|
|
16
|
-
count: 42,
|
|
17
|
-
tags: ['sample', 'generated'],
|
|
18
|
-
metadata: { source: 'planu', version: '1.0' },
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
3
|
const ERROR_RESPONSES = [
|
|
22
4
|
{
|
|
23
5
|
status: 400,
|
|
@@ -56,16 +38,11 @@ function getErrorResponsesForEndpoint(statusCodes) {
|
|
|
56
38
|
/**
|
|
57
39
|
* Build an ApiMockDefinition from a ContractSpecEndpoint.
|
|
58
40
|
*/
|
|
59
|
-
function buildMockFromEndpoint(endpoint
|
|
60
|
-
const sampleData = endpoint.method === 'GET' && endpoint.path.endsWith('}')
|
|
61
|
-
? generateSampleData(resource)
|
|
62
|
-
: endpoint.method === 'GET'
|
|
63
|
-
? { items: [generateSampleData(resource)], total: 1, page: 1, pageSize: 20 }
|
|
64
|
-
: generateSampleData(resource);
|
|
41
|
+
function buildMockFromEndpoint(endpoint) {
|
|
65
42
|
return {
|
|
66
43
|
endpoint: endpoint.path,
|
|
67
44
|
method: endpoint.method,
|
|
68
|
-
sampleData,
|
|
45
|
+
sampleData: endpoint.responseSchema ?? {},
|
|
69
46
|
delayMs: 250,
|
|
70
47
|
errorResponses: getErrorResponsesForEndpoint(endpoint.statusCodes),
|
|
71
48
|
};
|
|
@@ -73,15 +50,14 @@ function buildMockFromEndpoint(endpoint, resource) {
|
|
|
73
50
|
/**
|
|
74
51
|
* Generate mock definitions from a list of endpoints.
|
|
75
52
|
*/
|
|
76
|
-
export function generateMockDefinitions(endpoints,
|
|
77
|
-
|
|
78
|
-
.toLowerCase()
|
|
79
|
-
.replace(/[^a-z0-9]/g, '-')
|
|
80
|
-
.replace(/-+/g, '-');
|
|
81
|
-
return endpoints.map((ep) => buildMockFromEndpoint(ep, resource));
|
|
53
|
+
export function generateMockDefinitions(endpoints, _specTitle) {
|
|
54
|
+
return endpoints.map((ep) => buildMockFromEndpoint(ep));
|
|
82
55
|
}
|
|
83
56
|
// === MSW (Mock Service Worker) content generation ===
|
|
84
57
|
function buildMswMockContent(mocks, specTitle) {
|
|
58
|
+
if (mocks.length === 0) {
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
85
61
|
const handlerLines = [];
|
|
86
62
|
for (const mock of mocks) {
|
|
87
63
|
const method = mock.method.toLowerCase();
|
|
@@ -109,6 +85,9 @@ ${mocks
|
|
|
109
85
|
}
|
|
110
86
|
// === json-server content generation ===
|
|
111
87
|
function buildJsonServerContent(mocks, specTitle) {
|
|
88
|
+
if (mocks.length === 0) {
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
112
91
|
const db = {};
|
|
113
92
|
const resource = specTitle
|
|
114
93
|
.toLowerCase()
|
|
@@ -133,6 +112,9 @@ ${JSON.stringify(db, null, 2)}
|
|
|
133
112
|
}
|
|
134
113
|
// === WireMock content generation ===
|
|
135
114
|
function buildWiremockContent(mocks, _specTitle) {
|
|
115
|
+
if (mocks.length === 0) {
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
136
118
|
const stubs = mocks.map((mock) => ({
|
|
137
119
|
request: {
|
|
138
120
|
method: mock.method,
|
|
@@ -56,10 +56,16 @@ export function buildContractMockSummaryLines(specTitle, knowledge) {
|
|
|
56
56
|
'### Mock Server',
|
|
57
57
|
'',
|
|
58
58
|
`**Tool:** ${tool} (auto-detected)`,
|
|
59
|
-
`**Mocks:** ${endpoints.length} endpoint mocks
|
|
60
|
-
|
|
59
|
+
`**Mocks:** ${endpoints.length} endpoint mocks generated from explicit contracts`,
|
|
60
|
+
...(endpoints.length === 0
|
|
61
|
+
? [
|
|
62
|
+
'> No endpoints were provided; Planu did not fabricate API paths, status codes, or mock payloads.',
|
|
63
|
+
]
|
|
64
|
+
: []),
|
|
61
65
|
'',
|
|
62
|
-
|
|
66
|
+
...(endpoints.length > 0
|
|
67
|
+
? ['> Frontend developers can use these mocks to work independently of the backend.']
|
|
68
|
+
: []),
|
|
63
69
|
'',
|
|
64
70
|
];
|
|
65
71
|
return lines;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPEC-715: heal_spec_docs is now the SINGLE tool that performs migrations.
|
|
3
3
|
// list_specs is read-only; migrations were moved here.
|
|
4
4
|
// SPEC-724: Added dryRun, backup flags and Jaccard goal-scenario drift warning.
|
|
5
|
-
import { readFile, readdir } from 'node:fs/promises';
|
|
5
|
+
import { readFile, readdir, rm } from 'node:fs/promises';
|
|
6
6
|
import { atomicWriteFile } from '../engine/safety/atomic-write-file.js';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { hashProjectPath } from '../storage/base-store.js';
|
|
@@ -46,33 +46,13 @@ function isPlaceholderTechnical(content) {
|
|
|
46
46
|
}
|
|
47
47
|
return false;
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
function buildFileEntries(type, isDone) {
|
|
53
|
-
const status = isDone ? '(done)' : '(pending)';
|
|
54
|
-
// SPEC-579: never fabricate paths from title slug — produces garbage like
|
|
55
|
-
// `src/tools/<spanish-title-slug>.ts` for paths that do not exist.
|
|
56
|
-
// Always return placeholders; real paths are populated manually or by
|
|
57
|
-
// detection tools (scan_project, link_pr_to_spec, sync_spec_from_code).
|
|
58
|
-
if (type === 'refactor') {
|
|
59
|
-
return [
|
|
60
|
-
`- modify: (to be determined by implementation) ${status}`,
|
|
61
|
-
`- test: (to be determined) ${status}`,
|
|
62
|
-
];
|
|
63
|
-
}
|
|
64
|
-
if (type === 'bugfix' || type === 'fix') {
|
|
65
|
-
return [
|
|
66
|
-
`- modify: (affected files TBD) ${status}`,
|
|
67
|
-
`- test: tests/ (regression test) ${status}`,
|
|
68
|
-
];
|
|
69
|
-
}
|
|
70
|
-
return [`- modify: (to be determined) ${status}`];
|
|
49
|
+
function hasInlineTechnicalFilesPlaceholder(content) {
|
|
50
|
+
return /^##\s+Technical\b[\s\S]*?^##\s+Files\b[\s\S]*?(?:\(pending\)|\(to be determined\)|\bTBD\b|--[a-z])/im.test(content);
|
|
71
51
|
}
|
|
72
52
|
// ---------------------------------------------------------------------------
|
|
73
|
-
//
|
|
53
|
+
// File entry generation
|
|
74
54
|
// ---------------------------------------------------------------------------
|
|
75
|
-
async function writeTechnicalMd(specDir,
|
|
55
|
+
async function writeTechnicalMd(specDir, isDone, projectPath, specBody) {
|
|
76
56
|
// SPEC-586: Try to extract real file paths from spec body before using placeholders
|
|
77
57
|
let extractedBody = null;
|
|
78
58
|
if (projectPath && specBody !== undefined) {
|
|
@@ -102,18 +82,13 @@ async function writeTechnicalMd(specDir, specId, type, isDone, projectPath, spec
|
|
|
102
82
|
// best-effort — fall through to placeholder
|
|
103
83
|
}
|
|
104
84
|
}
|
|
105
|
-
const fileEntries = buildFileEntries(type, isDone);
|
|
106
|
-
// Build the body for the ## Technical section (no outer heading — replaceSectionInSpec adds it)
|
|
107
|
-
const technicalBody = extractedBody ??
|
|
108
|
-
[
|
|
109
|
-
`## Files — ${specId}`,
|
|
110
|
-
...fileEntries,
|
|
111
|
-
'',
|
|
112
|
-
'## Notes',
|
|
113
|
-
`> Auto-generated by heal_spec_docs (SPEC-493). Review and update paths as needed.`,
|
|
114
|
-
'',
|
|
115
|
-
].join('\n');
|
|
116
85
|
const specMdPath = join(specDir, 'spec.md');
|
|
86
|
+
if (extractedBody === null) {
|
|
87
|
+
const removedInline = await removeTechnicalSectionIfPlaceholder(specMdPath);
|
|
88
|
+
const removedLegacy = await removeLegacyTechnicalPlaceholder(join(specDir, 'technical.md'));
|
|
89
|
+
return removedInline || removedLegacy;
|
|
90
|
+
}
|
|
91
|
+
const technicalBody = extractedBody;
|
|
117
92
|
// SPEC-579: idempotency check — skip write if section already matches.
|
|
118
93
|
try {
|
|
119
94
|
const existing = await readFile(specMdPath, 'utf-8');
|
|
@@ -128,13 +103,74 @@ async function writeTechnicalMd(specDir, specId, type, isDone, projectPath, spec
|
|
|
128
103
|
await replaceSectionInSpec(specMdPath, 'Technical', technicalBody);
|
|
129
104
|
return true;
|
|
130
105
|
}
|
|
106
|
+
async function removeLegacyTechnicalPlaceholder(technicalPath) {
|
|
107
|
+
let legacy;
|
|
108
|
+
try {
|
|
109
|
+
legacy = await readFile(technicalPath, 'utf-8');
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
if (!isPlaceholderTechnical(legacy)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
await rm(technicalPath, { force: true });
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
async function removeTechnicalSectionIfPlaceholder(specMdPath) {
|
|
121
|
+
let existing;
|
|
122
|
+
try {
|
|
123
|
+
existing = await readFile(specMdPath, 'utf-8');
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const currentTechnical = extractTechnicalBodyLocal(existing);
|
|
129
|
+
if (currentTechnical === '' || !isPlaceholderTechnical(currentTechnical)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const updated = removeTopLevelSection(existing, 'Technical');
|
|
133
|
+
if (updated === existing) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
await atomicWriteFile(specMdPath, updated);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
function removeTopLevelSection(content, sectionName) {
|
|
140
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
141
|
+
const masked = normalized.replace(/(```|~~~)[\s\S]*?\1/g, (block) => block.replace(/[^\n]/g, ' '));
|
|
142
|
+
const headingRe = new RegExp(`^##\\s+${escapeRegex(sectionName)}\\b.*$`, 'm');
|
|
143
|
+
const match = headingRe.exec(masked);
|
|
144
|
+
if (!match) {
|
|
145
|
+
return content;
|
|
146
|
+
}
|
|
147
|
+
const afterHeading = match.index + match[0].length;
|
|
148
|
+
const nextRe = /^##\s+\S/gm;
|
|
149
|
+
nextRe.lastIndex = afterHeading;
|
|
150
|
+
const next = nextRe.exec(masked);
|
|
151
|
+
const before = normalized.slice(0, match.index).trimEnd();
|
|
152
|
+
const after = next ? normalized.slice(next.index).trimStart() : '';
|
|
153
|
+
if (before.length === 0) {
|
|
154
|
+
return after.length > 0 ? `${after.trimEnd()}\n` : '';
|
|
155
|
+
}
|
|
156
|
+
if (after.length === 0) {
|
|
157
|
+
return `${before}\n`;
|
|
158
|
+
}
|
|
159
|
+
return `${before}\n\n${after.trimEnd()}\n`;
|
|
160
|
+
}
|
|
161
|
+
function escapeRegex(input) {
|
|
162
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
163
|
+
}
|
|
131
164
|
/** Local extraction of ## Technical body to avoid circular imports. */
|
|
132
165
|
function extractTechnicalBodyLocal(content) {
|
|
166
|
+
return extractSectionBodyLocal(content, 'Technical') ?? '';
|
|
167
|
+
}
|
|
168
|
+
function extractSectionBodyLocal(content, sectionName) {
|
|
133
169
|
const normalized = content.replace(/\r\n/g, '\n');
|
|
134
170
|
const masked = normalized.replace(/(```|~~~)[\s\S]*?\1/g, (block) => block.replace(/[^\n]/g, ' '));
|
|
135
|
-
const headingMatch =
|
|
171
|
+
const headingMatch = new RegExp(`^##\\s+${escapeRegex(sectionName)}\\b.*$`, 'm').exec(masked);
|
|
136
172
|
if (!headingMatch) {
|
|
137
|
-
return
|
|
173
|
+
return null;
|
|
138
174
|
}
|
|
139
175
|
const afterHeading = headingMatch.index + headingMatch[0].length;
|
|
140
176
|
const nextRe = /^##\s+\S/gm;
|
|
@@ -271,9 +307,11 @@ export async function handleHealSpecDocs(params) {
|
|
|
271
307
|
let legacyContent = null;
|
|
272
308
|
// Determine whether healing is needed
|
|
273
309
|
let needsHeal;
|
|
310
|
+
const inlinePlaceholder = technicalInSpecMd !== null &&
|
|
311
|
+
(isPlaceholderTechnical(technicalInSpecMd) || hasInlineTechnicalFilesPlaceholder(spec.body));
|
|
274
312
|
if (technicalInSpecMd !== null) {
|
|
275
313
|
// Check if the inline section is a placeholder
|
|
276
|
-
needsHeal =
|
|
314
|
+
needsHeal = inlinePlaceholder;
|
|
277
315
|
}
|
|
278
316
|
else {
|
|
279
317
|
// No inline section — try standalone technical.md (legacy)
|
|
@@ -326,9 +364,27 @@ export async function handleHealSpecDocs(params) {
|
|
|
326
364
|
}
|
|
327
365
|
}
|
|
328
366
|
}
|
|
367
|
+
if (inlinePlaceholder) {
|
|
368
|
+
const specContent = await readFile(specMdPath, 'utf-8').catch(() => '');
|
|
369
|
+
let updated = removeTopLevelSection(specContent, 'Technical');
|
|
370
|
+
const filesBody = extractSectionBodyLocal(updated, 'Files');
|
|
371
|
+
if (filesBody !== null && isPlaceholderTechnical(filesBody)) {
|
|
372
|
+
updated = removeTopLevelSection(updated, 'Files');
|
|
373
|
+
}
|
|
374
|
+
if (updated !== specContent) {
|
|
375
|
+
await atomicWriteFile(specMdPath, updated);
|
|
376
|
+
healed += 1;
|
|
377
|
+
}
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (legacyContent !== null && isPlaceholderTechnical(legacyContent)) {
|
|
381
|
+
await rm(technicalPath, { force: true });
|
|
382
|
+
healed += 1;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
329
385
|
const isDone = spec.status === 'done' || spec.status === 'implemented';
|
|
330
386
|
try {
|
|
331
|
-
const wrote = await writeTechnicalMd(spec.dir,
|
|
387
|
+
const wrote = await writeTechnicalMd(spec.dir, isDone, projectPath, spec.body);
|
|
332
388
|
if (wrote) {
|
|
333
389
|
healed += 1;
|
|
334
390
|
}
|
|
@@ -1,41 +1,20 @@
|
|
|
1
1
|
// tools/migrate-tech/core-handlers.ts — Handlers: analyze | map | plan | strategy | validate
|
|
2
2
|
import { t } from '../../i18n/index.js';
|
|
3
|
-
import { buildStackAnalysis,
|
|
3
|
+
import { buildStackAnalysis, buildEquivalenceMap, generateMigrationPlan, recommendMigrationStrategy, generateParityValidation, } from '../../engine/migration/index.js';
|
|
4
4
|
export function handleAnalyze(args) {
|
|
5
|
-
const components = scoreMigratableComponents([
|
|
6
|
-
{
|
|
7
|
-
id: 'app-core',
|
|
8
|
-
name: 'Application Core',
|
|
9
|
-
path: args.projectPath ?? './src',
|
|
10
|
-
type: 'module',
|
|
11
|
-
cyclomaticComplexity: 8,
|
|
12
|
-
couplingScore: 5,
|
|
13
|
-
dependsOn: [],
|
|
14
|
-
dependents: ['api-handler'],
|
|
15
|
-
linesOfCode: 400,
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
id: 'api-handler',
|
|
19
|
-
name: 'API Handler',
|
|
20
|
-
path: args.projectPath ? `${args.projectPath}/api` : './src/api',
|
|
21
|
-
type: 'handler',
|
|
22
|
-
cyclomaticComplexity: 12,
|
|
23
|
-
couplingScore: 6,
|
|
24
|
-
dependsOn: ['app-core'],
|
|
25
|
-
dependents: [],
|
|
26
|
-
linesOfCode: 250,
|
|
27
|
-
},
|
|
28
|
-
]);
|
|
29
5
|
const data = buildStackAnalysis({
|
|
30
6
|
language: args.sourceStack ?? 'unknown',
|
|
31
7
|
framework: args.sourceStack ?? 'unknown',
|
|
32
8
|
components: [],
|
|
33
|
-
entryPoints: [
|
|
9
|
+
entryPoints: [],
|
|
34
10
|
externalIntegrations: [],
|
|
35
|
-
migratableComponents:
|
|
11
|
+
migratableComponents: [],
|
|
36
12
|
hasTests: false,
|
|
37
13
|
});
|
|
38
|
-
const warnings =
|
|
14
|
+
const warnings = [
|
|
15
|
+
...data.riskFlags,
|
|
16
|
+
'No project-derived migration inventory was provided; analysis does not fabricate components or entry points.',
|
|
17
|
+
];
|
|
39
18
|
return {
|
|
40
19
|
action: 'analyze',
|
|
41
20
|
data,
|
|
@@ -60,48 +39,15 @@ export function handleMap(args) {
|
|
|
60
39
|
};
|
|
61
40
|
}
|
|
62
41
|
export function handlePlan(args) {
|
|
63
|
-
const components = scoreMigratableComponents([
|
|
64
|
-
{
|
|
65
|
-
id: 'utils',
|
|
66
|
-
name: 'Utils',
|
|
67
|
-
path: './src/utils',
|
|
68
|
-
type: 'util',
|
|
69
|
-
cyclomaticComplexity: 3,
|
|
70
|
-
couplingScore: 1,
|
|
71
|
-
dependsOn: [],
|
|
72
|
-
dependents: ['service'],
|
|
73
|
-
linesOfCode: 100,
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
id: 'service',
|
|
77
|
-
name: 'Service',
|
|
78
|
-
path: './src/service',
|
|
79
|
-
type: 'service',
|
|
80
|
-
cyclomaticComplexity: 10,
|
|
81
|
-
couplingScore: 4,
|
|
82
|
-
dependsOn: ['utils'],
|
|
83
|
-
dependents: ['handler'],
|
|
84
|
-
linesOfCode: 300,
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
id: 'handler',
|
|
88
|
-
name: 'Handler',
|
|
89
|
-
path: './src/handler',
|
|
90
|
-
type: 'handler',
|
|
91
|
-
cyclomaticComplexity: 6,
|
|
92
|
-
couplingScore: 3,
|
|
93
|
-
dependsOn: ['service'],
|
|
94
|
-
dependents: [],
|
|
95
|
-
linesOfCode: 150,
|
|
96
|
-
},
|
|
97
|
-
]);
|
|
98
42
|
const data = generateMigrationPlan({
|
|
99
43
|
sourceStack: args.sourceStack ?? 'unknown',
|
|
100
44
|
targetStack: args.targetStack ?? 'unknown',
|
|
101
|
-
components,
|
|
45
|
+
components: [],
|
|
102
46
|
hasTests: false,
|
|
103
47
|
});
|
|
104
|
-
const warnings = [
|
|
48
|
+
const warnings = [
|
|
49
|
+
'No project-derived migration components were provided; plan does not fabricate modules.',
|
|
50
|
+
];
|
|
105
51
|
if (data.circularDependencies.length > 0) {
|
|
106
52
|
warnings.push(`${data.circularDependencies.length} circular dependency cycles detected — resolve before migrating`);
|
|
107
53
|
}
|
|
@@ -114,32 +60,38 @@ export function handlePlan(args) {
|
|
|
114
60
|
}
|
|
115
61
|
export function handleStrategy(_args) {
|
|
116
62
|
const data = recommendMigrationStrategy({
|
|
117
|
-
componentCount:
|
|
118
|
-
hasActiveUsers:
|
|
63
|
+
componentCount: 0,
|
|
64
|
+
hasActiveUsers: false,
|
|
119
65
|
hasHighSla: false,
|
|
120
66
|
hasCriticalData: false,
|
|
121
67
|
migratableComponents: [],
|
|
122
|
-
entryPoints: [
|
|
68
|
+
entryPoints: [],
|
|
123
69
|
});
|
|
124
70
|
return {
|
|
125
71
|
action: 'strategy',
|
|
126
72
|
data,
|
|
127
73
|
message: t('tools.migrate_tech.strategySuccess'),
|
|
128
|
-
warnings: [
|
|
74
|
+
warnings: [
|
|
75
|
+
'No project-derived strategy inputs were provided; recommendation uses neutral empty evidence.',
|
|
76
|
+
],
|
|
129
77
|
};
|
|
130
78
|
}
|
|
131
79
|
export function handleValidate(args) {
|
|
132
|
-
const component = args.component ?? '
|
|
80
|
+
const component = args.component?.trim() ?? '';
|
|
133
81
|
const data = generateParityValidation({
|
|
134
82
|
component,
|
|
135
|
-
endpoints: [
|
|
83
|
+
endpoints: [],
|
|
136
84
|
observedDifferences: [],
|
|
137
85
|
});
|
|
138
86
|
return {
|
|
139
87
|
action: 'validate',
|
|
140
88
|
data,
|
|
141
89
|
message: t('tools.migrate_tech.validateSuccess'),
|
|
142
|
-
warnings:
|
|
90
|
+
warnings: args.component === undefined
|
|
91
|
+
? [
|
|
92
|
+
'No component was provided; validation checklist is unscoped and no contract tests were generated.',
|
|
93
|
+
]
|
|
94
|
+
: ['No endpoints were provided; contract tests were not generated.'],
|
|
143
95
|
};
|
|
144
96
|
}
|
|
145
97
|
//# sourceMappingURL=core-handlers.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.22",
|
|
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",
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
"packageName": "@planu/core"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
|
-
"@planu/core-darwin-arm64": "4.3.
|
|
38
|
-
"@planu/core-darwin-x64": "4.3.
|
|
39
|
-
"@planu/core-linux-arm64-gnu": "4.3.
|
|
40
|
-
"@planu/core-linux-arm64-musl": "4.3.
|
|
41
|
-
"@planu/core-linux-x64-gnu": "4.3.
|
|
42
|
-
"@planu/core-linux-x64-musl": "4.3.
|
|
43
|
-
"@planu/core-win32-arm64-msvc": "4.3.
|
|
44
|
-
"@planu/core-win32-x64-msvc": "4.3.
|
|
37
|
+
"@planu/core-darwin-arm64": "4.3.22",
|
|
38
|
+
"@planu/core-darwin-x64": "4.3.22",
|
|
39
|
+
"@planu/core-linux-arm64-gnu": "4.3.22",
|
|
40
|
+
"@planu/core-linux-arm64-musl": "4.3.22",
|
|
41
|
+
"@planu/core-linux-x64-gnu": "4.3.22",
|
|
42
|
+
"@planu/core-linux-x64-musl": "4.3.22",
|
|
43
|
+
"@planu/core-win32-arm64-msvc": "4.3.22",
|
|
44
|
+
"@planu/core-win32-x64-msvc": "4.3.22"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=24.0.0"
|
package/planu-native.json
CHANGED
package/planu-plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dev.planu.cli",
|
|
3
3
|
"displayName": "Planu — Spec Driven Development",
|
|
4
4
|
"description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
|
|
5
|
-
"version": "4.3.
|
|
5
|
+
"version": "4.3.22",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": [
|
|
8
8
|
"npx",
|