@planu/cli 5.5.3 → 5.6.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 +38 -0
- package/dist/.planu-build.json +1 -1
- package/dist/engine/autopilot/bootstrap.js +1 -1
- package/dist/engine/cascade-hooks/core/append-releases.js +22 -12
- 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/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/runtime-residue-sweep.d.ts +9 -0
- package/dist/engine/housekeeping/runtime-residue-sweep.js +57 -0
- 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 +1 -1
- 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/universal-rules/rules/planu-release-policy.js +1 -1
- package/dist/engine/validator/spec-compliance-runner.js +45 -0
- package/dist/hosts/claude-code/ux/mcp-resources.js +7 -23
- 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 +10 -9
- 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-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/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/housekeeping.d.ts +4 -0
- 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/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-format.d.ts +1 -0
- package/package.json +1 -1
- package/planu-plugin.json +1 -1
- package/scripts/lib/pending-release-file.mjs +20 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
## [5.6.0] - 2026-08-31
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
- feat(init-project): regenerate conventions.md from folded conventions.json (SPEC-1699)
|
|
5
|
+
- feat(init-project): fold legacy planu/ root artifacts on update (SPEC-1699)
|
|
6
|
+
- feat(init-project): gitignore legacy planu/ root files (SPEC-1699)
|
|
7
|
+
- feat(create-spec): refresh the regenerable spec index after writing spec.md (SPEC-1699)
|
|
8
|
+
- feat(storage): add regenerable spec.md index (SPEC-1699)
|
|
9
|
+
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
- fix(test): reconcile suites with SPEC-1699 runtime relocation and serialize frozen-lockfile suite
|
|
12
|
+
- fix(storage): classify retention and current-project catch degradations
|
|
13
|
+
- fix(release): require the exact tsgo package dir, not any node_modules content
|
|
14
|
+
- fix(release): drop CI=true from frozen-lockfile install, probe tsgo native package
|
|
15
|
+
- fix(SPEC-1696): accept inline YAML arrays for scenario tests in frontmatter
|
|
16
|
+
- fix(storage): close retention review gaps from Codex CHANGES_REQUIRED
|
|
17
|
+
- fix(storage): enforce retention budgets on runtime storage writers
|
|
18
|
+
- fix(SPEC-1694): report the actual test runner instead of Unknown
|
|
19
|
+
- fix(SPEC-1694): promote framework/database claims only from runtime dependencies
|
|
20
|
+
- fix: parse ordered-list markers in BDD criteria extraction (SPEC-1688)
|
|
21
|
+
- fix: repair release-gate collateral fallout from SPEC-1699 fold (blocker 6 sweep)
|
|
22
|
+
- fix: release.sh repoints version marker to planu/project.json (SPEC-1699 blocker 6)
|
|
23
|
+
- fix: reconcile-release-pending falls back to legacy pending.json (SPEC-1699 blocker 4)
|
|
24
|
+
- fix: legacy-root-migration idempotency and fail-safe malformed handling (SPEC-1699 blocker 3)
|
|
25
|
+
- fix: append-releases fails closed on ledger errors (SPEC-1699 blocker 5)
|
|
26
|
+
- fix: verify spec.md digest at the spec-store read choke point (SPEC-1699 blocker 2)
|
|
27
|
+
- fix(housekeeping): protect planu/.runtime as the canonical runtime home (SPEC-1699)
|
|
28
|
+
- fix: address implementation-review blockers for SPEC-1695 runtime relocation
|
|
29
|
+
- fix: reroute default proposal/export/docs-site outputs to planu/.runtime (SPEC-1695)
|
|
30
|
+
- fix: relocate runtime artifacts to planu/.runtime (SPEC-1695)
|
|
31
|
+
|
|
32
|
+
### Refactoring
|
|
33
|
+
- refactor(release): fold pending release ledger into planu/project.json (SPEC-1699)
|
|
34
|
+
- refactor: relocate session.json and session-context.md under planu/.runtime (SPEC-1699)
|
|
35
|
+
- refactor: route spec resources through the current-project spec index (SPEC-1699)
|
|
36
|
+
- refactor: canonical planu/ set shrinks to project.json + specs/ (SPEC-1699)
|
|
37
|
+
|
|
38
|
+
|
|
1
39
|
## [5.5.3] - 2026-08-30
|
|
2
40
|
|
|
3
41
|
### Bug Fixes
|
package/dist/.planu-build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"schemaVersion":1,"commit":"
|
|
1
|
+
{"schemaVersion":1,"commit":"fa03e03df9ac0a344ac184a06f67f4de5956d0d6"}
|
|
@@ -52,7 +52,7 @@ function registerImplementingCascadeListener() {
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
// SPEC-649: registerDoneCascadeListener (optimize_context on done) removed — cascade reduced to
|
|
55
|
-
// exactly 2 actions: session.json update +
|
|
55
|
+
// exactly 2 actions: session.json update + project.json releases append.
|
|
56
56
|
/**
|
|
57
57
|
* SPEC-628: Fire-and-forget auto_fix_health on cron:daily.
|
|
58
58
|
* Moved from update_status(done) cascade to reduce latency on done transitions.
|
|
@@ -35,13 +35,17 @@ export function normalizePendingReleaseEntries(value) {
|
|
|
35
35
|
throw new Error('Pending release ledger is not an array');
|
|
36
36
|
}
|
|
37
37
|
const deduped = new Map();
|
|
38
|
+
const preserved = [];
|
|
38
39
|
for (const entry of value) {
|
|
39
40
|
if (!isValidPendingReleaseEntry(entry) || !hasParsableCompletionDate(entry)) {
|
|
41
|
+
// Unknown/invalid shape: never silently drop ledger data — pass it
|
|
42
|
+
// through verbatim so a human can inspect and fix it later.
|
|
43
|
+
preserved.push(entry);
|
|
40
44
|
continue;
|
|
41
45
|
}
|
|
42
46
|
deduped.set(entry.specId, entry);
|
|
43
47
|
}
|
|
44
|
-
return [...deduped.values()];
|
|
48
|
+
return [...preserved, ...deduped.values()];
|
|
45
49
|
}
|
|
46
50
|
export function reconcilePublishedPendingEntries(entries, release, isCommitReachable = requireIsCommitReachable) {
|
|
47
51
|
if (!release) {
|
|
@@ -78,16 +82,23 @@ async function captureHeadCommit(projectPath) {
|
|
|
78
82
|
return undefined;
|
|
79
83
|
}
|
|
80
84
|
}
|
|
85
|
+
function extractReleasesField(parsedProject) {
|
|
86
|
+
return parsedProject !== null &&
|
|
87
|
+
typeof parsedProject === 'object' &&
|
|
88
|
+
!Array.isArray(parsedProject)
|
|
89
|
+
? parsedProject.releases
|
|
90
|
+
: undefined;
|
|
91
|
+
}
|
|
81
92
|
async function rewritePendingLedger(input) {
|
|
82
93
|
let pendingList = [];
|
|
83
94
|
try {
|
|
84
|
-
const raw = await readFile(input.
|
|
85
|
-
pendingList = normalizePendingReleaseEntries(JSON.parse(raw));
|
|
95
|
+
const raw = await readFile(input.projectJsonPath, 'utf-8');
|
|
96
|
+
pendingList = normalizePendingReleaseEntries(extractReleasesField(JSON.parse(raw)) ?? []);
|
|
86
97
|
}
|
|
87
98
|
catch (error) {
|
|
88
99
|
if (error.code !== 'ENOENT') {
|
|
89
100
|
reportClassifiedDegradation('PENDING_RELEASE_LEDGER_UNREADABLE', error);
|
|
90
|
-
|
|
101
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
91
102
|
}
|
|
92
103
|
}
|
|
93
104
|
pendingList = reconcilePublishedPendingEntries(pendingList, input.releaseInfo, input.isCommitReachable).filter((entry) => entry.specId !== input.specId);
|
|
@@ -97,7 +108,7 @@ async function rewritePendingLedger(input) {
|
|
|
97
108
|
completedAt: new Date().toISOString().substring(0, 10),
|
|
98
109
|
...(input.implementationCommit ? { implementationCommit: input.implementationCommit } : {}),
|
|
99
110
|
});
|
|
100
|
-
await input.writePendingReleaseLedger(input.
|
|
111
|
+
await input.writePendingReleaseLedger(input.projectJsonPath, pendingList);
|
|
101
112
|
}
|
|
102
113
|
async function actuallyAppendReleasesPending(ctx, opts) {
|
|
103
114
|
const { projectPath, projectId, specId } = ctx;
|
|
@@ -116,19 +127,19 @@ async function actuallyAppendReleasesPending(ctx, opts) {
|
|
|
116
127
|
return;
|
|
117
128
|
}
|
|
118
129
|
const { isCommitReachable, resolveLatestReleaseTag, withPendingReleaseLock, writePendingReleaseLedger, } = helpers;
|
|
119
|
-
const
|
|
120
|
-
const
|
|
130
|
+
const planuDir = join(projectPath, 'planu');
|
|
131
|
+
const projectJsonPath = join(planuDir, 'project.json');
|
|
121
132
|
await new Promise((resolve, reject) => {
|
|
122
133
|
opts.signal.addEventListener('abort', () => {
|
|
123
134
|
reject(new Error('CoreActionTimeoutError: append-releases-pending timed out'));
|
|
124
135
|
});
|
|
125
136
|
(async () => {
|
|
126
|
-
await mkdir(
|
|
137
|
+
await mkdir(planuDir, { recursive: true });
|
|
127
138
|
const spec = await specStore.getSpec(projectId, specId);
|
|
128
139
|
const releaseInfo = await resolveLatestReleaseTag(projectPath);
|
|
129
140
|
const implementationCommit = await captureHeadCommit(projectPath);
|
|
130
|
-
await withPendingReleaseLock(
|
|
131
|
-
|
|
141
|
+
await withPendingReleaseLock(projectJsonPath, { timeoutMs: 1_500, signal: opts.signal }, () => rewritePendingLedger({
|
|
142
|
+
projectJsonPath,
|
|
132
143
|
specId,
|
|
133
144
|
title: spec?.title ?? specId,
|
|
134
145
|
implementationCommit,
|
|
@@ -150,8 +161,7 @@ async function actuallyAppendReleasesPending(ctx, opts) {
|
|
|
150
161
|
resolve();
|
|
151
162
|
})
|
|
152
163
|
.catch((err) => {
|
|
153
|
-
|
|
154
|
-
resolve();
|
|
164
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
155
165
|
});
|
|
156
166
|
});
|
|
157
167
|
}
|
|
@@ -4,6 +4,7 @@ export { fileExists };
|
|
|
4
4
|
declare const IGNORE_DIRS: string[];
|
|
5
5
|
export declare function hasAnyFile(basePath: string, patterns: string[]): Promise<boolean>;
|
|
6
6
|
export declare function collectDependencies(projectPath: string): Promise<Set<string>>;
|
|
7
|
+
export declare function collectRuntimeDependencies(projectPath: string): Promise<Set<string>>;
|
|
7
8
|
export declare function loadStackSignatures(): Promise<StackSignatures>;
|
|
8
9
|
export declare function loadArchPatterns(): Promise<ArchPatternsFile>;
|
|
9
10
|
export declare function readJsonFile(filePath: string): Promise<Record<string, unknown> | null>;
|
|
@@ -53,6 +53,17 @@ export async function collectDependencies(projectPath) {
|
|
|
53
53
|
await collectPhpDeps(projectPath, deps);
|
|
54
54
|
return deps;
|
|
55
55
|
}
|
|
56
|
+
export async function collectRuntimeDependencies(projectPath) {
|
|
57
|
+
const deps = new Set();
|
|
58
|
+
await collectNodeRuntimeDeps(projectPath, deps);
|
|
59
|
+
await collectPythonRequirements(projectPath, deps);
|
|
60
|
+
await collectPyprojectDeps(projectPath, deps);
|
|
61
|
+
await collectGoDeps(projectPath, deps);
|
|
62
|
+
await collectRubyDeps(projectPath, deps);
|
|
63
|
+
await collectRustDeps(projectPath, deps);
|
|
64
|
+
await collectPhpRuntimeDeps(projectPath, deps);
|
|
65
|
+
return deps;
|
|
66
|
+
}
|
|
56
67
|
async function collectNodeDeps(projectPath, deps) {
|
|
57
68
|
try {
|
|
58
69
|
const pkgJson = JSON.parse(await readFile(join(projectPath, 'package.json'), 'utf-8'));
|
|
@@ -67,6 +78,17 @@ async function collectNodeDeps(projectPath, deps) {
|
|
|
67
78
|
// No package.json
|
|
68
79
|
}
|
|
69
80
|
}
|
|
81
|
+
async function collectNodeRuntimeDeps(projectPath, deps) {
|
|
82
|
+
try {
|
|
83
|
+
const pkgJson = JSON.parse(await readFile(join(projectPath, 'package.json'), 'utf-8'));
|
|
84
|
+
for (const dep of Object.keys(pkgJson.dependencies ?? {})) {
|
|
85
|
+
deps.add(dep);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// No package.json
|
|
90
|
+
}
|
|
91
|
+
}
|
|
70
92
|
async function collectPythonRequirements(projectPath, deps) {
|
|
71
93
|
try {
|
|
72
94
|
const content = await readFile(join(projectPath, 'requirements.txt'), 'utf-8');
|
|
@@ -181,6 +203,17 @@ async function collectPhpDeps(projectPath, deps) {
|
|
|
181
203
|
// No composer.json
|
|
182
204
|
}
|
|
183
205
|
}
|
|
206
|
+
async function collectPhpRuntimeDeps(projectPath, deps) {
|
|
207
|
+
try {
|
|
208
|
+
const composerJson = JSON.parse(await readFile(join(projectPath, 'composer.json'), 'utf-8'));
|
|
209
|
+
for (const dep of Object.keys(composerJson.require ?? {})) {
|
|
210
|
+
deps.add(dep);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// No composer.json
|
|
215
|
+
}
|
|
216
|
+
}
|
|
184
217
|
export async function loadStackSignatures() {
|
|
185
218
|
try {
|
|
186
219
|
const configPath = new URL('../config/stack-signatures.json', import.meta.url);
|
|
@@ -16,7 +16,7 @@ export { computeVelocity } from './data-aggregators/velocity-aggregator.js';
|
|
|
16
16
|
export { computeBurndown } from './data-aggregators/burndown-aggregator.js';
|
|
17
17
|
export { detectBottlenecks } from './data-aggregators/bottleneck-detector.js';
|
|
18
18
|
export { computeAccuracy } from './data-aggregators/accuracy-aggregator.js';
|
|
19
|
-
export { scheduleRegeneration, regeneratePages } from './portal-regenerator.js';
|
|
19
|
+
export { scheduleRegeneration, regeneratePages, portalPageOutputPath, } from './portal-regenerator.js';
|
|
20
20
|
export { notifyStoreChange } from './regen-hook.js';
|
|
21
21
|
export { getAffectedPages } from './dirty-flag-map.js';
|
|
22
22
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -19,7 +19,7 @@ export { computeBurndown } from './data-aggregators/burndown-aggregator.js';
|
|
|
19
19
|
export { detectBottlenecks } from './data-aggregators/bottleneck-detector.js';
|
|
20
20
|
export { computeAccuracy } from './data-aggregators/accuracy-aggregator.js';
|
|
21
21
|
// SPEC-146 — Auto-regeneration on store changes
|
|
22
|
-
export { scheduleRegeneration, regeneratePages } from './portal-regenerator.js';
|
|
22
|
+
export { scheduleRegeneration, regeneratePages, portalPageOutputPath, } from './portal-regenerator.js';
|
|
23
23
|
export { notifyStoreChange } from './regen-hook.js';
|
|
24
24
|
export { getAffectedPages } from './dirty-flag-map.js';
|
|
25
25
|
//# sourceMappingURL=index.js.map
|
|
@@ -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)) {
|
|
@@ -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,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
|
|
@@ -196,7 +196,7 @@ function normalizeLegacyTier(value) {
|
|
|
196
196
|
}
|
|
197
197
|
async function readPlanVersion(projectPath) {
|
|
198
198
|
try {
|
|
199
|
-
const raw = await readFile(join(projectPath, 'planu', 'session.json'), 'utf-8');
|
|
199
|
+
const raw = await readFile(join(projectPath, 'planu', '.runtime', 'session.json'), 'utf-8');
|
|
200
200
|
const index = JSON.parse(raw);
|
|
201
201
|
return index.orchestrationPlan?.planVersion ?? 0;
|
|
202
202
|
}
|
|
@@ -68,7 +68,7 @@ export async function writeOrchestrationPlan(projectPath, plan) {
|
|
|
68
68
|
*/
|
|
69
69
|
export async function patchSessionJson(projectPath, patch) {
|
|
70
70
|
try {
|
|
71
|
-
const filePath = join(projectPath, 'planu', 'session.json');
|
|
71
|
+
const filePath = join(projectPath, 'planu', '.runtime', 'session.json');
|
|
72
72
|
let existingContent = null;
|
|
73
73
|
let current = { activeSpecs: [], updatedAt: '' };
|
|
74
74
|
try {
|
|
@@ -8,6 +8,8 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
|
|
10
10
|
import { projectDataDir, readJson, writeJson } from '../../storage/base-store.js';
|
|
11
|
+
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
|
|
12
|
+
import { enforceRetention } from '../../storage/retention.js';
|
|
11
13
|
import { LockBusyError, withSpecLock } from '../safety/cross-process-lock.js';
|
|
12
14
|
const POLICY_PATH = join(dirname(fileURLToPath(import.meta.url)), '../../config/project-knowledge-graph.json');
|
|
13
15
|
const CACHE_VERSION = 2;
|
|
@@ -200,7 +202,13 @@ export async function readProjectGraph(projectId, policy) {
|
|
|
200
202
|
return null;
|
|
201
203
|
}
|
|
202
204
|
export async function writeProjectGraph(projectId, policy, graph) {
|
|
203
|
-
|
|
205
|
+
const graphPath = projectGraphPath(projectId, policy);
|
|
206
|
+
await writeJson(graphPath, graph);
|
|
207
|
+
await enforceRetention({
|
|
208
|
+
kind: 'graph-cache',
|
|
209
|
+
path: projectGraphDir(projectId, policy),
|
|
210
|
+
protectedPaths: [graphPath],
|
|
211
|
+
});
|
|
204
212
|
}
|
|
205
213
|
export async function readProjectGraphSourceHashes(projectId, policy) {
|
|
206
214
|
const active = activeBuilds.get(projectGraphDir(projectId, policy));
|
|
@@ -217,6 +225,11 @@ export async function writeProjectGraphSourceHashes(projectId, policy, hashes) {
|
|
|
217
225
|
records: hashes,
|
|
218
226
|
fragments: current.cache?.fragments ?? {},
|
|
219
227
|
});
|
|
228
|
+
await enforceRetention({
|
|
229
|
+
kind: 'graph-cache',
|
|
230
|
+
path: projectGraphDir(projectId, policy),
|
|
231
|
+
protectedPaths: [projectGraphCachePath(projectId, policy)],
|
|
232
|
+
});
|
|
220
233
|
}
|
|
221
234
|
export async function writeProjectGraphArtifacts(args) {
|
|
222
235
|
if (args.graph.generation.length === 0 ||
|
|
@@ -225,8 +238,15 @@ export async function writeProjectGraphArtifacts(args) {
|
|
|
225
238
|
throw new Error('[Planu] Project graph/cache generation mismatch');
|
|
226
239
|
}
|
|
227
240
|
// Readers verify the shared generation, so neither write order can expose a mixed snapshot.
|
|
228
|
-
|
|
229
|
-
|
|
241
|
+
const cachePath = projectGraphCachePath(args.projectId, args.policy);
|
|
242
|
+
const graphPath = projectGraphPath(args.projectId, args.policy);
|
|
243
|
+
await writeJson(cachePath, args.cache);
|
|
244
|
+
await writeJson(graphPath, args.graph);
|
|
245
|
+
await enforceRetention({
|
|
246
|
+
kind: 'graph-cache',
|
|
247
|
+
path: projectGraphDir(args.projectId, args.policy),
|
|
248
|
+
protectedPaths: [cachePath, graphPath],
|
|
249
|
+
});
|
|
230
250
|
}
|
|
231
251
|
export async function withProjectGraphBuildLock(projectId, policy, task) {
|
|
232
252
|
const key = projectGraphDir(projectId, policy);
|
|
@@ -428,7 +428,7 @@ export async function checkSpecReadiness(spec, mode, projectHash) {
|
|
|
428
428
|
if (bddScenarioCount > 0) {
|
|
429
429
|
const scenariosWithoutTests = findScenariosWithoutTests(huRaw);
|
|
430
430
|
for (const title of scenariosWithoutTests) {
|
|
431
|
-
allBlockers.push(`scenarios_missing_tests: scenario "${title}" has no tests array — add tests entries (SPEC-732)`);
|
|
431
|
+
allBlockers.push(`scenarios_missing_tests: scenario "${title}" has no tests array — add tests entries as a block list (tests: with one "- path" line per test) or an inline array (tests: ["path"]) (SPEC-732)`);
|
|
432
432
|
}
|
|
433
433
|
}
|
|
434
434
|
// SPEC-1214: BDD-criteria executable-evidence gate — when a spec has no frontmatter
|