@planu/cli 4.3.20 → 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 +12 -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/spec-generator/fallback-generator.js +4 -30
- 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/bump-spec-version.js +2 -1
- package/dist/tools/create-spec.js +30 -10
- package/dist/tools/generate-batch-script.js +2 -1
- package/dist/tools/generate-proposal.js +2 -1
- 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/dist/tools/register-agent-squad-tools.js +2 -1
- package/dist/tools/register-platform-tools/design-stack-tools.js +10 -10
- package/dist/tools/register-platform-tools/lifecycle-infra-tools.js +8 -8
- package/dist/tools/register-spec-tools/analysis-tools.js +7 -7
- package/dist/tools/register-spec-tools/core-spec-tools.js +6 -6
- package/dist/tools/render-spec-for-provider.js +2 -1
- package/dist/tools/schemas/github.js +4 -3
- package/dist/tools/schemas/index.d.ts +1 -0
- package/dist/tools/schemas/index.js +1 -0
- package/dist/tools/schemas/spec-id.d.ts +7 -0
- package/dist/tools/schemas/spec-id.js +17 -0
- package/dist/tools/spec-prompt-handler.js +2 -1
- package/dist/tools/tool-registry/core-tools.js +3 -2
- package/dist/tools/tool-registry/group-infra.js +3 -3
- package/dist/tools/tool-registry/group-quality-compliance.js +11 -14
- package/package.json +9 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [4.3.22] - 2026-06-02
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix: stop fabricating spec and test artifacts
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## [4.3.21] - 2026-06-02
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
- fix: enforce canonical spec ids and harden fallback specs
|
|
11
|
+
|
|
12
|
+
|
|
1
13
|
## [4.3.20] - 2026-05-27
|
|
2
14
|
|
|
3
15
|
### Bug Fixes
|
|
@@ -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
|
}
|
|
@@ -2,17 +2,14 @@ export class FallbackGenerator {
|
|
|
2
2
|
generate(request) {
|
|
3
3
|
const sourceDescription = normalizeSourceDescription(request.description, request.title);
|
|
4
4
|
const contextLine = buildContextLine(request);
|
|
5
|
-
const technicalSection =
|
|
5
|
+
const technicalSection = '';
|
|
6
6
|
const specBody = [
|
|
7
7
|
'## Problem',
|
|
8
8
|
sourceDescription,
|
|
9
9
|
'',
|
|
10
10
|
'## Solution',
|
|
11
|
-
'Use the preserved source description above as the authoritative requirements body.
|
|
12
|
-
|
|
13
|
-
contextLine,
|
|
14
|
-
'',
|
|
15
|
-
technicalSection,
|
|
11
|
+
'Use the preserved source description above as the authoritative requirements body.',
|
|
12
|
+
contextLine,
|
|
16
13
|
].join('\n');
|
|
17
14
|
return Promise.resolve({
|
|
18
15
|
specBody,
|
|
@@ -20,7 +17,7 @@ export class FallbackGenerator {
|
|
|
20
17
|
generatedWithModel: 'deterministic-fallback',
|
|
21
18
|
generatedAt: new Date().toISOString(),
|
|
22
19
|
qualityWarnings: [
|
|
23
|
-
'Fallback generator used. Source description was preserved;
|
|
20
|
+
'Fallback generator used. Source description was preserved; technical ownership must come from explicit source paths or local project analysis.',
|
|
24
21
|
],
|
|
25
22
|
fallbackReason: 'No external model generator is configured for create_spec.',
|
|
26
23
|
});
|
|
@@ -44,27 +41,4 @@ function buildContextLine(request) {
|
|
|
44
41
|
}
|
|
45
42
|
return `Use the existing ${parts.join(' / ')} project conventions.`;
|
|
46
43
|
}
|
|
47
|
-
function buildTechnicalSection(contextLine) {
|
|
48
|
-
return [
|
|
49
|
-
'## Technical',
|
|
50
|
-
'',
|
|
51
|
-
'### Implementation Notes',
|
|
52
|
-
`- ${contextLine}`,
|
|
53
|
-
'- Preserve the source requirements as the contract until a reviewer or agent adds exact implementation ownership.',
|
|
54
|
-
'',
|
|
55
|
-
'## Files',
|
|
56
|
-
'',
|
|
57
|
-
'### Create',
|
|
58
|
-
'- _Not specified in source description._',
|
|
59
|
-
'### Modify',
|
|
60
|
-
'- _Not specified in source description._',
|
|
61
|
-
'### Test',
|
|
62
|
-
'- _Not specified in source description._',
|
|
63
|
-
'',
|
|
64
|
-
'## Verification',
|
|
65
|
-
'- pnpm typecheck',
|
|
66
|
-
'- pnpm lint',
|
|
67
|
-
'- pnpm test',
|
|
68
|
-
].join('\n');
|
|
69
|
-
}
|
|
70
44
|
//# sourceMappingURL=fallback-generator.js.map
|
|
@@ -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,
|
|
@@ -7,6 +7,7 @@ import { getSpec } from '../storage/spec-store.js';
|
|
|
7
7
|
import { safeTracked } from './safe-handler.js';
|
|
8
8
|
import { bumpSpecVersion } from '../engine/spec-versioning/bump-version.js';
|
|
9
9
|
import { resolveProjectId } from './resolve-project-id.js';
|
|
10
|
+
import { SpecIdSchema } from './schemas/index.js';
|
|
10
11
|
// SPEC-747: Grant unlock token after successful bump
|
|
11
12
|
import { grantUnlock } from '../engine/freeze/unlock-token.js';
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
@@ -25,7 +26,7 @@ const BumpSpecVersionSchema = {
|
|
|
25
26
|
.max(500)
|
|
26
27
|
.optional()
|
|
27
28
|
.describe('Project ID (hash). Required if projectPath is not provided.'),
|
|
28
|
-
specId:
|
|
29
|
+
specId: SpecIdSchema.describe('Spec ID to bump, e.g. SPEC-042.'),
|
|
29
30
|
kind: z
|
|
30
31
|
.enum(['patch', 'minor', 'major'])
|
|
31
32
|
.describe('Bump kind: patch (typo/format), minor (new criterion/scope expansion), major (breaking change).'),
|
|
@@ -16,7 +16,8 @@ import { compactObj } from '../engine/compact-obj.js';
|
|
|
16
16
|
import { buildCreateSpecSummary } from '../engine/human-summary.js';
|
|
17
17
|
import { runAutoPostCreatePipeline } from './create-spec/auto-pipeline.js';
|
|
18
18
|
import { generateLeanSpecContent } from '../engine/spec-format/lean-spec-generator.js';
|
|
19
|
-
import { generateLeanTechnicalContent } from '../engine/spec-format/lean-technical-generator.js';
|
|
19
|
+
import { generateLeanTechnicalContent, } from '../engine/spec-format/lean-technical-generator.js';
|
|
20
|
+
import { extractFilesFromSpecBody } from '../engine/spec-format/technical-md-populator.js';
|
|
20
21
|
import { buildUnifiedSpecContent } from '../engine/spec-format/unified-spec-builder.js';
|
|
21
22
|
import { validateEnglishOnlySpecText } from '../engine/spec-language/english-only.js';
|
|
22
23
|
import { ApiKeyResolver, FallbackGenerator, OpusGenerator, } from '../engine/spec-generator/index.js';
|
|
@@ -104,6 +105,22 @@ function handleClarification(_server, description, knowledge, autopilot, params)
|
|
|
104
105
|
earlyReturn: interactiveResult(questions, 'The description needs more detail before a spec can be created. Answer the questions, then retry create_spec with clarificationAnswers or an enriched description.', 'universal'),
|
|
105
106
|
};
|
|
106
107
|
}
|
|
108
|
+
function hasFileEntries(files) {
|
|
109
|
+
return files.create.length + files.modify.length + files.test.length > 0;
|
|
110
|
+
}
|
|
111
|
+
async function resolveTechnicalFiles(input) {
|
|
112
|
+
const generatedSource = [input.generatedTechnicalSection, input.generatedSpecBody]
|
|
113
|
+
.filter((part) => part.trim().length > 0)
|
|
114
|
+
.join('\n\n');
|
|
115
|
+
const extracted = await extractFilesFromSpecBody(generatedSource, input.projectPath);
|
|
116
|
+
if (extracted !== null && hasFileEntries(extracted)) {
|
|
117
|
+
return extracted;
|
|
118
|
+
}
|
|
119
|
+
if (input.fallbackReason !== undefined && hasFileEntries(input.autopilot.suggestedFiles)) {
|
|
120
|
+
return input.autopilot.suggestedFiles;
|
|
121
|
+
}
|
|
122
|
+
return { create: [], modify: [], test: [] };
|
|
123
|
+
}
|
|
107
124
|
const HIGH_RISK_WARNING = 'High-risk spec — consider: ' +
|
|
108
125
|
'1) Is the approach technically feasible within the project constraints? ' +
|
|
109
126
|
'2) What edge cases (network failure, concurrent writes, empty state) could break this? ' +
|
|
@@ -558,15 +575,18 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
558
575
|
extraCriteria: filteredCriteria,
|
|
559
576
|
acFormat: params.acFormat,
|
|
560
577
|
});
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
578
|
+
const technicalFiles = await measureStep('resolveTechnicalFiles', () => resolveTechnicalFiles({
|
|
579
|
+
generatedSpecBody: generatedSpec.specBody,
|
|
580
|
+
generatedTechnicalSection: generatedSpec.technicalSection,
|
|
581
|
+
projectPath: params.projectPath ?? '',
|
|
582
|
+
autopilot,
|
|
583
|
+
fallbackReason: generatedSpec.fallbackReason,
|
|
584
|
+
}));
|
|
565
585
|
const leanTechnical = generateLeanTechnicalContent({
|
|
566
586
|
specId: spec.id,
|
|
567
|
-
filesToCreate:
|
|
568
|
-
filesToModify:
|
|
569
|
-
filesToTest:
|
|
587
|
+
filesToCreate: technicalFiles.create,
|
|
588
|
+
filesToModify: technicalFiles.modify,
|
|
589
|
+
filesToTest: technicalFiles.test,
|
|
570
590
|
});
|
|
571
591
|
// SPEC-709: write unified spec.md from origin — no separate technical.md.
|
|
572
592
|
// The legacy two-file output is preserved by appending the technical body
|
|
@@ -1071,8 +1091,8 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1071
1091
|
catch {
|
|
1072
1092
|
// best-effort — token issuance failure must not block spec creation
|
|
1073
1093
|
}
|
|
1074
|
-
// SPEC-1011 Bug E: surface
|
|
1075
|
-
//
|
|
1094
|
+
// SPEC-1011 Bug E / fallback hardening: surface local file analysis in the response payload.
|
|
1095
|
+
// Those paths are also written into ## Files only when used as technical evidence.
|
|
1076
1096
|
const suggestedFilesPayload = autopilot.suggestedFiles.create.length +
|
|
1077
1097
|
autopilot.suggestedFiles.modify.length +
|
|
1078
1098
|
autopilot.suggestedFiles.test.length >
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// tools/generate-batch-script.ts — Handle generate_batch_script tool calls (SPEC-253)
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { generateBatchScript } from '../engine/batch-script-generator/index.js';
|
|
4
|
+
import { SpecIdSchema } from './schemas/index.js';
|
|
4
5
|
export const GenerateBatchScriptInputSchema = z.object({
|
|
5
6
|
projectPath: z.string().describe('Absolute path to the project root'),
|
|
6
7
|
taskDescription: z.string().describe('What claude -p should do to each file'),
|
|
7
8
|
fileGlob: z.string().describe("Glob pattern for files, e.g. 'src/**/*.ts'"),
|
|
8
|
-
specId:
|
|
9
|
+
specId: SpecIdSchema.optional().describe('SPEC ID for naming the output script, e.g. SPEC-253'),
|
|
9
10
|
allowedTools: z
|
|
10
11
|
.array(z.string())
|
|
11
12
|
.optional()
|
|
@@ -6,10 +6,11 @@ import { hashProjectPath, projectDataDir } from '../storage/base-store.js';
|
|
|
6
6
|
import { specStore, knowledgeStore, decisionStore, metricsStore } from '../storage/index.js';
|
|
7
7
|
import { loadAllRiskRegisters } from '../storage/risk-store.js';
|
|
8
8
|
import { generateProposal } from '../engine/doc-generator/proposal/proposal-generator.js';
|
|
9
|
+
import { SpecIdSchema } from './schemas/index.js';
|
|
9
10
|
// ── Zod schema ────────────────────────────────────────────────────────────────
|
|
10
11
|
const PhaseSchema = z.object({
|
|
11
12
|
name: z.string().describe('Phase name (e.g. "Phase 1 — Core", "MVP")'),
|
|
12
|
-
specIds: z.array(
|
|
13
|
+
specIds: z.array(SpecIdSchema).describe('List of spec IDs included in this phase'),
|
|
13
14
|
color: z
|
|
14
15
|
.string()
|
|
15
16
|
.optional()
|
|
@@ -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
|
}
|