@planu/cli 5.5.3 → 5.7.0
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 +58 -0
- package/dist/.planu-build.json +1 -1
- package/dist/cli/commands/telemetry.d.ts +3 -0
- package/dist/cli/commands/telemetry.js +118 -0
- package/dist/cli/router.js +3 -1
- package/dist/config/environment-schema.json +14 -0
- package/dist/engine/autopilot/bootstrap.js +1 -1
- package/dist/engine/cascade-hooks/core/append-releases.js +22 -12
- package/dist/engine/contradiction-detector.d.ts +2 -1
- package/dist/engine/contradiction-detector.js +215 -0
- package/dist/engine/detection-utils.d.ts +1 -0
- package/dist/engine/detection-utils.js +33 -0
- package/dist/engine/doc-generator/portal/index.d.ts +1 -1
- package/dist/engine/doc-generator/portal/index.js +1 -1
- package/dist/engine/doc-generator/portal/portal-regenerator.d.ts +8 -3
- package/dist/engine/doc-generator/portal/portal-regenerator.js +16 -7
- package/dist/engine/evidence-gates/evidence-autofill.d.ts +1 -1
- package/dist/engine/evidence-gates/evidence-autofill.js +1 -1
- package/dist/engine/framework-detector.js +8 -6
- package/dist/engine/handoff-artifacts/schemas.js +4 -0
- package/dist/engine/housekeeping/history-log.d.ts +1 -0
- package/dist/engine/housekeeping/history-log.js +58 -3
- package/dist/engine/housekeeping/index.d.ts +2 -1
- package/dist/engine/housekeeping/index.js +2 -1
- package/dist/engine/housekeeping/legacy-planu-demolisher.d.ts +3 -0
- package/dist/engine/housekeeping/legacy-planu-demolisher.js +164 -0
- package/dist/engine/housekeeping/runtime-residue-sweep.d.ts +9 -0
- package/dist/engine/housekeeping/runtime-residue-sweep.js +57 -0
- package/dist/engine/lifecycle-reconciliation.js +87 -40
- package/dist/engine/next-spec-resolver/orchestration-planner.js +1 -1
- package/dist/engine/next-spec-resolver/session-writer.js +1 -1
- package/dist/engine/project-graph/cache.js +23 -3
- package/dist/engine/readiness-checker.js +14 -2
- package/dist/engine/sdd-flow/checkpoints.js +29 -2
- package/dist/engine/session/checkpoint-writer.d.ts +1 -1
- package/dist/engine/session/checkpoint-writer.js +6 -5
- package/dist/engine/session-state/writer.js +4 -3
- package/dist/engine/spec-format/lean-spec-generator.js +1 -1
- package/dist/engine/spec-migrator/planu-canonical-policy.d.ts +8 -1
- package/dist/engine/spec-migrator/planu-canonical-policy.js +14 -13
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +28 -3
- package/dist/engine/telemetry/error-reporter.d.ts +9 -9
- package/dist/engine/telemetry/error-reporter.js +15 -34
- package/dist/engine/telemetry/event-envelope.d.ts +11 -0
- package/dist/engine/telemetry/event-envelope.js +124 -0
- package/dist/engine/telemetry/telemetry-client.d.ts +8 -1
- package/dist/engine/telemetry/telemetry-client.js +38 -20
- package/dist/engine/telemetry/telemetry-store.d.ts +15 -2
- package/dist/engine/telemetry/telemetry-store.js +73 -2
- package/dist/engine/universal-rules/rules/planu-release-policy.js +1 -1
- package/dist/engine/validator/spec-compliance-runner.d.ts +2 -1
- package/dist/engine/validator/spec-compliance-runner.js +123 -1
- package/dist/hosts/claude-code/ux/mcp-resources.js +7 -23
- package/dist/index.js +26 -0
- package/dist/resources/specs.js +12 -33
- package/dist/storage/current-project.d.ts +3 -0
- package/dist/storage/current-project.js +21 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/migrations/canonical-storage.js +5 -0
- package/dist/storage/retention.d.ts +14 -0
- package/dist/storage/retention.js +279 -0
- package/dist/storage/spec-index.d.ts +23 -0
- package/dist/storage/spec-index.js +123 -0
- package/dist/storage/spec-store.d.ts +8 -3
- package/dist/storage/spec-store.js +76 -6
- package/dist/storage/storage-catalog.js +3 -3
- package/dist/storage/storage-layout.d.ts +8 -0
- package/dist/storage/storage-layout.js +9 -0
- package/dist/storage/transition-log.js +2 -0
- package/dist/tools/challenge-spec.js +35 -19
- package/dist/tools/create-spec.js +10 -0
- package/dist/tools/execute-sdd-flow.js +11 -2
- package/dist/tools/export-spec.js +2 -1
- package/dist/tools/force-status-analytics.js +2 -1
- package/dist/tools/generate-docs-site.js +2 -1
- package/dist/tools/generate-proposal.js +6 -2
- package/dist/tools/init-project/claude-md-generator.js +19 -2
- package/dist/tools/init-project/conventions-writer.d.ts +5 -2
- package/dist/tools/init-project/conventions-writer.js +18 -13
- package/dist/tools/init-project/git-setup.js +9 -0
- package/dist/tools/init-project/handler.js +9 -1
- package/dist/tools/init-project/legacy-planu.d.ts +2 -0
- package/dist/tools/init-project/legacy-planu.js +18 -0
- package/dist/tools/init-project/legacy-root-migration.d.ts +15 -0
- package/dist/tools/init-project/legacy-root-migration.js +213 -0
- package/dist/tools/init-project/runtime-residue.d.ts +2 -0
- package/dist/tools/init-project/runtime-residue.js +11 -0
- package/dist/tools/init-project/schedule-housekeeping.d.ts +2 -0
- package/dist/tools/init-project/schedule-housekeeping.js +8 -0
- package/dist/tools/reconcile-spec.js +29 -2
- package/dist/tools/register-spec-tools/analysis-tools.d.ts +7 -0
- package/dist/tools/register-spec-tools/analysis-tools.js +13 -1
- package/dist/tools/safe-handler.js +6 -12
- package/dist/tools/session-checkpoint.js +1 -1
- package/dist/tools/update-status/index.js +7 -1
- package/dist/tools/update-status-actions.d.ts +7 -1
- package/dist/tools/update-status-actions.js +10 -2
- package/dist/types/handoff-artifacts.d.ts +1 -0
- package/dist/types/housekeeping.d.ts +38 -0
- package/dist/types/housekeeping.js +0 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/retention.d.ts +12 -0
- package/dist/types/retention.js +3 -0
- package/dist/types/scope.d.ts +23 -0
- package/dist/types/spec/core.d.ts +7 -0
- package/dist/types/spec/index-cache.d.ts +25 -0
- package/dist/types/spec/index-cache.js +3 -0
- package/dist/types/spec/index.d.ts +1 -0
- package/dist/types/spec/index.js +1 -0
- package/dist/types/spec/inputs.d.ts +9 -0
- package/dist/types/spec-format.d.ts +1 -0
- package/dist/types/telemetry.d.ts +39 -1
- package/dist/types/validation-evidence.d.ts +6 -0
- package/package.json +1 -1
- package/planu-plugin.json +1 -1
- package/scripts/lib/pending-release-file.mjs +20 -4
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import type { StoreType } from '../../../types/portal.js';
|
|
1
|
+
import type { RegeneratedPortalPage, StoreType } from '../../../types/portal.js';
|
|
2
2
|
/**
|
|
3
3
|
* Schedule a debounced regeneration (500 ms window).
|
|
4
4
|
* Multiple calls within the window are coalesced per project.
|
|
5
5
|
*/
|
|
6
6
|
export declare function scheduleRegeneration(projectPath: string, changedStores: StoreType[]): void;
|
|
7
|
+
/**
|
|
8
|
+
* SPEC-1695: Absolute path of a portal page's HTML output, under the
|
|
9
|
+
* per-project storage-layout cache directory (planu/.runtime/cache).
|
|
10
|
+
*/
|
|
11
|
+
export declare function portalPageOutputPath(projectPath: string, page: RegeneratedPortalPage): string;
|
|
7
12
|
/**
|
|
8
13
|
* Immediately regenerate all portal pages affected by the given store changes.
|
|
9
|
-
* Creates
|
|
10
|
-
* Returns the
|
|
14
|
+
* Creates the storage-layout cache directory if it does not exist.
|
|
15
|
+
* Returns the absolute output paths of the pages that were successfully written.
|
|
11
16
|
* Never throws.
|
|
12
17
|
*/
|
|
13
18
|
export declare function regeneratePages(projectPath: string, changedStores: StoreType[]): Promise<string[]>;
|
|
@@ -3,6 +3,8 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
|
|
5
5
|
import { hashProjectPath } from '../../../storage/base-store.js';
|
|
6
|
+
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1699
|
|
7
|
+
import { projectRuntimeDir } from '../../../storage/storage-layout.js';
|
|
6
8
|
import { getAffectedPages } from './dirty-flag-map.js';
|
|
7
9
|
import { withAudit } from '../../../engine/autopilot/audit-logger.js';
|
|
8
10
|
// Debounce state keyed by projectPath to avoid cross-project interference
|
|
@@ -30,16 +32,24 @@ export function scheduleRegeneration(projectPath, changedStores) {
|
|
|
30
32
|
}, 500);
|
|
31
33
|
debounceTimers.set(projectPath, timer);
|
|
32
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* SPEC-1695: Absolute path of a portal page's HTML output, under the
|
|
37
|
+
* per-project storage-layout cache directory (planu/.runtime/cache).
|
|
38
|
+
*/
|
|
39
|
+
export function portalPageOutputPath(projectPath, page) {
|
|
40
|
+
const filename = page === 'index' ? 'index.html' : `${page}.html`;
|
|
41
|
+
return join(projectRuntimeDir(projectPath, 'cache'), filename);
|
|
42
|
+
}
|
|
33
43
|
/**
|
|
34
44
|
* Immediately regenerate all portal pages affected by the given store changes.
|
|
35
|
-
* Creates
|
|
36
|
-
* Returns the
|
|
45
|
+
* Creates the storage-layout cache directory if it does not exist.
|
|
46
|
+
* Returns the absolute output paths of the pages that were successfully written.
|
|
37
47
|
* Never throws.
|
|
38
48
|
*/
|
|
39
49
|
export async function regeneratePages(projectPath, changedStores) {
|
|
40
|
-
const
|
|
50
|
+
const cacheDir = projectRuntimeDir(projectPath, 'cache');
|
|
41
51
|
try {
|
|
42
|
-
await mkdir(
|
|
52
|
+
await mkdir(cacheDir, { recursive: true });
|
|
43
53
|
}
|
|
44
54
|
catch {
|
|
45
55
|
return [];
|
|
@@ -51,10 +61,9 @@ export async function regeneratePages(projectPath, changedStores) {
|
|
|
51
61
|
await withAudit(projectPath, 'portal_regeneration', `regeneratePage(${page})`, async () => {
|
|
52
62
|
const html = await generatePage(projectPath, page);
|
|
53
63
|
if (html) {
|
|
54
|
-
const
|
|
55
|
-
const filePath = join(planuDir, filename);
|
|
64
|
+
const filePath = portalPageOutputPath(projectPath, page);
|
|
56
65
|
await writeFile(filePath, html, 'utf-8');
|
|
57
|
-
regenerated.push(
|
|
66
|
+
regenerated.push(filePath);
|
|
58
67
|
}
|
|
59
68
|
});
|
|
60
69
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AutofillTraceabilityMatrixArgs, AutofillTraceabilityMatrixResult } from '../../types/evidence-autofill.js';
|
|
2
2
|
/** Exact Planu lifecycle files every spec branch mutates; never counted as implementation drift. */
|
|
3
|
-
export declare const PLANU_BOOKKEEPING_FILES: readonly ['planu/context.md', 'planu/session-context.md', 'planu/session.json', 'planu/status.json', 'planu/
|
|
3
|
+
export declare const PLANU_BOOKKEEPING_FILES: readonly ['planu/context.md', 'planu/session-context.md', 'planu/session.json', 'planu/status.json', 'planu/project.json', 'planu/conventions.json'];
|
|
4
4
|
/** Normalizes a spec.md path (absolute or already repo-relative) to a repo-relative,
|
|
5
5
|
* forward-slash path so it can be compared against `git diff` output. */
|
|
6
6
|
export declare function toRepoRelativePath(specPath: string, projectPath?: string): string;
|
|
@@ -42,7 +42,7 @@ export const PLANU_BOOKKEEPING_FILES = [
|
|
|
42
42
|
'planu/session-context.md',
|
|
43
43
|
'planu/session.json',
|
|
44
44
|
'planu/status.json',
|
|
45
|
-
'planu/
|
|
45
|
+
'planu/project.json',
|
|
46
46
|
'planu/conventions.json',
|
|
47
47
|
];
|
|
48
48
|
const OTHER_SPEC_MD_PATTERN = /^planu\/specs\/[^/]+\/spec\.md$/;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { technologyValue } from './technology-registry.js';
|
|
2
2
|
// Planu — Framework, ORM, and database detection
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { fileExists, hasAnyFile, collectDependencies, readJsonFile } from './detection-utils.js';
|
|
4
|
+
import { fileExists, hasAnyFile, collectDependencies, collectRuntimeDependencies, readJsonFile, } from './detection-utils.js';
|
|
5
5
|
/**
|
|
6
6
|
* Detect the primary framework from indicator files and dependencies.
|
|
7
7
|
*/
|
|
@@ -14,8 +14,9 @@ export async function detectFramework(projectPath, signatures) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
// Check dependencies
|
|
18
|
-
|
|
17
|
+
// Check runtime dependencies only — a framework declared solely in
|
|
18
|
+
// devDependencies is tooling, not the project's runtime stack.
|
|
19
|
+
const deps = await collectRuntimeDependencies(projectPath);
|
|
19
20
|
for (const [fw, sig] of Object.entries(signatures.frameworks)) {
|
|
20
21
|
for (const dep of sig.dependencies) {
|
|
21
22
|
if (deps.has(dep)) {
|
|
@@ -55,9 +56,9 @@ export async function detectFrameworkVersion(projectPath, framework, signatures)
|
|
|
55
56
|
* Detect the database engine from indicator files and dependencies.
|
|
56
57
|
*/
|
|
57
58
|
export async function detectDatabase(projectPath, signatures) {
|
|
58
|
-
const deps = await
|
|
59
|
+
const deps = await collectRuntimeDependencies(projectPath);
|
|
59
60
|
for (const [, sig] of Object.entries(signatures.databases)) {
|
|
60
|
-
// Check indicator files
|
|
61
|
+
// Check indicator files (migration dirs, connection configs)
|
|
61
62
|
for (const indicator of sig.indicators) {
|
|
62
63
|
if (await hasAnyFile(projectPath, [indicator])) {
|
|
63
64
|
if (isKnownEngine(sig.engine)) {
|
|
@@ -65,7 +66,8 @@ export async function detectDatabase(projectPath, signatures) {
|
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
|
-
// Check dependencies
|
|
69
|
+
// Check runtime dependencies only — a driver declared solely in
|
|
70
|
+
// devDependencies never produces a Database claim.
|
|
69
71
|
for (const dep of sig.dependencies) {
|
|
70
72
|
if (deps.has(dep)) {
|
|
71
73
|
if (isKnownEngine(sig.engine)) {
|
|
@@ -168,6 +168,10 @@ export const ReconciliationReceiptV1Schema = z.object({
|
|
|
168
168
|
transitionId: z.string().min(1).optional(),
|
|
169
169
|
phase: z.enum(['prepared', 'status-committed', 'spec-synced', 'audit-appended']).optional(),
|
|
170
170
|
auditPending: z.boolean().optional(),
|
|
171
|
+
driftSource: z
|
|
172
|
+
.literal('declared-architectural-premise')
|
|
173
|
+
.optional()
|
|
174
|
+
.describe('Set when the reconciliation demotion evidence is an orchestrator-declared drift rather than an automated validation-report finding.'),
|
|
171
175
|
});
|
|
172
176
|
// ---------------------------------------------------------------------------
|
|
173
177
|
// Schema registry
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HousekeepingHistoryEntry } from '../../types/housekeeping.js';
|
|
2
2
|
export declare function housekeepingHistoryPath(projectPath: string): string;
|
|
3
|
+
export declare function migrateLegacyHousekeepingHistory(projectPath: string): Promise<string | null>;
|
|
3
4
|
export declare function appendHousekeepingEntry(projectPath: string, input: Omit<HousekeepingHistoryEntry, 'id' | 'deletedAt' | 'prevSha' | 'sha256'>): Promise<HousekeepingHistoryEntry>;
|
|
4
5
|
//# sourceMappingURL=history-log.d.ts.map
|
|
@@ -1,14 +1,68 @@
|
|
|
1
1
|
// engine/housekeeping/history-log.ts — SPEC-751
|
|
2
2
|
// Hash-chained JSONL backup log for deleted housekeeping items.
|
|
3
3
|
// Mirrors the SPEC-723/734 transition-log pattern for recoverability.
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
4
5
|
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
-
import { appendFile, readFile, mkdir } from 'node:fs/promises';
|
|
6
|
+
import { appendFile, readFile, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
6
7
|
import { dirname, join } from 'node:path';
|
|
8
|
+
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1699
|
|
9
|
+
import { projectRuntimeDir } from '../../storage/storage-layout.js';
|
|
7
10
|
// ---------------------------------------------------------------------------
|
|
8
|
-
// Path
|
|
11
|
+
// Path helpers
|
|
9
12
|
// ---------------------------------------------------------------------------
|
|
13
|
+
const HISTORY_FILENAME = '.housekeeping-history.jsonl';
|
|
10
14
|
export function housekeepingHistoryPath(projectPath) {
|
|
11
|
-
return join(projectPath, '
|
|
15
|
+
return join(projectRuntimeDir(projectPath, 'state'), HISTORY_FILENAME);
|
|
16
|
+
}
|
|
17
|
+
function legacyHousekeepingHistoryPath(projectPath) {
|
|
18
|
+
return join(projectPath, 'planu', HISTORY_FILENAME);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* SPEC-1695: One-time migration of the legacy planu/.housekeeping-history.jsonl
|
|
22
|
+
* into the storage-layout location. Copies content, verifies it round-tripped,
|
|
23
|
+
* then deletes the legacy file. Idempotent — a missing legacy file or an
|
|
24
|
+
* already-populated new file is a no-op.
|
|
25
|
+
* Returns the legacy path when a migration happened, otherwise null.
|
|
26
|
+
*/
|
|
27
|
+
function entryId(line) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(line).id ?? null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function jsonlLines(content) {
|
|
36
|
+
return content.split('\n').filter((line) => line.trim().length > 0);
|
|
37
|
+
}
|
|
38
|
+
export async function migrateLegacyHousekeepingHistory(projectPath) {
|
|
39
|
+
const legacyPath = legacyHousekeepingHistoryPath(projectPath);
|
|
40
|
+
const newPath = housekeepingHistoryPath(projectPath);
|
|
41
|
+
if (!existsSync(legacyPath)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const legacyContent = await readFile(legacyPath, 'utf-8');
|
|
45
|
+
if (!existsSync(newPath)) {
|
|
46
|
+
await mkdir(dirname(newPath), { recursive: true });
|
|
47
|
+
await writeFile(newPath, legacyContent, 'utf-8');
|
|
48
|
+
const verified = await readFile(newPath, 'utf-8');
|
|
49
|
+
if (verified !== legacyContent) {
|
|
50
|
+
throw new Error('[Planu] housekeeping history migration verification failed');
|
|
51
|
+
}
|
|
52
|
+
await rm(legacyPath, { force: true });
|
|
53
|
+
return legacyPath;
|
|
54
|
+
}
|
|
55
|
+
const destinationContent = await readFile(newPath, 'utf-8');
|
|
56
|
+
const destinationIds = new Set(jsonlLines(destinationContent).map(entryId));
|
|
57
|
+
const missingLines = jsonlLines(legacyContent).filter((line) => {
|
|
58
|
+
const id = entryId(line);
|
|
59
|
+
return id === null || !destinationIds.has(id);
|
|
60
|
+
});
|
|
61
|
+
if (missingLines.length > 0) {
|
|
62
|
+
await appendFile(newPath, missingLines.map((line) => `${line}\n`).join(''), 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
await rm(legacyPath, { force: true });
|
|
65
|
+
return legacyPath;
|
|
12
66
|
}
|
|
13
67
|
// ---------------------------------------------------------------------------
|
|
14
68
|
// Hash chain helpers
|
|
@@ -47,6 +101,7 @@ async function readLastSha(filePath) {
|
|
|
47
101
|
}
|
|
48
102
|
}
|
|
49
103
|
async function doAppend(filePath, input) {
|
|
104
|
+
await migrateLegacyHousekeepingHistory(input.projectPath);
|
|
50
105
|
await mkdir(dirname(filePath), { recursive: true });
|
|
51
106
|
const prevSha = await readLastSha(filePath);
|
|
52
107
|
const partial = {
|
|
@@ -2,7 +2,8 @@ export { findStaleBranches } from './find-stale-branches.js';
|
|
|
2
2
|
export { findStaleWorktrees } from './find-stale-worktrees.js';
|
|
3
3
|
export { findStaleStashes } from './find-stale-stashes.js';
|
|
4
4
|
export { runHousekeepingSweep } from './sweep-runner.js';
|
|
5
|
-
export { appendHousekeepingEntry, housekeepingHistoryPath } from './history-log.js';
|
|
5
|
+
export { appendHousekeepingEntry, housekeepingHistoryPath, migrateLegacyHousekeepingHistory, } from './history-log.js';
|
|
6
|
+
export { sweepRuntimeResidue } from './runtime-residue-sweep.js';
|
|
6
7
|
export { cleanOrphanBrainstormingMarkdowns } from './orphan-brainstorming-cleaner.js';
|
|
7
8
|
export { cleanEphemeralArtifacts } from './ephemeral-artifacts-cleaner.js';
|
|
8
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -4,7 +4,8 @@ export { findStaleBranches } from './find-stale-branches.js';
|
|
|
4
4
|
export { findStaleWorktrees } from './find-stale-worktrees.js';
|
|
5
5
|
export { findStaleStashes } from './find-stale-stashes.js';
|
|
6
6
|
export { runHousekeepingSweep } from './sweep-runner.js';
|
|
7
|
-
export { appendHousekeepingEntry, housekeepingHistoryPath } from './history-log.js';
|
|
7
|
+
export { appendHousekeepingEntry, housekeepingHistoryPath, migrateLegacyHousekeepingHistory, } from './history-log.js';
|
|
8
|
+
export { sweepRuntimeResidue } from './runtime-residue-sweep.js';
|
|
8
9
|
export { cleanOrphanBrainstormingMarkdowns } from './orphan-brainstorming-cleaner.js';
|
|
9
10
|
export { cleanEphemeralArtifacts } from './ephemeral-artifacts-cleaner.js';
|
|
10
11
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { LegacyPlanuDemolisherOptions, LegacyPlanuDemolitionReport } from '../../types/housekeeping.js';
|
|
2
|
+
export declare function demolishLegacyPlanuRoot(options?: LegacyPlanuDemolisherOptions): Promise<LegacyPlanuDemolitionReport>;
|
|
3
|
+
//# sourceMappingURL=legacy-planu-demolisher.d.ts.map
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// engine/housekeeping/legacy-planu-demolisher.ts — SPEC-1709
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { lstat, readdir, realpath, rm, stat } from 'node:fs/promises';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { isAbsolute, join, relative } from 'node:path';
|
|
6
|
+
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1709
|
|
7
|
+
import { globalDataDir, projectDataDir } from '../../storage/base-store.js';
|
|
8
|
+
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1709
|
|
9
|
+
import { getRegistry, readLogicalProjectId } from '../../storage/global-projects-store.js';
|
|
10
|
+
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1709
|
|
11
|
+
import { migrateCanonicalStorage } from '../../storage/migrations/canonical-storage.js';
|
|
12
|
+
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1709
|
|
13
|
+
import { resolveStorageLayout } from '../../storage/storage-layout.js';
|
|
14
|
+
import { pathExistsStrictByStat as pathExists } from '../../core/shared/fs.js';
|
|
15
|
+
const CANONICAL_ROOT_COLLISION_REASON = 'legacy root coincides with the canonical storage root; refusing to demolish';
|
|
16
|
+
function sha256Hex(value) {
|
|
17
|
+
return createHash('sha256').update(value).digest('hex');
|
|
18
|
+
}
|
|
19
|
+
function isInsideDirectory(parent, candidate) {
|
|
20
|
+
const rel = relative(parent, candidate);
|
|
21
|
+
return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
|
|
22
|
+
}
|
|
23
|
+
async function realpathOrSelf(path) {
|
|
24
|
+
try {
|
|
25
|
+
return await realpath(path);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function isRealPathContained(parentReal, candidate) {
|
|
32
|
+
const candidateReal = await realpathOrSelf(candidate);
|
|
33
|
+
return candidateReal === parentReal || isInsideDirectory(parentReal, candidateReal);
|
|
34
|
+
}
|
|
35
|
+
async function canonicalRootCollidesWithLegacy(legacyRoot) {
|
|
36
|
+
const legacyReal = await realpathOrSelf(legacyRoot);
|
|
37
|
+
const canonicalCandidates = [resolveStorageLayout().data, globalDataDir()];
|
|
38
|
+
for (const candidate of canonicalCandidates) {
|
|
39
|
+
if (await isRealPathContained(legacyReal, candidate)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
async function directorySizeBytes(root) {
|
|
46
|
+
let total = 0;
|
|
47
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
const path = join(root, entry.name);
|
|
50
|
+
if (entry.isDirectory()) {
|
|
51
|
+
total += await directorySizeBytes(path);
|
|
52
|
+
}
|
|
53
|
+
else if (entry.isFile()) {
|
|
54
|
+
total += (await stat(path)).size;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return total;
|
|
58
|
+
}
|
|
59
|
+
async function defaultResolveDestination(project) {
|
|
60
|
+
const logicalProjectId = await readLogicalProjectId(project.path);
|
|
61
|
+
const projectId = logicalProjectId !== undefined
|
|
62
|
+
? logicalProjectId.replaceAll('-', '').toLowerCase()
|
|
63
|
+
: project.hash;
|
|
64
|
+
return projectDataDir(projectId);
|
|
65
|
+
}
|
|
66
|
+
async function buildDestinationMap(registry, resolveDestination) {
|
|
67
|
+
const map = new Map();
|
|
68
|
+
for (const project of registry.projects) {
|
|
69
|
+
const destination = await resolveDestination(project);
|
|
70
|
+
const fullDigest = sha256Hex(project.path);
|
|
71
|
+
map.set(fullDigest, destination);
|
|
72
|
+
map.set(fullDigest.slice(0, 16), destination);
|
|
73
|
+
}
|
|
74
|
+
return map;
|
|
75
|
+
}
|
|
76
|
+
async function migrateOrRecordFailure(dirName, dirPath, destination, migrated, failures) {
|
|
77
|
+
const sizeBeforeMigration = await directorySizeBytes(dirPath);
|
|
78
|
+
try {
|
|
79
|
+
const migration = await migrateCanonicalStorage({
|
|
80
|
+
legacyRoots: [dirPath],
|
|
81
|
+
destinationRoot: destination,
|
|
82
|
+
});
|
|
83
|
+
if (migration.status === 'error' || migration.status === 'rolled-back') {
|
|
84
|
+
failures.push({ dir: dirName, reason: migration.reason ?? migration.status });
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
migrated.push(dirName);
|
|
88
|
+
return sizeBeforeMigration;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
failures.push({ dir: dirName, reason: error instanceof Error ? error.message : String(error) });
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function sweepLegacyProjectsDir(legacyProjectsDir, trustedProjectsDirReal, destinationMap) {
|
|
96
|
+
const migrated = [];
|
|
97
|
+
const failures = [];
|
|
98
|
+
let demolishedUnmappable = 0;
|
|
99
|
+
let freedBytes = 0;
|
|
100
|
+
if (!(await pathExists(legacyProjectsDir))) {
|
|
101
|
+
return { migrated, demolishedUnmappable, failures, freedBytes };
|
|
102
|
+
}
|
|
103
|
+
const entries = await readdir(legacyProjectsDir, { withFileTypes: true });
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
const dirPath = join(legacyProjectsDir, entry.name);
|
|
106
|
+
if (entry.isSymbolicLink()) {
|
|
107
|
+
freedBytes += (await lstat(dirPath)).size;
|
|
108
|
+
await rm(dirPath, { force: true });
|
|
109
|
+
demolishedUnmappable += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!entry.isDirectory()) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!(await isRealPathContained(trustedProjectsDirReal, dirPath))) {
|
|
116
|
+
failures.push({ dir: entry.name, reason: 'path escapes the legacy projects directory' });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const destination = destinationMap.get(entry.name);
|
|
120
|
+
if (destination !== undefined) {
|
|
121
|
+
freedBytes += await migrateOrRecordFailure(entry.name, dirPath, destination, migrated, failures);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
freedBytes += await directorySizeBytes(dirPath);
|
|
125
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
126
|
+
demolishedUnmappable += 1;
|
|
127
|
+
}
|
|
128
|
+
return { migrated, demolishedUnmappable, failures, freedBytes };
|
|
129
|
+
}
|
|
130
|
+
async function remainingProjectEntryCount(legacyProjectsDir) {
|
|
131
|
+
if (!(await pathExists(legacyProjectsDir))) {
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
const entries = await readdir(legacyProjectsDir, { withFileTypes: true });
|
|
135
|
+
return entries.length;
|
|
136
|
+
}
|
|
137
|
+
export async function demolishLegacyPlanuRoot(options = {}) {
|
|
138
|
+
const legacyRoot = options.legacyRoot ?? join(homedir(), '.planu');
|
|
139
|
+
if (await canonicalRootCollidesWithLegacy(legacyRoot)) {
|
|
140
|
+
return {
|
|
141
|
+
status: 'retained',
|
|
142
|
+
migrated: [],
|
|
143
|
+
demolishedUnmappable: 0,
|
|
144
|
+
failures: [{ dir: '.', reason: CANONICAL_ROOT_COLLISION_REASON }],
|
|
145
|
+
freedBytes: 0,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (!(await pathExists(legacyRoot))) {
|
|
149
|
+
return { status: 'absent', migrated: [], demolishedUnmappable: 0, failures: [], freedBytes: 0 };
|
|
150
|
+
}
|
|
151
|
+
const registry = options.registry ?? (await getRegistry());
|
|
152
|
+
const resolveDestination = options.resolveDestination ?? defaultResolveDestination;
|
|
153
|
+
const destinationMap = await buildDestinationMap(registry, resolveDestination);
|
|
154
|
+
const legacyProjectsDir = join(legacyRoot, 'data', 'projects');
|
|
155
|
+
const trustedProjectsDirReal = join(await realpathOrSelf(legacyRoot), 'data', 'projects');
|
|
156
|
+
const { migrated, demolishedUnmappable, failures, freedBytes: sweptBytes, } = await sweepLegacyProjectsDir(legacyProjectsDir, trustedProjectsDirReal, destinationMap);
|
|
157
|
+
if ((await remainingProjectEntryCount(legacyProjectsDir)) > 0) {
|
|
158
|
+
return { status: 'retained', migrated, demolishedUnmappable, failures, freedBytes: 0 };
|
|
159
|
+
}
|
|
160
|
+
const freedBytes = sweptBytes + (await directorySizeBytes(legacyRoot));
|
|
161
|
+
await rm(legacyRoot, { recursive: true, force: true });
|
|
162
|
+
return { status: 'demolished', migrated, demolishedUnmappable, failures, freedBytes };
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=legacy-planu-demolisher.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RuntimeResidueSweepResult } from '../../types/housekeeping.js';
|
|
2
|
+
/**
|
|
3
|
+
* Delete every legacy runtime-residue path from planu/ without ever touching
|
|
4
|
+
* canonicalRootFiles / canonicalRootDirs. Idempotent — a checkout with no
|
|
5
|
+
* residue returns an empty removed list. Wired into init_project and the
|
|
6
|
+
* session-start housekeeping path so existing client checkouts self-heal.
|
|
7
|
+
*/
|
|
8
|
+
export declare function sweepRuntimeResidue(projectPath: string): Promise<RuntimeResidueSweepResult>;
|
|
9
|
+
//# sourceMappingURL=runtime-residue-sweep.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// engine/housekeeping/runtime-residue-sweep.ts — SPEC-1695
|
|
2
|
+
// Removes legacy runtime artifacts that PLANU_CANONICAL_POLICY already
|
|
3
|
+
// classifies as regenerable (generatedRuntimePatterns), so a client checkout
|
|
4
|
+
// self-heals on init_project / session start instead of accumulating residue.
|
|
5
|
+
import { rm } from 'node:fs/promises';
|
|
6
|
+
import { join, relative, sep } from 'node:path';
|
|
7
|
+
import { glob } from 'glob';
|
|
8
|
+
import { PLANU_CANONICAL_POLICY, isCanonicalPlanuRootFile, } from '../spec-migrator/planu-canonical-policy.js';
|
|
9
|
+
import { migrateLegacyHousekeepingHistory } from './history-log.js';
|
|
10
|
+
const HOUSEKEEPING_HISTORY_PATTERN = 'planu/.housekeeping-history.jsonl';
|
|
11
|
+
const RUNTIME_HOME_TOP_DIR = PLANU_CANONICAL_POLICY.runtimeHomeDir
|
|
12
|
+
.replace(/^planu\//, '')
|
|
13
|
+
.replace(/\/$/, '');
|
|
14
|
+
function isProtectedPath(planuDir, absolutePath) {
|
|
15
|
+
const rel = relative(planuDir, absolutePath);
|
|
16
|
+
if (rel === '' || rel.startsWith('..')) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
const top = rel.split(sep)[0] ?? '';
|
|
20
|
+
if (isCanonicalPlanuRootFile(top)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return top === RUNTIME_HOME_TOP_DIR || rel === 'releases' || rel === 'specs';
|
|
24
|
+
}
|
|
25
|
+
async function removeMatches(projectPath, pattern) {
|
|
26
|
+
const planuDir = join(projectPath, 'planu');
|
|
27
|
+
const relativePattern = pattern.replace(/^planu\//, '').replace(/\/$/, '');
|
|
28
|
+
const matches = await glob(relativePattern, { cwd: planuDir, dot: true, absolute: true });
|
|
29
|
+
const removed = [];
|
|
30
|
+
for (const absolutePath of matches) {
|
|
31
|
+
if (isProtectedPath(planuDir, absolutePath)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
await rm(absolutePath, { recursive: true, force: true });
|
|
35
|
+
removed.push(absolutePath);
|
|
36
|
+
}
|
|
37
|
+
return removed;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Delete every legacy runtime-residue path from planu/ without ever touching
|
|
41
|
+
* canonicalRootFiles / canonicalRootDirs. Idempotent — a checkout with no
|
|
42
|
+
* residue returns an empty removed list. Wired into init_project and the
|
|
43
|
+
* session-start housekeeping path so existing client checkouts self-heal.
|
|
44
|
+
*/
|
|
45
|
+
export async function sweepRuntimeResidue(projectPath) {
|
|
46
|
+
const removed = [];
|
|
47
|
+
const patterns = PLANU_CANONICAL_POLICY.generatedRuntimePatterns.filter((pattern) => pattern !== HOUSEKEEPING_HISTORY_PATTERN);
|
|
48
|
+
for (const pattern of patterns) {
|
|
49
|
+
removed.push(...(await removeMatches(projectPath, pattern)));
|
|
50
|
+
}
|
|
51
|
+
const migratedHistoryPath = await migrateLegacyHousekeepingHistory(projectPath);
|
|
52
|
+
if (migratedHistoryPath) {
|
|
53
|
+
removed.push(migratedHistoryPath);
|
|
54
|
+
}
|
|
55
|
+
return { removed };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=runtime-residue-sweep.js.map
|