@planu/cli 5.3.25 → 5.3.27
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 +19 -0
- package/README.md +1 -1
- package/dist/config/legacy-artifacts.json +26 -0
- package/dist/config/official-sdd-tools.d.ts +1 -1
- package/dist/config/official-sdd-tools.js +1 -0
- package/dist/config/registries/hosts/codex.json +2 -1
- package/dist/engine/handoff-artifacts/io.js +1 -0
- package/dist/engine/handoff-artifacts/schemas.d.ts +46 -0
- package/dist/engine/handoff-artifacts/schemas.js +15 -0
- package/dist/engine/lifecycle-reconciliation.js +6 -5
- package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-arm64.node.sbom.json +4 -4
- package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-x64.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +4 -4
- package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +4 -4
- package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +4 -4
- package/dist/engine/scope-boundaries/contradiction-checker.js +9 -3
- package/dist/engine/spec-format/read-technical-section.js +3 -2
- package/dist/engine/spec-format/unified-spec-builder.js +27 -0
- package/dist/engine/spec-migrator/index.d.ts +0 -1
- package/dist/engine/spec-migrator/index.js +0 -1
- package/dist/engine/spec-migrator/legacy-classifier.d.ts +4 -0
- package/dist/engine/spec-migrator/legacy-classifier.js +70 -0
- package/dist/engine/spec-migrator/planu-canonical-policy.js +14 -9
- package/dist/engine/spec-migrator/strict-planu-cleanup.d.ts +4 -2
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +64 -53
- package/dist/engine/validator/validation-report-writer.d.ts +1 -0
- package/dist/engine/validator/validation-report-writer.js +3 -2
- package/dist/tools/heal-spec-docs.js +15 -12
- package/dist/tools/init-project/git-setup.js +44 -40
- package/dist/tools/init-project/handler.js +2 -1
- package/dist/tools/init-project/migration-runner.d.ts +2 -1
- package/dist/tools/init-project/migration-runner.js +18 -5
- package/dist/tools/init-project/result-builder.js +9 -0
- package/dist/tools/list-specs.js +39 -0
- package/dist/tools/migrate-legacy-spec.d.ts +10 -0
- package/dist/tools/migrate-legacy-spec.js +133 -0
- package/dist/tools/register-spec-tools/analysis-tools.js +2 -0
- package/dist/tools/schemas/output-schemas.d.ts +28 -0
- package/dist/tools/schemas/output-schemas.js +13 -0
- package/dist/tools/update-status/dod-gates.d.ts +1 -0
- package/dist/tools/update-status/dod-gates.js +123 -4
- package/dist/types/handoff-artifacts.d.ts +17 -1
- package/dist/types/project/planu-config.d.ts +3 -0
- package/dist/types/spec/core.d.ts +0 -7
- package/dist/types/spec-format.d.ts +17 -0
- package/dist/types/transition-log.d.ts +1 -1
- package/package.json +9 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +3 -2
- package/dist/engine/spec-migrator/unified-migration.d.ts +0 -18
- package/dist/engine/spec-migrator/unified-migration.js +0 -105
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
|
-
"cliVersion": "5.3.
|
|
4
|
-
"engineVersion": "5.3.
|
|
5
|
-
"buildId": "1-5.3.
|
|
3
|
+
"cliVersion": "5.3.27",
|
|
4
|
+
"engineVersion": "5.3.27",
|
|
5
|
+
"buildId": "1-5.3.27-aarch64-pc-windows-msvc",
|
|
6
6
|
"protocolAbi": 1,
|
|
7
7
|
"capabilityAbi": 2,
|
|
8
8
|
"nodeApiAbi": 4,
|
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
"platform": "win32",
|
|
17
17
|
"arch": "arm64",
|
|
18
18
|
"libc": null,
|
|
19
|
-
"artifactSha256": "
|
|
19
|
+
"artifactSha256": "b847ff92831078fb9d108cbf191eb39307c49a25d404da070db732dded3fc894",
|
|
20
20
|
"sbom": {
|
|
21
21
|
"format": "CycloneDX-1.5",
|
|
22
22
|
"path": "planu-core.win32-arm64-msvc.node.sbom.json",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "eea89ce4eb73df92230c4507c0a182332df56d16e8a32e5409f34a95a7a7f05f"
|
|
24
24
|
},
|
|
25
25
|
"provenance": {
|
|
26
26
|
"builder": "scripts/build-rust-local.sh",
|
|
27
|
-
"sourceCommit": "
|
|
27
|
+
"sourceCommit": "79788c0b8d9bfbc05294316f3546693af74482ab",
|
|
28
28
|
"sourceTreeDirty": false,
|
|
29
29
|
"sourceDateEpoch": 1,
|
|
30
30
|
"rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
|
|
@@ -33,6 +33,6 @@
|
|
|
33
33
|
"signature": {
|
|
34
34
|
"algorithm": "Ed25519",
|
|
35
35
|
"keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
|
|
36
|
-
"value": "
|
|
36
|
+
"value": "7TTGhX99A+J+lpXk1+fUcDLgxki0Vmnp9nVkfJkEKS0cgGGLAVAnHAEF0uawZjywCJF1MaYSMn6jEmUUqVTJCg=="
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"version": 1,
|
|
5
5
|
"metadata": {
|
|
6
6
|
"component": {
|
|
7
|
-
"bom-ref": "pkg:cargo/planu-core@5.3.
|
|
7
|
+
"bom-ref": "pkg:cargo/planu-core@5.3.27",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.27",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "b847ff92831078fb9d108cbf191eb39307c49a25d404da070db732dded3fc894"
|
|
15
15
|
}
|
|
16
16
|
]
|
|
17
17
|
}
|
|
@@ -1106,7 +1106,7 @@
|
|
|
1106
1106
|
"dependsOn": []
|
|
1107
1107
|
},
|
|
1108
1108
|
{
|
|
1109
|
-
"ref": "pkg:cargo/planu-core@5.3.
|
|
1109
|
+
"ref": "pkg:cargo/planu-core@5.3.27",
|
|
1110
1110
|
"dependsOn": [
|
|
1111
1111
|
"pkg:cargo/core-foundation@0.10.1",
|
|
1112
1112
|
"pkg:cargo/hmac@0.12.1",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
|
-
"cliVersion": "5.3.
|
|
4
|
-
"engineVersion": "5.3.
|
|
5
|
-
"buildId": "1-5.3.
|
|
3
|
+
"cliVersion": "5.3.27",
|
|
4
|
+
"engineVersion": "5.3.27",
|
|
5
|
+
"buildId": "1-5.3.27-x86_64-pc-windows-msvc",
|
|
6
6
|
"protocolAbi": 1,
|
|
7
7
|
"capabilityAbi": 2,
|
|
8
8
|
"nodeApiAbi": 4,
|
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
"platform": "win32",
|
|
17
17
|
"arch": "x64",
|
|
18
18
|
"libc": null,
|
|
19
|
-
"artifactSha256": "
|
|
19
|
+
"artifactSha256": "557925ec6e5f7f32feb1bb42683e058e974b20032ff19a0807a3c57059af748d",
|
|
20
20
|
"sbom": {
|
|
21
21
|
"format": "CycloneDX-1.5",
|
|
22
22
|
"path": "planu-core.win32-x64-msvc.node.sbom.json",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "e61c9fd311db17fcac42bec3d7fa6ab61bf2c7082ac80196bfb6b9ba54977fda"
|
|
24
24
|
},
|
|
25
25
|
"provenance": {
|
|
26
26
|
"builder": "scripts/build-rust-local.sh",
|
|
27
|
-
"sourceCommit": "
|
|
27
|
+
"sourceCommit": "79788c0b8d9bfbc05294316f3546693af74482ab",
|
|
28
28
|
"sourceTreeDirty": false,
|
|
29
29
|
"sourceDateEpoch": 1,
|
|
30
30
|
"rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
|
|
@@ -33,6 +33,6 @@
|
|
|
33
33
|
"signature": {
|
|
34
34
|
"algorithm": "Ed25519",
|
|
35
35
|
"keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
|
|
36
|
-
"value": "
|
|
36
|
+
"value": "O3ZXl5L6qzts4ZRg9WDNIHioY1qH1I/yx9V/aMi0Bb5/Ct3hq+eMS+8njmwY5p9qbhGzQq2yYf4DwJ3vbWH8DQ=="
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"version": 1,
|
|
5
5
|
"metadata": {
|
|
6
6
|
"component": {
|
|
7
|
-
"bom-ref": "pkg:cargo/planu-core@5.3.
|
|
7
|
+
"bom-ref": "pkg:cargo/planu-core@5.3.27",
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "planu-core",
|
|
10
|
-
"version": "5.3.
|
|
10
|
+
"version": "5.3.27",
|
|
11
11
|
"hashes": [
|
|
12
12
|
{
|
|
13
13
|
"alg": "SHA-256",
|
|
14
|
-
"content": "
|
|
14
|
+
"content": "557925ec6e5f7f32feb1bb42683e058e974b20032ff19a0807a3c57059af748d"
|
|
15
15
|
}
|
|
16
16
|
]
|
|
17
17
|
}
|
|
@@ -1106,7 +1106,7 @@
|
|
|
1106
1106
|
"dependsOn": []
|
|
1107
1107
|
},
|
|
1108
1108
|
{
|
|
1109
|
-
"ref": "pkg:cargo/planu-core@5.3.
|
|
1109
|
+
"ref": "pkg:cargo/planu-core@5.3.27",
|
|
1110
1110
|
"dependsOn": [
|
|
1111
1111
|
"pkg:cargo/core-foundation@0.10.1",
|
|
1112
1112
|
"pkg:cargo/hmac@0.12.1",
|
|
@@ -66,12 +66,16 @@ function splitClauses(text) {
|
|
|
66
66
|
.map((clause) => clause.trim())
|
|
67
67
|
.filter(Boolean);
|
|
68
68
|
}
|
|
69
|
+
const AFFIRMATIVE_ACTION_VERBS = /\b(?:build(?:s|ing)?|chang(?:e|es|ing)|modif(?:y|ies|ying)|alter(?:s|ing)?|add(?:s|ing)?|introduc(?:e|es|ing)|implement(?:s|ing)?|enabl(?:e|es|ing)|expos(?:e|es|ing|ed)|mutat(?:e|es|ing)|rewrit(?:e|es|ing|ten))\b/;
|
|
70
|
+
function assertsForbiddenAction(text) {
|
|
71
|
+
return AFFIRMATIVE_ACTION_VERBS.test(normalize(text));
|
|
72
|
+
}
|
|
69
73
|
function hasAffirmativeDrift(text, outOfScopeItem) {
|
|
70
|
-
const action =
|
|
74
|
+
const action = new RegExp(AFFIRMATIVE_ACTION_VERBS, 'g');
|
|
71
75
|
const actionSegments = text.split(/\band\b/i).map(normalize);
|
|
72
76
|
for (const [index, segment] of actionSegments.entries()) {
|
|
73
77
|
const inheritsRelevantSubject = index > 0 &&
|
|
74
|
-
|
|
78
|
+
new RegExp(`^${AFFIRMATIVE_ACTION_VERBS.source}`).test(segment) &&
|
|
75
79
|
clauseMatchesScope(actionSegments[index - 1] ?? '', outOfScopeItem);
|
|
76
80
|
for (const match of segment.matchAll(action)) {
|
|
77
81
|
const before = segment.slice(Math.max(0, match.index - 24), match.index);
|
|
@@ -133,7 +137,9 @@ function contradicts(criterionText, outOfScopeItem) {
|
|
|
133
137
|
if (keywords(outOfScopeItem).length === 0) {
|
|
134
138
|
return false;
|
|
135
139
|
}
|
|
136
|
-
|
|
140
|
+
const scopeAssertsForbiddenAction = assertsForbiddenAction(outOfScopeItem);
|
|
141
|
+
return clauses.some((clause) => clauseMatchesScope(clause, outOfScopeItem) &&
|
|
142
|
+
(!scopeAssertsForbiddenAction || assertsForbiddenAction(clause)));
|
|
137
143
|
}
|
|
138
144
|
/**
|
|
139
145
|
* Check acceptance criteria against the spec's outOfScope declarations.
|
|
@@ -41,13 +41,14 @@ export function extractSectionBody(body, sectionName) {
|
|
|
41
41
|
// match `## Technical Notes` etc. Use `[ \\t]*` (not `\\s*`) to avoid
|
|
42
42
|
// consuming the line terminator via greedy backtrack. (SPEC-1010 PR-C
|
|
43
43
|
// dual-Opus review)
|
|
44
|
-
const headingRe = new RegExp(
|
|
44
|
+
const headingRe = new RegExp(`^(#{1,6})[ \\t]+${escapeRegex(sectionName)}[ \\t]*$`, 'm');
|
|
45
45
|
const match = headingRe.exec(masked);
|
|
46
46
|
if (!match) {
|
|
47
47
|
return '';
|
|
48
48
|
}
|
|
49
|
+
const level = match[1]?.length ?? 2;
|
|
49
50
|
const afterHeading = match.index + match[0].length;
|
|
50
|
-
const nextRe =
|
|
51
|
+
const nextRe = new RegExp(`^#{1,${level}}[ \\t]+\\S`, 'gm');
|
|
51
52
|
nextRe.lastIndex = afterHeading;
|
|
52
53
|
const next = nextRe.exec(masked);
|
|
53
54
|
const end = next ? next.index : normalized.length;
|
|
@@ -37,9 +37,36 @@ export function buildCanonicalUnifiedSpecContent(input) {
|
|
|
37
37
|
renderSection('Acceptance Criteria', acceptance),
|
|
38
38
|
renderSection('Files', renderOwnedFiles(input.files)),
|
|
39
39
|
renderSection('Out of scope', outOfScope),
|
|
40
|
+
...extractAuthorSections(source),
|
|
40
41
|
];
|
|
41
42
|
return `${frontmatter ? `${frontmatter}\n\n` : ''}${sections.join('\n\n')}\n`;
|
|
42
43
|
}
|
|
44
|
+
const CANONICAL_SECTION_NAMES = new Set([
|
|
45
|
+
'Problem',
|
|
46
|
+
'Goal',
|
|
47
|
+
'Technical',
|
|
48
|
+
'Implementation Contract',
|
|
49
|
+
'Acceptance Criteria',
|
|
50
|
+
'Files',
|
|
51
|
+
'Out of scope',
|
|
52
|
+
].map((name) => name.toLowerCase()));
|
|
53
|
+
function extractAuthorSections(source) {
|
|
54
|
+
const scannable = stripFencedCodeBlocks(source);
|
|
55
|
+
const headingRe = /^##[ \t]+(\S.*?)[ \t]*$/gm;
|
|
56
|
+
const preserved = [];
|
|
57
|
+
let match;
|
|
58
|
+
while ((match = headingRe.exec(scannable)) !== null) {
|
|
59
|
+
const name = match[1] ?? '';
|
|
60
|
+
if (CANONICAL_SECTION_NAMES.has(name.toLowerCase())) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const body = extractTopLevelSectionBody(source, name);
|
|
64
|
+
if (body !== null && body.length > 0) {
|
|
65
|
+
preserved.push(renderSection(name, body));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return preserved;
|
|
69
|
+
}
|
|
43
70
|
/**
|
|
44
71
|
* Combine a generated lean spec.md body and a generated lean technical.md body
|
|
45
72
|
* into a single unified spec.md content. The technical body's YAML frontmatter
|
|
@@ -9,7 +9,6 @@ export { hasLegacyConfig } from './version-detection.js';
|
|
|
9
9
|
export { validateMigrationResult, mergeMigrationResults, emptyMigrationResult, } from './migration-validator.js';
|
|
10
10
|
export { migrateAllSpecsToLean, migrateSpecToLean, isOldFormat } from './lean-migration.js';
|
|
11
11
|
export { scanForAmbiguousCriteria } from './criteria-scanner.js';
|
|
12
|
-
export { migrateToUnified, migrateAllSpecsToUnified } from './unified-migration.js';
|
|
13
12
|
export { findLegacyMultiFileSpecs } from './find-legacy-multifile-specs.js';
|
|
14
13
|
export { foldTechnicalIntoSpec } from './fold-technical.js';
|
|
15
14
|
export { foldProgressIntoSpec, isBoilerplateProgress } from './fold-progress.js';
|
|
@@ -21,7 +21,6 @@ export { migrateAllSpecsToLean, migrateSpecToLean, isOldFormat } from './lean-mi
|
|
|
21
21
|
// SPEC-487: Scan lean specs for ambiguous criteria (score < 50)
|
|
22
22
|
export { scanForAmbiguousCriteria } from './criteria-scanner.js';
|
|
23
23
|
// SPEC-630: Merge technical.md into unified spec.md
|
|
24
|
-
export { migrateToUnified, migrateAllSpecsToUnified } from './unified-migration.js';
|
|
25
24
|
// SPEC-752: SSR back-migration — fold legacy technical.md/progress.md into spec.md
|
|
26
25
|
export { findLegacyMultiFileSpecs } from './find-legacy-multifile-specs.js';
|
|
27
26
|
export { foldTechnicalIntoSpec } from './fold-technical.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { LegacyArtifactHit, LegacyMigrationDirective } from '../../types/index.js';
|
|
2
|
+
export declare function scanLegacyArtifacts(specDir: string): Promise<LegacyArtifactHit[]>;
|
|
3
|
+
export declare function buildMigrationDirective(specId: string, specDir: string, hits: LegacyArtifactHit[]): Promise<LegacyMigrationDirective | null>;
|
|
4
|
+
//# sourceMappingURL=legacy-classifier.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { ConfigLoader } from '../config-loader.js';
|
|
5
|
+
const legacyArtifactRuleSchema = z.object({
|
|
6
|
+
id: z.string().min(1).describe('Unique identifier for this legacy artifact rule'),
|
|
7
|
+
pattern: z
|
|
8
|
+
.string()
|
|
9
|
+
.min(1)
|
|
10
|
+
.describe('Basename (file) or directory name to match inside a spec folder'),
|
|
11
|
+
classification: z
|
|
12
|
+
.enum(['content-bearing', 'transient'])
|
|
13
|
+
.describe('content-bearing requires host-LLM synthesis into targetSection; transient is deleted with no synthesis'),
|
|
14
|
+
targetSection: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('spec.md section this artifact folds into, required for content-bearing rules'),
|
|
18
|
+
});
|
|
19
|
+
const legacyArtifactRulesSchema = z
|
|
20
|
+
.array(legacyArtifactRuleSchema)
|
|
21
|
+
.describe('Registry of legacy spec-folder artifact classifications');
|
|
22
|
+
const loader = new ConfigLoader('legacy-artifacts', legacyArtifactRulesSchema);
|
|
23
|
+
function loadRules() {
|
|
24
|
+
return loader.load();
|
|
25
|
+
}
|
|
26
|
+
export async function scanLegacyArtifacts(specDir) {
|
|
27
|
+
const rules = loadRules();
|
|
28
|
+
let entries;
|
|
29
|
+
try {
|
|
30
|
+
entries = await readdir(specDir);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const hits = [];
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
if (entry === 'spec.md') {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const rule = rules.find((r) => r.pattern === entry);
|
|
41
|
+
if (!rule) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
hits.push({
|
|
45
|
+
path: entry,
|
|
46
|
+
classification: rule.classification,
|
|
47
|
+
...(rule.targetSection !== undefined ? { targetSection: rule.targetSection } : {}),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return hits;
|
|
51
|
+
}
|
|
52
|
+
export async function buildMigrationDirective(specId, specDir, hits) {
|
|
53
|
+
if (hits.length === 0) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const legacyFiles = [];
|
|
57
|
+
for (const hit of hits) {
|
|
58
|
+
if (hit.classification !== 'content-bearing') {
|
|
59
|
+
legacyFiles.push(hit);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const inlinedContent = await readFile(join(specDir, hit.path), 'utf-8').catch(() => undefined);
|
|
63
|
+
legacyFiles.push({
|
|
64
|
+
...hit,
|
|
65
|
+
...(inlinedContent !== undefined ? { inlinedContent } : {}),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return { specId, legacyFiles, directive: 'migrate_legacy_spec' };
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=legacy-classifier.js.map
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
// engine/spec-migrator/planu-canonical-policy.ts — SPEC-1017
|
|
2
2
|
// Single source of truth for the strict Planu managed directory contract.
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { ConfigLoader } from '../config-loader.js';
|
|
5
|
+
const legacyArtifactRuleSchema = z.object({
|
|
6
|
+
id: z.string().min(1),
|
|
7
|
+
pattern: z.string().min(1),
|
|
8
|
+
classification: z.enum(['content-bearing', 'transient']),
|
|
9
|
+
targetSection: z.string().optional(),
|
|
10
|
+
});
|
|
11
|
+
const legacyArtifactRulesLoader = new ConfigLoader('legacy-artifacts', z.array(legacyArtifactRuleSchema));
|
|
12
|
+
const LEGACY_CONTENT_BEARING_PATTERNS = legacyArtifactRulesLoader
|
|
13
|
+
.load()
|
|
14
|
+
.filter((rule) => rule.classification === 'content-bearing')
|
|
15
|
+
.map((rule) => rule.pattern);
|
|
3
16
|
export const PLANU_CANONICAL_POLICY = {
|
|
4
17
|
canonicalRootFiles: [
|
|
5
18
|
'conventions.json',
|
|
@@ -33,15 +46,7 @@ export const PLANU_CANONICAL_POLICY = {
|
|
|
33
46
|
'planu/specs/*/implementation-brief.md',
|
|
34
47
|
'planu/specs/*/risk-register.md',
|
|
35
48
|
],
|
|
36
|
-
legacyMergeBeforeDeleteFiles:
|
|
37
|
-
'technical.md',
|
|
38
|
-
'plan.md',
|
|
39
|
-
'PLAN.md',
|
|
40
|
-
'progress.md',
|
|
41
|
-
'HU.md',
|
|
42
|
-
'FICHA-TECNICA.md',
|
|
43
|
-
'PROGRESS.md',
|
|
44
|
-
],
|
|
49
|
+
legacyMergeBeforeDeleteFiles: LEGACY_CONTENT_BEARING_PATTERNS,
|
|
45
50
|
};
|
|
46
51
|
const ROOT_FILE_SET = new Set(PLANU_CANONICAL_POLICY.canonicalRootFiles);
|
|
47
52
|
const ROOT_DIR_SET = new Set(PLANU_CANONICAL_POLICY.canonicalRootDirs);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { StrictPlanuCleanupResult, StrictPlanuValidationOptions, StrictPlanuValidationResult, HousekeepingAuthority } from '../../types/index.js';
|
|
1
|
+
import type { StrictPlanuCleanupResult, StrictPlanuValidationOptions, StrictPlanuValidationResult, HousekeepingAuthority, LegacyArtifactHit } from '../../types/index.js';
|
|
2
2
|
import { PLANU_CANONICAL_POLICY } from './planu-canonical-policy.js';
|
|
3
|
+
declare function classifyPendingLegacySpecFile(specDir: string, fileName: string): Promise<LegacyArtifactHit>;
|
|
4
|
+
export declare function findTrackedNonCanonicalSpecFiles(projectPath: string): Promise<string[]>;
|
|
3
5
|
export declare function runStrictPlanuCleanup(projectPath: string, authority?: HousekeepingAuthority): Promise<StrictPlanuCleanupResult>;
|
|
4
6
|
export declare function validateStrictPlanuLayout(projectPath: string, options?: StrictPlanuValidationOptions): Promise<StrictPlanuValidationResult>;
|
|
5
|
-
export { PLANU_CANONICAL_POLICY };
|
|
7
|
+
export { PLANU_CANONICAL_POLICY, classifyPendingLegacySpecFile };
|
|
6
8
|
//# sourceMappingURL=strict-planu-cleanup.d.ts.map
|
|
@@ -7,6 +7,7 @@ import { promisify } from 'node:util';
|
|
|
7
7
|
import { dirname, isAbsolute, join, relative } from 'node:path';
|
|
8
8
|
import { atomicWriteFile } from '../safety/atomic-write-file.js';
|
|
9
9
|
import { safeUnlink } from './git-aware-fs.js';
|
|
10
|
+
import { scanLegacyArtifacts } from './legacy-classifier.js';
|
|
10
11
|
import { PLANU_CANONICAL_POLICY, canonicalContractText, isCanonicalPlanuRootDir, isCanonicalPlanuRootFile, isCanonicalReleaseFile, isCanonicalSpecDir, isCanonicalSpecFile, mustMergeBeforeDeleteSpecFile, } from './planu-canonical-policy.js';
|
|
11
12
|
const execFileAsync = promisify(execFile);
|
|
12
13
|
async function pathIsDirectory(path) {
|
|
@@ -41,37 +42,10 @@ async function gitRmCached(projectPath, relPath) {
|
|
|
41
42
|
/* best-effort */
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (body.trim().length === 0 || new RegExp(`\\n## ${heading}(\\n|$)`).test(specContent)) {
|
|
49
|
-
return specContent;
|
|
50
|
-
}
|
|
51
|
-
return `${specContent.trimEnd()}\n\n## ${heading}\n\n${body.trim()}\n`;
|
|
52
|
-
}
|
|
53
|
-
async function mergeLegacySpecFile(projectPath, specDir, fileName) {
|
|
54
|
-
const specPath = join(specDir, 'spec.md');
|
|
55
|
-
const legacyPath = join(specDir, fileName);
|
|
56
|
-
const [specContent, legacyContent] = await Promise.all([
|
|
57
|
-
readFile(specPath, 'utf-8'),
|
|
58
|
-
readFile(legacyPath, 'utf-8'),
|
|
59
|
-
]);
|
|
60
|
-
const body = stripFrontmatter(legacyContent);
|
|
61
|
-
const section = fileName === 'progress.md' || fileName === 'PROGRESS.md'
|
|
62
|
-
? 'Progress'
|
|
63
|
-
: fileName === 'technical.md' || fileName === 'FICHA-TECNICA.md'
|
|
64
|
-
? 'Technical'
|
|
65
|
-
: 'Files';
|
|
66
|
-
const merged = appendSectionIfMissing(specContent, section, body);
|
|
67
|
-
if (merged !== specContent) {
|
|
68
|
-
await atomicWriteFile(specPath, merged, {
|
|
69
|
-
forceEdit: {
|
|
70
|
-
reason: `SPEC-1017 strict cleanup is merging legacy ${fileName} into canonical spec.md.`,
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
await safeUnlink(projectPath, legacyPath);
|
|
45
|
+
async function classifyPendingLegacySpecFile(specDir, fileName) {
|
|
46
|
+
const hits = await scanLegacyArtifacts(specDir);
|
|
47
|
+
const hit = hits.find((h) => h.path === fileName);
|
|
48
|
+
return hit ?? { path: fileName, classification: 'content-bearing' };
|
|
75
49
|
}
|
|
76
50
|
async function removePath(projectPath, absolutePath) {
|
|
77
51
|
const rel = relative(projectPath, absolutePath);
|
|
@@ -83,16 +57,9 @@ async function removePath(projectPath, absolutePath) {
|
|
|
83
57
|
await safeUnlink(projectPath, absolutePath);
|
|
84
58
|
await rm(absolutePath, { force: true });
|
|
85
59
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
current = await readFile(gitignorePath, 'utf-8');
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
/* missing .gitignore is fine */
|
|
94
|
-
}
|
|
95
|
-
const required = [
|
|
60
|
+
const PLANU_GITIGNORE_BLOCK_HEADER = '# Planu generated/runtime';
|
|
61
|
+
function buildPlanuIgnoreBlock() {
|
|
62
|
+
return [
|
|
96
63
|
'planu/*.html',
|
|
97
64
|
'planu/status.json',
|
|
98
65
|
'planu/CHANGELOG.md',
|
|
@@ -102,27 +69,71 @@ async function updateGitignore(projectPath) {
|
|
|
102
69
|
'planu/data/',
|
|
103
70
|
'planu/state/',
|
|
104
71
|
'planu/.locks/',
|
|
105
|
-
'planu/specs
|
|
106
|
-
'planu/specs
|
|
107
|
-
'planu/specs/**/
|
|
108
|
-
'planu/specs/**/reference/',
|
|
109
|
-
'planu/specs/**/*.bak.*',
|
|
72
|
+
'planu/specs/**',
|
|
73
|
+
'!planu/specs/**/',
|
|
74
|
+
'!planu/specs/**/spec.md',
|
|
110
75
|
];
|
|
111
|
-
|
|
112
|
-
|
|
76
|
+
}
|
|
77
|
+
function removePlanuGitignoreBlock(lines) {
|
|
78
|
+
let result = lines;
|
|
79
|
+
let startIndex = result.findIndex((line) => line.trim() === PLANU_GITIGNORE_BLOCK_HEADER);
|
|
80
|
+
while (startIndex !== -1) {
|
|
81
|
+
let endIndex = result.length;
|
|
82
|
+
for (let i = startIndex + 1; i < result.length; i++) {
|
|
83
|
+
if (result[i]?.trim() === '') {
|
|
84
|
+
endIndex = i;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
result = [...result.slice(0, startIndex), ...result.slice(endIndex)];
|
|
89
|
+
startIndex = result.findIndex((line) => line.trim() === PLANU_GITIGNORE_BLOCK_HEADER);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
async function updateGitignore(projectPath) {
|
|
94
|
+
const gitignorePath = join(projectPath, '.gitignore');
|
|
95
|
+
let original = '';
|
|
96
|
+
try {
|
|
97
|
+
original = await readFile(gitignorePath, 'utf-8');
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
/* missing .gitignore is fine */
|
|
101
|
+
}
|
|
102
|
+
const cleaned = removePlanuGitignoreBlock(original.split('\n')).join('\n');
|
|
103
|
+
const separator = cleaned === '' || cleaned.endsWith('\n') ? '' : '\n';
|
|
104
|
+
const updatedContent = `${cleaned}${separator}${PLANU_GITIGNORE_BLOCK_HEADER}\n${buildPlanuIgnoreBlock().join('\n')}\n`;
|
|
105
|
+
if (updatedContent === original) {
|
|
113
106
|
return false;
|
|
114
107
|
}
|
|
115
|
-
|
|
116
|
-
await atomicWriteFile(gitignorePath, `${current}${separator}# Planu generated/runtime\n${missing.join('\n')}\n`);
|
|
108
|
+
await atomicWriteFile(gitignorePath, updatedContent);
|
|
117
109
|
return true;
|
|
118
110
|
}
|
|
111
|
+
async function listTrackedFiles(projectPath, relPath) {
|
|
112
|
+
if (!existsSync(join(projectPath, '.git'))) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const { stdout } = await execFileAsync('git', ['ls-files', relPath], {
|
|
117
|
+
cwd: projectPath,
|
|
118
|
+
timeout: 5_000,
|
|
119
|
+
});
|
|
120
|
+
return stdout.trim().split('\n').filter(Boolean);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export async function findTrackedNonCanonicalSpecFiles(projectPath) {
|
|
127
|
+
const tracked = await listTrackedFiles(projectPath, 'planu/specs');
|
|
128
|
+
return tracked.filter((relPath) => !isCanonicalSpecFile(relPath.split('/').pop() ?? ''));
|
|
129
|
+
}
|
|
119
130
|
async function walkSpecDirectory(projectPath, specDir, result) {
|
|
120
131
|
const entries = await readDirectory(specDir);
|
|
121
132
|
for (const entry of entries) {
|
|
122
133
|
const full = join(specDir, entry);
|
|
123
134
|
if (mustMergeBeforeDeleteSpecFile(entry)) {
|
|
124
|
-
await
|
|
125
|
-
result.
|
|
135
|
+
await classifyPendingLegacySpecFile(specDir, entry);
|
|
136
|
+
result.proposed.push(relative(projectPath, full));
|
|
126
137
|
continue;
|
|
127
138
|
}
|
|
128
139
|
const isDir = await pathIsDirectory(full);
|
|
@@ -251,5 +262,5 @@ export async function validateStrictPlanuLayout(projectPath, options = {}) {
|
|
|
251
262
|
}
|
|
252
263
|
return { ok: offenders.length === 0, offenders, contract: canonicalContractText() };
|
|
253
264
|
}
|
|
254
|
-
export { PLANU_CANONICAL_POLICY };
|
|
265
|
+
export { PLANU_CANONICAL_POLICY, classifyPendingLegacySpecFile };
|
|
255
266
|
//# sourceMappingURL=strict-planu-cleanup.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Spec } from '../../types/index.js';
|
|
2
2
|
import type { ValidationReportV1 } from '../../types/handoff-artifacts.js';
|
|
3
3
|
import type { MinimalImplementationReport } from '../../types/minimal-implementation-gate.js';
|
|
4
|
+
export declare const AUTOMATED_VALIDATOR_AGENT = "planu-automated-validator";
|
|
4
5
|
export declare function writeImplementationReviewReport(input: {
|
|
5
6
|
projectId: string;
|
|
6
7
|
specId: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { appendArtifact, readArtifact } from '../handoff-artifacts/io.js';
|
|
2
2
|
import { runSpecCompliance, } from './spec-compliance-runner.js';
|
|
3
|
+
export const AUTOMATED_VALIDATOR_AGENT = 'planu-automated-validator';
|
|
3
4
|
export async function writeImplementationReviewReport(input) {
|
|
4
5
|
const specCompliance = await runSpecCompliance(input.spec, input.projectPath, input.signal, input.projectId);
|
|
5
6
|
const reportScore = resolveExecutableValidationScore(input.score, specCompliance);
|
|
@@ -13,8 +14,8 @@ export async function writeImplementationReviewReport(input) {
|
|
|
13
14
|
const mergedGates = await preserveUnobservedGates(input, gates);
|
|
14
15
|
const passed = mergedGates.every((gate) => gate.passed);
|
|
15
16
|
const reviewer = {
|
|
16
|
-
kind: '
|
|
17
|
-
agent:
|
|
17
|
+
kind: 'automation',
|
|
18
|
+
agent: AUTOMATED_VALIDATOR_AGENT,
|
|
18
19
|
verdict: passed ? 'approved' : 'changes-requested',
|
|
19
20
|
};
|
|
20
21
|
const reportCompliance = toValidationReportCompliance(specCompliance);
|
|
@@ -11,6 +11,7 @@ import { extractFilesFromSpecBody } from '../engine/spec-format/technical-md-pop
|
|
|
11
11
|
import { jaccard } from '../engine/spec-format/jaccard.js';
|
|
12
12
|
import { replaceSectionInSpec } from '../engine/spec-format/replace-section.js';
|
|
13
13
|
import { executionRemove } from '../engine/execution/deadline-io.js';
|
|
14
|
+
import { buildMigrationDirective, scanLegacyArtifacts, } from '../engine/spec-migrator/legacy-classifier.js';
|
|
14
15
|
const SPANISH_INDICATORS = [
|
|
15
16
|
'de',
|
|
16
17
|
'del',
|
|
@@ -350,8 +351,8 @@ export async function handleHealSpecDocs(params) {
|
|
|
350
351
|
// (so any regenerated technical.md files are also merged and cleaned up).
|
|
351
352
|
// Migrations are git-aware via safeUnlink; previously they ran in list_specs (read-only).
|
|
352
353
|
let leanMigratedCount = 0;
|
|
353
|
-
let unifiedMigratedCount = 0;
|
|
354
354
|
let rootCleanedCount = 0;
|
|
355
|
+
const migrationDirectives = [];
|
|
355
356
|
// Only run full migrations when no specific specId is requested (mass healing)
|
|
356
357
|
if (!specId && !dryRun) {
|
|
357
358
|
try {
|
|
@@ -362,14 +363,6 @@ export async function handleHealSpecDocs(params) {
|
|
|
362
363
|
catch {
|
|
363
364
|
// best-effort
|
|
364
365
|
}
|
|
365
|
-
try {
|
|
366
|
-
const { migrateAllSpecsToUnified } = await import('../engine/spec-migrator/unified-migration.js');
|
|
367
|
-
const unifiedResult = await migrateAllSpecsToUnified(projectPath);
|
|
368
|
-
unifiedMigratedCount = unifiedResult.mergedCount;
|
|
369
|
-
}
|
|
370
|
-
catch {
|
|
371
|
-
// best-effort
|
|
372
|
-
}
|
|
373
366
|
try {
|
|
374
367
|
const { cleanPlanuRoot } = await import('../engine/spec-migrator/planu-root-cleaner.js');
|
|
375
368
|
const planuDir = join(projectPath, 'planu');
|
|
@@ -380,6 +373,15 @@ export async function handleHealSpecDocs(params) {
|
|
|
380
373
|
// best-effort
|
|
381
374
|
}
|
|
382
375
|
}
|
|
376
|
+
if (!dryRun) {
|
|
377
|
+
for (const spec of specs) {
|
|
378
|
+
const hits = await scanLegacyArtifacts(spec.dir);
|
|
379
|
+
const directive = await buildMigrationDirective(spec.specId, spec.dir, hits);
|
|
380
|
+
if (directive !== null) {
|
|
381
|
+
migrationDirectives.push(directive);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
383
385
|
const parts = [];
|
|
384
386
|
parts.push(`status.json repaired: ${statusJsonRepaired ? 'yes' : 'no'}`);
|
|
385
387
|
parts.push(`technical.md healed: ${String(healed)}`);
|
|
@@ -392,12 +394,12 @@ export async function handleHealSpecDocs(params) {
|
|
|
392
394
|
if (leanMigratedCount > 0) {
|
|
393
395
|
parts.push(`lean migrations: ${String(leanMigratedCount)}`);
|
|
394
396
|
}
|
|
395
|
-
if (unifiedMigratedCount > 0) {
|
|
396
|
-
parts.push(`unified migrations (technical.md merged+deleted): ${String(unifiedMigratedCount)}`);
|
|
397
|
-
}
|
|
398
397
|
if (rootCleanedCount > 0) {
|
|
399
398
|
parts.push(`planu/ root cleaned: ${String(rootCleanedCount)} file(s)`);
|
|
400
399
|
}
|
|
400
|
+
if (migrationDirectives.length > 0) {
|
|
401
|
+
parts.push(`legacy artifacts pending host-LLM migration: ${String(migrationDirectives.length)} spec(s) — call migrate_legacy_spec`);
|
|
402
|
+
}
|
|
401
403
|
if (titlesNonEnglish > 0) {
|
|
402
404
|
parts.push(`non-English titles detected (not modified): ${String(titlesNonEnglish)}`);
|
|
403
405
|
}
|
|
@@ -410,6 +412,7 @@ export async function handleHealSpecDocs(params) {
|
|
|
410
412
|
dryRunDiff,
|
|
411
413
|
backupPath,
|
|
412
414
|
warnings,
|
|
415
|
+
migrationDirectives,
|
|
413
416
|
},
|
|
414
417
|
};
|
|
415
418
|
}
|