@planu/cli 0.36.0 → 0.38.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/dist/config/license-plans.json +2 -1
- package/dist/engine/reverse-engineer/api-detector.d.ts +3 -0
- package/dist/engine/reverse-engineer/api-detector.d.ts.map +1 -0
- package/dist/engine/reverse-engineer/api-detector.js +170 -0
- package/dist/engine/reverse-engineer/api-detector.js.map +1 -0
- package/dist/engine/reverse-engineer/complexity-analyzer.d.ts +3 -0
- package/dist/engine/reverse-engineer/complexity-analyzer.d.ts.map +1 -0
- package/dist/engine/reverse-engineer/complexity-analyzer.js +230 -0
- package/dist/engine/reverse-engineer/complexity-analyzer.js.map +1 -0
- package/dist/engine/reverse-engineer/config-analyzer.d.ts +3 -0
- package/dist/engine/reverse-engineer/config-analyzer.d.ts.map +1 -0
- package/dist/engine/reverse-engineer/config-analyzer.js +247 -0
- package/dist/engine/reverse-engineer/config-analyzer.js.map +1 -0
- package/dist/engine/reverse-engineer/dependency-graph.d.ts +3 -0
- package/dist/engine/reverse-engineer/dependency-graph.d.ts.map +1 -0
- package/dist/engine/reverse-engineer/dependency-graph.js +197 -0
- package/dist/engine/reverse-engineer/dependency-graph.js.map +1 -0
- package/dist/engine/reverse-engineer/index.d.ts +7 -0
- package/dist/engine/reverse-engineer/index.d.ts.map +1 -0
- package/dist/engine/reverse-engineer/index.js +7 -0
- package/dist/engine/reverse-engineer/index.js.map +1 -0
- package/dist/engine/reverse-engineer/orchestrator.d.ts +5 -0
- package/dist/engine/reverse-engineer/orchestrator.d.ts.map +1 -0
- package/dist/engine/reverse-engineer/orchestrator.js +200 -0
- package/dist/engine/reverse-engineer/orchestrator.js.map +1 -0
- package/dist/engine/reverse-engineer/test-analyzer.d.ts +4 -0
- package/dist/engine/reverse-engineer/test-analyzer.d.ts.map +1 -0
- package/dist/engine/reverse-engineer/test-analyzer.js +130 -0
- package/dist/engine/reverse-engineer/test-analyzer.js.map +1 -0
- package/dist/engine/safety/atomic-writer.d.ts +22 -0
- package/dist/engine/safety/atomic-writer.d.ts.map +1 -0
- package/dist/engine/safety/atomic-writer.js +89 -0
- package/dist/engine/safety/atomic-writer.js.map +1 -0
- package/dist/engine/safety/circuit-breaker.d.ts +14 -0
- package/dist/engine/safety/circuit-breaker.d.ts.map +1 -0
- package/dist/engine/safety/circuit-breaker.js +63 -0
- package/dist/engine/safety/circuit-breaker.js.map +1 -0
- package/dist/engine/safety/file-mutex.d.ts +13 -0
- package/dist/engine/safety/file-mutex.d.ts.map +1 -0
- package/dist/engine/safety/file-mutex.js +108 -0
- package/dist/engine/safety/file-mutex.js.map +1 -0
- package/dist/engine/safety/health-monitor.d.ts +4 -0
- package/dist/engine/safety/health-monitor.d.ts.map +1 -0
- package/dist/engine/safety/health-monitor.js +95 -0
- package/dist/engine/safety/health-monitor.js.map +1 -0
- package/dist/engine/safety/index.d.ts +7 -0
- package/dist/engine/safety/index.d.ts.map +1 -0
- package/dist/engine/safety/index.js +8 -0
- package/dist/engine/safety/index.js.map +1 -0
- package/dist/engine/safety/path-sanitizer.d.ts +17 -0
- package/dist/engine/safety/path-sanitizer.d.ts.map +1 -0
- package/dist/engine/safety/path-sanitizer.js +36 -0
- package/dist/engine/safety/path-sanitizer.js.map +1 -0
- package/dist/engine/safety/transaction.d.ts +18 -0
- package/dist/engine/safety/transaction.d.ts.map +1 -0
- package/dist/engine/safety/transaction.js +70 -0
- package/dist/engine/safety/transaction.js.map +1 -0
- package/dist/engine/spec-kit-exporter.d.ts +6 -0
- package/dist/engine/spec-kit-exporter.d.ts.map +1 -0
- package/dist/engine/spec-kit-exporter.js +227 -0
- package/dist/engine/spec-kit-exporter.js.map +1 -0
- package/dist/engine/spec-migrator.d.ts +9 -0
- package/dist/engine/spec-migrator.d.ts.map +1 -1
- package/dist/engine/spec-migrator.js +70 -0
- package/dist/engine/spec-migrator.js.map +1 -1
- package/dist/engine/webhook/server.d.ts +4 -10
- package/dist/engine/webhook/server.d.ts.map +1 -1
- package/dist/engine/webhook/server.js +78 -24
- package/dist/engine/webhook/server.js.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/storage/base-store.d.ts.map +1 -1
- package/dist/storage/base-store.js +3 -2
- package/dist/storage/base-store.js.map +1 -1
- package/dist/storage/trash-store.d.ts +10 -0
- package/dist/storage/trash-store.d.ts.map +1 -1
- package/dist/storage/trash-store.js +74 -0
- package/dist/storage/trash-store.js.map +1 -1
- package/dist/tools/check-config-health.d.ts.map +1 -1
- package/dist/tools/check-config-health.js +2 -1
- package/dist/tools/check-config-health.js.map +1 -1
- package/dist/tools/create-spec/spec-builder.js +1 -1
- package/dist/tools/create-spec/spec-builder.js.map +1 -1
- package/dist/tools/export-spec.d.ts +3 -0
- package/dist/tools/export-spec.d.ts.map +1 -0
- package/dist/tools/export-spec.js +110 -0
- package/dist/tools/export-spec.js.map +1 -0
- package/dist/tools/generate-teammate-prompt.d.ts.map +1 -1
- package/dist/tools/generate-teammate-prompt.js +7 -4
- package/dist/tools/generate-teammate-prompt.js.map +1 -1
- package/dist/tools/init-project/handler.d.ts.map +1 -1
- package/dist/tools/init-project/handler.js +11 -1
- package/dist/tools/init-project/handler.js.map +1 -1
- package/dist/tools/init-project/result-builder.d.ts.map +1 -1
- package/dist/tools/init-project/result-builder.js +1 -0
- package/dist/tools/init-project/result-builder.js.map +1 -1
- package/dist/tools/plan-team-distribution.d.ts.map +1 -1
- package/dist/tools/plan-team-distribution.js +6 -2
- package/dist/tools/plan-team-distribution.js.map +1 -1
- package/dist/tools/register-agent-tools.d.ts.map +1 -1
- package/dist/tools/register-agent-tools.js +32 -14
- package/dist/tools/register-agent-tools.js.map +1 -1
- package/dist/tools/register-changelog-tools.d.ts.map +1 -1
- package/dist/tools/register-changelog-tools.js +6 -2
- package/dist/tools/register-changelog-tools.js.map +1 -1
- package/dist/tools/register-ci-tools.d.ts.map +1 -1
- package/dist/tools/register-ci-tools.js +12 -3
- package/dist/tools/register-ci-tools.js.map +1 -1
- package/dist/tools/register-context-tools.d.ts.map +1 -1
- package/dist/tools/register-context-tools.js +32 -11
- package/dist/tools/register-context-tools.js.map +1 -1
- package/dist/tools/register-coverage-tools.d.ts.map +1 -1
- package/dist/tools/register-coverage-tools.js +2 -1
- package/dist/tools/register-coverage-tools.js.map +1 -1
- package/dist/tools/register-delete-tools.d.ts.map +1 -1
- package/dist/tools/register-delete-tools.js +9 -8
- package/dist/tools/register-delete-tools.js.map +1 -1
- package/dist/tools/register-event-tools.d.ts.map +1 -1
- package/dist/tools/register-event-tools.js +69 -33
- package/dist/tools/register-event-tools.js.map +1 -1
- package/dist/tools/register-export-tools.d.ts +3 -0
- package/dist/tools/register-export-tools.d.ts.map +1 -0
- package/dist/tools/register-export-tools.js +21 -0
- package/dist/tools/register-export-tools.js.map +1 -0
- package/dist/tools/register-facilitator-tools.d.ts.map +1 -1
- package/dist/tools/register-facilitator-tools.js +10 -2
- package/dist/tools/register-facilitator-tools.js.map +1 -1
- package/dist/tools/register-governance-tools.d.ts.map +1 -1
- package/dist/tools/register-governance-tools.js +4 -1
- package/dist/tools/register-governance-tools.js.map +1 -1
- package/dist/tools/register-hooks-tools.d.ts.map +1 -1
- package/dist/tools/register-hooks-tools.js +45 -18
- package/dist/tools/register-hooks-tools.js.map +1 -1
- package/dist/tools/register-ide-tools.js +1 -1
- package/dist/tools/register-ide-tools.js.map +1 -1
- package/dist/tools/register-learning-tools.d.ts.map +1 -1
- package/dist/tools/register-learning-tools.js +7 -1
- package/dist/tools/register-learning-tools.js.map +1 -1
- package/dist/tools/register-license-tools.d.ts.map +1 -1
- package/dist/tools/register-license-tools.js +6 -1
- package/dist/tools/register-license-tools.js.map +1 -1
- package/dist/tools/register-llm-provider-tools.d.ts.map +1 -1
- package/dist/tools/register-llm-provider-tools.js +13 -9
- package/dist/tools/register-llm-provider-tools.js.map +1 -1
- package/dist/tools/register-memory-tools.d.ts.map +1 -1
- package/dist/tools/register-memory-tools.js +17 -6
- package/dist/tools/register-memory-tools.js.map +1 -1
- package/dist/tools/register-migration-tools.d.ts.map +1 -1
- package/dist/tools/register-migration-tools.js +8 -1
- package/dist/tools/register-migration-tools.js.map +1 -1
- package/dist/tools/register-orchestrator-tools.d.ts.map +1 -1
- package/dist/tools/register-orchestrator-tools.js +15 -14
- package/dist/tools/register-orchestrator-tools.js.map +1 -1
- package/dist/tools/register-platform-tools/design-stack-tools.d.ts.map +1 -1
- package/dist/tools/register-platform-tools/design-stack-tools.js +62 -35
- package/dist/tools/register-platform-tools/design-stack-tools.js.map +1 -1
- package/dist/tools/register-platform-tools/lifecycle-infra-tools.d.ts.map +1 -1
- package/dist/tools/register-platform-tools/lifecycle-infra-tools.js +72 -36
- package/dist/tools/register-platform-tools/lifecycle-infra-tools.js.map +1 -1
- package/dist/tools/register-readiness-tools.js +4 -4
- package/dist/tools/register-readiness-tools.js.map +1 -1
- package/dist/tools/register-runtime-security-tools.js +1 -1
- package/dist/tools/register-runtime-security-tools.js.map +1 -1
- package/dist/tools/register-scope-tools.d.ts.map +1 -1
- package/dist/tools/register-scope-tools.js +26 -6
- package/dist/tools/register-scope-tools.js.map +1 -1
- package/dist/tools/register-search-tools.js +2 -2
- package/dist/tools/register-search-tools.js.map +1 -1
- package/dist/tools/register-security-tools.d.ts.map +1 -1
- package/dist/tools/register-security-tools.js +4 -3
- package/dist/tools/register-security-tools.js.map +1 -1
- package/dist/tools/register-spec-tools/analysis-tools.d.ts.map +1 -1
- package/dist/tools/register-spec-tools/analysis-tools.js +36 -25
- package/dist/tools/register-spec-tools/analysis-tools.js.map +1 -1
- package/dist/tools/register-spec-tools/core-spec-tools.d.ts.map +1 -1
- package/dist/tools/register-spec-tools/core-spec-tools.js +42 -25
- package/dist/tools/register-spec-tools/core-spec-tools.js.map +1 -1
- package/dist/tools/register-stack-tools.d.ts.map +1 -1
- package/dist/tools/register-stack-tools.js +18 -10
- package/dist/tools/register-stack-tools.js.map +1 -1
- package/dist/tools/register-team-tools.d.ts.map +1 -1
- package/dist/tools/register-team-tools.js +17 -9
- package/dist/tools/register-team-tools.js.map +1 -1
- package/dist/tools/register-template-tools.d.ts.map +1 -1
- package/dist/tools/register-template-tools.js +13 -2
- package/dist/tools/register-template-tools.js.map +1 -1
- package/dist/tools/register-transform-tools.d.ts.map +1 -1
- package/dist/tools/register-transform-tools.js +3 -2
- package/dist/tools/register-transform-tools.js.map +1 -1
- package/dist/tools/register-workspace-tools.d.ts.map +1 -1
- package/dist/tools/register-workspace-tools.js +8 -3
- package/dist/tools/register-workspace-tools.js.map +1 -1
- package/dist/tools/reverse-engineer/handler.d.ts.map +1 -1
- package/dist/tools/reverse-engineer/handler.js +156 -128
- package/dist/tools/reverse-engineer/handler.js.map +1 -1
- package/dist/tools/schemas/github.d.ts.map +1 -1
- package/dist/tools/schemas/github.js +18 -9
- package/dist/tools/schemas/github.js.map +1 -1
- package/dist/tools/schemas/ide-config.js +1 -1
- package/dist/tools/schemas/ide-config.js.map +1 -1
- package/dist/tools/schemas/plugins-schemas.d.ts.map +1 -1
- package/dist/tools/schemas/plugins-schemas.js +15 -4
- package/dist/tools/schemas/plugins-schemas.js.map +1 -1
- package/dist/tools/schemas/registry.d.ts.map +1 -1
- package/dist/tools/schemas/registry.js +27 -10
- package/dist/tools/schemas/registry.js.map +1 -1
- package/dist/tools/schemas/session.d.ts.map +1 -1
- package/dist/tools/schemas/session.js +5 -3
- package/dist/tools/schemas/session.js.map +1 -1
- package/dist/tools/schemas/workers-schema.d.ts.map +1 -1
- package/dist/tools/schemas/workers-schema.js +16 -9
- package/dist/tools/schemas/workers-schema.js.map +1 -1
- package/dist/tools/validate-team-results.d.ts.map +1 -1
- package/dist/tools/validate-team-results.js +9 -3
- package/dist/tools/validate-team-results.js.map +1 -1
- package/dist/transports/http-transport.d.ts +7 -0
- package/dist/transports/http-transport.d.ts.map +1 -0
- package/dist/transports/http-transport.js +170 -0
- package/dist/transports/http-transport.js.map +1 -0
- package/dist/transports/index.d.ts +4 -0
- package/dist/transports/index.d.ts.map +1 -0
- package/dist/transports/index.js +4 -0
- package/dist/transports/index.js.map +1 -0
- package/dist/transports/transport-factory.d.ts +5 -0
- package/dist/transports/transport-factory.d.ts.map +1 -0
- package/dist/transports/transport-factory.js +48 -0
- package/dist/transports/transport-factory.js.map +1 -0
- package/dist/types/estimation.d.ts +1 -1
- package/dist/types/estimation.d.ts.map +1 -1
- package/dist/types/export.d.ts +24 -0
- package/dist/types/export.d.ts.map +1 -0
- package/dist/types/export.js +3 -0
- package/dist/types/export.js.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/project/planu-config.d.ts +4 -0
- package/dist/types/project/planu-config.d.ts.map +1 -1
- package/dist/types/reverse-engineer.d.ts +90 -0
- package/dist/types/reverse-engineer.d.ts.map +1 -0
- package/dist/types/reverse-engineer.js +3 -0
- package/dist/types/reverse-engineer.js.map +1 -0
- package/dist/types/safe-schemas.d.ts +12 -0
- package/dist/types/safe-schemas.d.ts.map +1 -0
- package/dist/types/safe-schemas.js +27 -0
- package/dist/types/safe-schemas.js.map +1 -0
- package/dist/types/safety.d.ts +66 -0
- package/dist/types/safety.d.ts.map +1 -0
- package/dist/types/safety.js +3 -0
- package/dist/types/safety.js.map +1 -0
- package/dist/types/transport.d.ts +14 -0
- package/dist/types/transport.d.ts.map +1 -0
- package/dist/types/transport.js +2 -0
- package/dist/types/transport.js.map +1 -0
- package/package.json +11 -2
- package/src/config/license-plans.json +2 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// SPEC-094 AC-4: File-based mutex for resource locking
|
|
2
|
+
import { writeFile, readFile, unlink, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
const LOCK_DIR = 'data/.locks';
|
|
5
|
+
const STALE_THRESHOLD_MS = 30_000;
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
7
|
+
const POLL_INTERVAL_MS = 100;
|
|
8
|
+
function hashResource(resource) {
|
|
9
|
+
return createHash('sha256').update(resource).digest('hex').slice(0, 16);
|
|
10
|
+
}
|
|
11
|
+
function isProcessRunning(pid) {
|
|
12
|
+
try {
|
|
13
|
+
process.kill(pid, 0);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function isStale(content) {
|
|
21
|
+
const lockAge = Date.now() - new Date(content.timestamp).getTime();
|
|
22
|
+
if (lockAge > STALE_THRESHOLD_MS) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (!isProcessRunning(content.pid)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
async function tryReadLock(lockPath) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = await readFile(lockPath, 'utf-8');
|
|
33
|
+
return JSON.parse(raw);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function ensureLockDir() {
|
|
40
|
+
await mkdir(LOCK_DIR, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Acquires a file-based lock for a named resource.
|
|
44
|
+
* Polls until the lock is available or timeout expires.
|
|
45
|
+
* Stale locks (old timestamp or dead PID) are auto-released.
|
|
46
|
+
*/
|
|
47
|
+
export async function acquireLock(resource, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
48
|
+
await ensureLockDir();
|
|
49
|
+
const hash = hashResource(resource);
|
|
50
|
+
const lockPath = `${LOCK_DIR}/${hash}.lock`;
|
|
51
|
+
const deadline = Date.now() + timeoutMs;
|
|
52
|
+
while (Date.now() < deadline) {
|
|
53
|
+
const existing = await tryReadLock(lockPath);
|
|
54
|
+
if (existing) {
|
|
55
|
+
if (isStale(existing)) {
|
|
56
|
+
try {
|
|
57
|
+
await unlink(lockPath);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Another process may have already removed it
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const content = {
|
|
69
|
+
pid: process.pid,
|
|
70
|
+
timestamp: new Date().toISOString(),
|
|
71
|
+
resource,
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
await writeFile(lockPath, JSON.stringify(content, null, 2), {
|
|
75
|
+
flag: 'wx',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// File was created between our check and write — retry
|
|
80
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
resource,
|
|
85
|
+
lockPath,
|
|
86
|
+
pid: process.pid,
|
|
87
|
+
acquiredAt: content.timestamp,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const error = new Error(`Lock timeout after ${timeoutMs}ms for resource "${resource}". Another process holds the lock.`);
|
|
91
|
+
error.code = 'LOCK_TIMEOUT';
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Releases a previously acquired lock.
|
|
96
|
+
* No-op if the lock file was already removed.
|
|
97
|
+
*/
|
|
98
|
+
export async function releaseLock(handle) {
|
|
99
|
+
try {
|
|
100
|
+
await unlink(handle.lockPath);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
if (err.code !== 'ENOENT') {
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=file-mutex.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-mutex.js","sourceRoot":"","sources":["../../../src/engine/safety/file-mutex.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-monitor.d.ts","sourceRoot":"","sources":["../../../src/engine/safety/health-monitor.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAe,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAYvE,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAiCnF;AAmBD,wBAAgB,kBAAkB,IAAI,YAAY,CAqCjD"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// SPEC-094 AC-8: Health monitoring for storage and service
|
|
2
|
+
import { access, constants, readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
function aggregateStatus(checks) {
|
|
5
|
+
if (checks.some((c) => c.status === 'fail')) {
|
|
6
|
+
return 'unhealthy';
|
|
7
|
+
}
|
|
8
|
+
if (checks.some((c) => c.status === 'warn')) {
|
|
9
|
+
return 'degraded';
|
|
10
|
+
}
|
|
11
|
+
return 'healthy';
|
|
12
|
+
}
|
|
13
|
+
export async function checkStorageHealth(projectPath) {
|
|
14
|
+
const checks = [];
|
|
15
|
+
// 1. disk-writable
|
|
16
|
+
try {
|
|
17
|
+
await access(projectPath, constants.W_OK);
|
|
18
|
+
checks.push({ name: 'disk-writable', status: 'pass', message: 'Project path is writable' });
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
checks.push({ name: 'disk-writable', status: 'fail', message: 'Project path is not writable' });
|
|
22
|
+
}
|
|
23
|
+
// 2. specs-json
|
|
24
|
+
const specsPath = join(projectPath, 'specs.json');
|
|
25
|
+
checks.push(await checkJsonFile(specsPath, 'specs-json'));
|
|
26
|
+
// 3. decisions-json
|
|
27
|
+
const decisionsPath = join(projectPath, 'decisions.json');
|
|
28
|
+
checks.push(await checkJsonFile(decisionsPath, 'decisions-json'));
|
|
29
|
+
// 4. data-dir-exists
|
|
30
|
+
const dataDir = join(projectPath, '..');
|
|
31
|
+
try {
|
|
32
|
+
await access(dataDir, constants.R_OK);
|
|
33
|
+
checks.push({ name: 'data-dir-exists', status: 'pass', message: 'Data directory exists' });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
checks.push({ name: 'data-dir-exists', status: 'fail', message: 'Data directory not found' });
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
status: aggregateStatus(checks),
|
|
40
|
+
checks,
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function checkJsonFile(filePath, checkName) {
|
|
45
|
+
try {
|
|
46
|
+
await access(filePath, constants.R_OK);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// File not existing is OK (not yet created)
|
|
50
|
+
return { name: checkName, status: 'pass', message: `${checkName} not found (optional)` };
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const content = await readFile(filePath, 'utf-8');
|
|
54
|
+
JSON.parse(content);
|
|
55
|
+
return { name: checkName, status: 'pass', message: `${checkName} is valid JSON` };
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { name: checkName, status: 'fail', message: `${checkName} contains corrupt JSON` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function checkServiceHealth() {
|
|
62
|
+
const checks = [];
|
|
63
|
+
// 1. node-version
|
|
64
|
+
const versionMatch = /^v(\d+)/.exec(process.version);
|
|
65
|
+
const major = versionMatch?.[1] ? parseInt(versionMatch[1], 10) : 0;
|
|
66
|
+
checks.push({
|
|
67
|
+
name: 'node-version',
|
|
68
|
+
status: major >= 18 ? 'pass' : 'warn',
|
|
69
|
+
message: `Node.js ${process.version}`,
|
|
70
|
+
value: process.version,
|
|
71
|
+
});
|
|
72
|
+
// 2. memory-usage
|
|
73
|
+
const heapUsed = process.memoryUsage().heapUsed;
|
|
74
|
+
const heapMB = Math.round(heapUsed / 1024 / 1024);
|
|
75
|
+
checks.push({
|
|
76
|
+
name: 'memory-usage',
|
|
77
|
+
status: heapMB > 500 ? 'warn' : 'pass',
|
|
78
|
+
message: `Heap usage: ${heapMB}MB`,
|
|
79
|
+
value: heapMB,
|
|
80
|
+
});
|
|
81
|
+
// 3. uptime
|
|
82
|
+
const uptimeSeconds = Math.round(process.uptime());
|
|
83
|
+
checks.push({
|
|
84
|
+
name: 'uptime',
|
|
85
|
+
status: 'pass',
|
|
86
|
+
message: `Process uptime: ${uptimeSeconds}s`,
|
|
87
|
+
value: uptimeSeconds,
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
status: aggregateStatus(checks),
|
|
91
|
+
checks,
|
|
92
|
+
timestamp: new Date().toISOString(),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=health-monitor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-monitor.js","sourceRoot":"","sources":["../../../src/engine/safety/health-monitor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { writeJsonSafe, recoverFromBackup } from './atomic-writer.js';
|
|
2
|
+
export { Transaction, SafetyTransactionError } from './transaction.js';
|
|
3
|
+
export { sanitizePath, PathViolationError } from './path-sanitizer.js';
|
|
4
|
+
export { acquireLock, releaseLock } from './file-mutex.js';
|
|
5
|
+
export { CircuitBreaker } from './circuit-breaker.js';
|
|
6
|
+
export { checkStorageHealth, checkServiceHealth } from './health-monitor.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/engine/safety/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// SPEC-094: Safety module barrel export
|
|
2
|
+
export { writeJsonSafe, recoverFromBackup } from './atomic-writer.js';
|
|
3
|
+
export { Transaction, SafetyTransactionError } from './transaction.js';
|
|
4
|
+
export { sanitizePath, PathViolationError } from './path-sanitizer.js';
|
|
5
|
+
export { acquireLock, releaseLock } from './file-mutex.js';
|
|
6
|
+
export { CircuitBreaker } from './circuit-breaker.js';
|
|
7
|
+
export { checkStorageHealth, checkServiceHealth } from './health-monitor.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/engine/safety/index.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SafePath } from '../../types/safety.js';
|
|
2
|
+
export declare class PathViolationError extends Error {
|
|
3
|
+
readonly code: "PATH_VIOLATION";
|
|
4
|
+
readonly input: string;
|
|
5
|
+
constructor(message: string, input: string);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Sanitizes a path ensuring it stays within projectRoot.
|
|
9
|
+
* Returns a branded SafePath on success.
|
|
10
|
+
*
|
|
11
|
+
* Checks:
|
|
12
|
+
* 1. No null bytes
|
|
13
|
+
* 2. Path length <= 4096
|
|
14
|
+
* 3. Resolved path does not escape projectRoot (relative check)
|
|
15
|
+
*/
|
|
16
|
+
export declare function sanitizePath(input: string, projectRoot: string): SafePath;
|
|
17
|
+
//# sourceMappingURL=path-sanitizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-sanitizer.d.ts","sourceRoot":"","sources":["../../../src/engine/safety/path-sanitizer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAItD,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAG,gBAAgB,CAAU;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;gBAEX,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;CAK3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,QAAQ,CA0BzE"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPEC-094 AC-3: Path sanitizer — prevents path traversal attacks
|
|
2
|
+
import { resolve, normalize, relative } from 'node:path';
|
|
3
|
+
const MAX_PATH_LENGTH = 4096;
|
|
4
|
+
export class PathViolationError extends Error {
|
|
5
|
+
code = 'PATH_VIOLATION';
|
|
6
|
+
input;
|
|
7
|
+
constructor(message, input) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'PathViolationError';
|
|
10
|
+
this.input = input;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Sanitizes a path ensuring it stays within projectRoot.
|
|
15
|
+
* Returns a branded SafePath on success.
|
|
16
|
+
*
|
|
17
|
+
* Checks:
|
|
18
|
+
* 1. No null bytes
|
|
19
|
+
* 2. Path length <= 4096
|
|
20
|
+
* 3. Resolved path does not escape projectRoot (relative check)
|
|
21
|
+
*/
|
|
22
|
+
export function sanitizePath(input, projectRoot) {
|
|
23
|
+
if (input.includes('\0')) {
|
|
24
|
+
throw new PathViolationError('Path contains null bytes. Remove null characters and retry.', input);
|
|
25
|
+
}
|
|
26
|
+
if (input.length > MAX_PATH_LENGTH) {
|
|
27
|
+
throw new PathViolationError(`Path exceeds maximum length of ${MAX_PATH_LENGTH} characters. Use a shorter path.`, input);
|
|
28
|
+
}
|
|
29
|
+
const resolved = normalize(resolve(projectRoot, input));
|
|
30
|
+
const rel = relative(projectRoot, resolved);
|
|
31
|
+
if (rel.startsWith('..') || rel.startsWith('/')) {
|
|
32
|
+
throw new PathViolationError(`Path "${input}" escapes project root. Use a path within the project directory.`, input);
|
|
33
|
+
}
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=path-sanitizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-sanitizer.js","sourceRoot":"","sources":["../../../src/engine/safety/path-sanitizer.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAElE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAGzD,MAAM,eAAe,GAAG,IAAI,CAAC;AAE7B,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAClC,IAAI,GAAG,gBAAyB,CAAC;IACjC,KAAK,CAAS;IAEvB,YAAY,OAAe,EAAE,KAAa;QACxC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa,EAAE,WAAmB;IAC7D,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,kBAAkB,CAC1B,6DAA6D,EAC7D,KAAK,CACN,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,GAAG,eAAe,EAAE,CAAC;QACnC,MAAM,IAAI,kBAAkB,CAC1B,kCAAkC,eAAe,kCAAkC,EACnF,KAAK,CACN,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;IACxD,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAE5C,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,kBAAkB,CAC1B,SAAS,KAAK,kEAAkE,EAChF,KAAK,CACN,CAAC;IACJ,CAAC;IAED,OAAO,QAAoB,CAAC;AAC9B,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ErrorSeverity, RollbackEvent, SafetyErrorCode, TransactionStep } from '../../types/safety.js';
|
|
2
|
+
export declare class SafetyTransactionError extends Error {
|
|
3
|
+
readonly code: SafetyErrorCode;
|
|
4
|
+
readonly severity: ErrorSeverity;
|
|
5
|
+
readonly recovery: string;
|
|
6
|
+
readonly rollbackEvent: RollbackEvent;
|
|
7
|
+
constructor(message: string, code: SafetyErrorCode, severity: ErrorSeverity, recovery: string, rollbackEvent: RollbackEvent);
|
|
8
|
+
}
|
|
9
|
+
export declare class Transaction {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
private readonly steps;
|
|
12
|
+
private readonly completedSteps;
|
|
13
|
+
constructor();
|
|
14
|
+
addStep(step: TransactionStep): void;
|
|
15
|
+
commit(): Promise<void>;
|
|
16
|
+
rollback(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=transaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../../../src/engine/safety/transaction.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EAChB,MAAM,uBAAuB,CAAC;AAE/B,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;gBAGpC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,eAAe,EACrB,QAAQ,EAAE,aAAa,EACvB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,aAAa;CAS/B;AAmBD,qBAAa,WAAW;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;;IAM/C,OAAO,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI;IAI9B,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IA6BvB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAehC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// SPEC-094 AC-2 + AC-10: Transaction with auto-rollback and error classification
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
export class SafetyTransactionError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
severity;
|
|
6
|
+
recovery;
|
|
7
|
+
rollbackEvent;
|
|
8
|
+
constructor(message, code, severity, recovery, rollbackEvent) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'SafetyTransactionError';
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.severity = severity;
|
|
13
|
+
this.recovery = recovery;
|
|
14
|
+
this.rollbackEvent = rollbackEvent;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function createRollbackEvent(transactionId, failedStep, stepsRolledBack, error) {
|
|
18
|
+
const event = {
|
|
19
|
+
transactionId,
|
|
20
|
+
failedStep,
|
|
21
|
+
stepsRolledBack,
|
|
22
|
+
error,
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
24
|
+
};
|
|
25
|
+
console.warn('[Transaction] Rollback event:', JSON.stringify(event));
|
|
26
|
+
return event;
|
|
27
|
+
}
|
|
28
|
+
export class Transaction {
|
|
29
|
+
id;
|
|
30
|
+
steps = [];
|
|
31
|
+
completedSteps = [];
|
|
32
|
+
constructor() {
|
|
33
|
+
this.id = randomUUID();
|
|
34
|
+
}
|
|
35
|
+
addStep(step) {
|
|
36
|
+
this.steps.push(step);
|
|
37
|
+
}
|
|
38
|
+
async commit() {
|
|
39
|
+
for (const step of this.steps) {
|
|
40
|
+
try {
|
|
41
|
+
await step.doFn();
|
|
42
|
+
this.completedSteps.push(step.name);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
46
|
+
await this.rollback();
|
|
47
|
+
const rollbackEvent = createRollbackEvent(this.id, step.name, [...this.completedSteps], errorMessage);
|
|
48
|
+
throw new SafetyTransactionError(`Transaction failed at step "${step.name}": ${errorMessage}`, 'ROLLBACK_TRIGGERED', 'critical', `Review the failed step "${step.name}" and retry the operation. ` +
|
|
49
|
+
`${this.completedSteps.length} step(s) were rolled back.`, rollbackEvent);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async rollback() {
|
|
54
|
+
const stepsToUndo = [...this.completedSteps].reverse();
|
|
55
|
+
for (const stepName of stepsToUndo) {
|
|
56
|
+
const step = this.steps.find((s) => s.name === stepName);
|
|
57
|
+
if (!step) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
await step.undoFn();
|
|
62
|
+
}
|
|
63
|
+
catch (undoErr) {
|
|
64
|
+
const msg = undoErr instanceof Error ? undoErr.message : String(undoErr);
|
|
65
|
+
console.warn(`[Transaction] Undo failed for step "${stepName}": ${msg}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=transaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transaction.js","sourceRoot":"","sources":["../../../src/engine/safety/transaction.ts"],"names":[],"mappings":"AAAA,iFAAiF;AAEjF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAQzC,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IACtC,IAAI,CAAkB;IACtB,QAAQ,CAAgB;IACxB,QAAQ,CAAS;IACjB,aAAa,CAAgB;IAEtC,YACE,OAAe,EACf,IAAqB,EACrB,QAAuB,EACvB,QAAgB,EAChB,aAA4B;QAE5B,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACrC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;IACrC,CAAC;CACF;AAED,SAAS,mBAAmB,CAC1B,aAAqB,EACrB,UAAkB,EAClB,eAAyB,EACzB,KAAa;IAEb,MAAM,KAAK,GAAkB;QAC3B,aAAa;QACb,UAAU;QACV,eAAe;QACf,KAAK;QACL,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,+BAA+B,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;IACrE,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,OAAO,WAAW;IACb,EAAE,CAAS;IACH,KAAK,GAAsB,EAAE,CAAC;IAC9B,cAAc,GAAa,EAAE,CAAC;IAE/C;QACE,IAAI,CAAC,EAAE,GAAG,UAAU,EAAE,CAAC;IACzB,CAAC;IAED,OAAO,CAAC,IAAqB;QAC3B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IAED,KAAK,CAAC,MAAM;QACV,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC9B,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;gBAClB,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,YAAY,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAEtE,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAEtB,MAAM,aAAa,GAAG,mBAAmB,CACvC,IAAI,CAAC,EAAE,EACP,IAAI,CAAC,IAAI,EACT,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,EACxB,YAAY,CACb,CAAC;gBAEF,MAAM,IAAI,sBAAsB,CAC9B,+BAA+B,IAAI,CAAC,IAAI,MAAM,YAAY,EAAE,EAC5D,oBAAoB,EACpB,UAAU,EACV,2BAA2B,IAAI,CAAC,IAAI,6BAA6B;oBAC/D,GAAG,IAAI,CAAC,cAAc,CAAC,MAAM,4BAA4B,EAC3D,aAAa,CACd,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,QAAQ;QACZ,MAAM,WAAW,GAAG,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC,OAAO,EAAE,CAAC;QACvD,KAAK,MAAM,QAAQ,IAAI,WAAW,EAAE,CAAC;YACnC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;YACzD,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,SAAS;YACX,CAAC;YACD,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YACtB,CAAC;YAAC,OAAO,OAAO,EAAE,CAAC;gBACjB,MAAM,GAAG,GAAG,OAAO,YAAY,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBACzE,OAAO,CAAC,IAAI,CAAC,uCAAuC,QAAQ,MAAM,GAAG,EAAE,CAAC,CAAC;YAC3E,CAAC;QACH,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Spec, SpecKitExportOptions, SpecKitExportResult } from '../types/index.js';
|
|
2
|
+
export declare function generateSpecKitSpec(spec: Spec, specContent: string, _constitution?: string): string;
|
|
3
|
+
export declare function generateSpecKitPlan(spec: Spec, technicalContent?: string, constitution?: string, filePlan?: string[]): string;
|
|
4
|
+
export declare function generateSpecKitTasks(spec: Spec, specContent: string): string;
|
|
5
|
+
export declare function exportToSpecKit(spec: Spec, specContent: string, options?: SpecKitExportOptions): SpecKitExportResult;
|
|
6
|
+
//# sourceMappingURL=spec-kit-exporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec-kit-exporter.d.ts","sourceRoot":"","sources":["../../src/engine/spec-kit-exporter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,oBAAoB,EAAE,mBAAmB,EAAa,MAAM,mBAAmB,CAAC;AA+CpG,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,MAAM,EACnB,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,CAqDR;AAED,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,EACV,gBAAgB,CAAC,EAAE,MAAM,EACzB,YAAY,CAAC,EAAE,MAAM,EACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,GAClB,MAAM,CAgER;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CA8B5E;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,oBAAyB,GACjC,mBAAmB,CAWrB"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// --- Helpers ---
|
|
2
|
+
function extractAcceptanceCriteria(specContent) {
|
|
3
|
+
const criteria = [];
|
|
4
|
+
const lines = specContent.split('\n');
|
|
5
|
+
let acIndex = 0;
|
|
6
|
+
for (const line of lines) {
|
|
7
|
+
const match = /^-\s*\[[ x]\]\s*(.+)$/i.exec(line);
|
|
8
|
+
if (match?.[1]) {
|
|
9
|
+
acIndex++;
|
|
10
|
+
criteria.push({ id: `AC-${String(acIndex)}`, text: match[1].trim() });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return criteria;
|
|
14
|
+
}
|
|
15
|
+
function acToGivenWhenThen(text) {
|
|
16
|
+
// Best-effort heuristic: if AC is behavioral, map to Given/When/Then
|
|
17
|
+
const lower = text.toLowerCase();
|
|
18
|
+
if (lower.includes(' when ') || lower.includes(' if ')) {
|
|
19
|
+
return `- Given the system is ready\n - When ${text}\n - Then the expected outcome occurs`;
|
|
20
|
+
}
|
|
21
|
+
return `- Given the system is ready\n - When the user triggers: ${text}\n - Then the feature works as specified`;
|
|
22
|
+
}
|
|
23
|
+
function generateYamlFrontmatter(spec) {
|
|
24
|
+
const lines = ['---'];
|
|
25
|
+
lines.push(`title: "${spec.title}"`);
|
|
26
|
+
lines.push(`planu_id: "${spec.id}"`);
|
|
27
|
+
lines.push(`type: ${spec.type}`);
|
|
28
|
+
lines.push(`scope: ${spec.scope}`);
|
|
29
|
+
lines.push(`status: ${spec.status}`);
|
|
30
|
+
lines.push(`risk: ${spec.risk}`);
|
|
31
|
+
lines.push(`difficulty: ${String(spec.difficulty)}`);
|
|
32
|
+
if (spec.tags.length > 0) {
|
|
33
|
+
lines.push(`tags: [${spec.tags.map((t) => `"${t}"`).join(', ')}]`);
|
|
34
|
+
}
|
|
35
|
+
lines.push(`estimated_hours: ${String(spec.estimation.devHours + spec.estimation.reviewHours)}`);
|
|
36
|
+
lines.push('---');
|
|
37
|
+
return lines.join('\n');
|
|
38
|
+
}
|
|
39
|
+
// --- Public API ---
|
|
40
|
+
export function generateSpecKitSpec(spec, specContent, _constitution) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
const frontmatter = generateYamlFrontmatter(spec);
|
|
43
|
+
lines.push(frontmatter);
|
|
44
|
+
lines.push('');
|
|
45
|
+
lines.push(`# ${spec.title}`);
|
|
46
|
+
lines.push('');
|
|
47
|
+
// User Scenarios
|
|
48
|
+
lines.push('## User Scenarios');
|
|
49
|
+
lines.push('');
|
|
50
|
+
const sections = extractSections(specContent);
|
|
51
|
+
const description = sections.summary ?? sections.description ?? spec.title;
|
|
52
|
+
lines.push(description);
|
|
53
|
+
lines.push('');
|
|
54
|
+
// Acceptance Scenarios (Given/When/Then)
|
|
55
|
+
const criteria = extractAcceptanceCriteria(specContent);
|
|
56
|
+
if (criteria.length > 0) {
|
|
57
|
+
lines.push('## Acceptance Scenarios');
|
|
58
|
+
lines.push('');
|
|
59
|
+
for (const ac of criteria) {
|
|
60
|
+
lines.push(`### ${ac.id}`);
|
|
61
|
+
lines.push(acToGivenWhenThen(ac.text));
|
|
62
|
+
lines.push('');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Functional Requirements
|
|
66
|
+
if (criteria.length > 0) {
|
|
67
|
+
lines.push('## Functional Requirements');
|
|
68
|
+
lines.push('');
|
|
69
|
+
for (const ac of criteria) {
|
|
70
|
+
const frId = ac.id.replace('AC-', 'FR-');
|
|
71
|
+
lines.push(`- **${frId}**: ${ac.text}`);
|
|
72
|
+
}
|
|
73
|
+
lines.push('');
|
|
74
|
+
}
|
|
75
|
+
// Success Criteria
|
|
76
|
+
lines.push('## Success Criteria');
|
|
77
|
+
lines.push('');
|
|
78
|
+
if (criteria.length > 0) {
|
|
79
|
+
lines.push(`- All ${String(criteria.length)} acceptance criteria pass`);
|
|
80
|
+
}
|
|
81
|
+
lines.push('- No regressions introduced');
|
|
82
|
+
lines.push('- Code passes type checking and linting');
|
|
83
|
+
lines.push('');
|
|
84
|
+
// Planu marker for round-trip
|
|
85
|
+
lines.push(`<!-- planu:${spec.id} -->`);
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
}
|
|
88
|
+
export function generateSpecKitPlan(spec, technicalContent, constitution, filePlan) {
|
|
89
|
+
const lines = [];
|
|
90
|
+
lines.push(`# Plan: ${spec.title}`);
|
|
91
|
+
lines.push('');
|
|
92
|
+
// Technical Context
|
|
93
|
+
if (technicalContent) {
|
|
94
|
+
lines.push('## Technical Context');
|
|
95
|
+
lines.push('');
|
|
96
|
+
const techSections = extractSections(technicalContent);
|
|
97
|
+
if (techSections.architecture) {
|
|
98
|
+
lines.push('### Architecture');
|
|
99
|
+
lines.push(techSections.architecture);
|
|
100
|
+
lines.push('');
|
|
101
|
+
}
|
|
102
|
+
if (techSections['key decisions']) {
|
|
103
|
+
lines.push('### Key Decisions');
|
|
104
|
+
lines.push(techSections['key decisions']);
|
|
105
|
+
lines.push('');
|
|
106
|
+
}
|
|
107
|
+
// Fallback: if no recognized sections, include full content
|
|
108
|
+
if (!techSections.architecture && !techSections['key decisions']) {
|
|
109
|
+
lines.push(technicalContent.replace(/^#\s+.+\n/, '').trim());
|
|
110
|
+
lines.push('');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Language/Version, Dependencies, Storage, Testing
|
|
114
|
+
lines.push('## Implementation Details');
|
|
115
|
+
lines.push('');
|
|
116
|
+
lines.push(`- **Type**: ${spec.type}`);
|
|
117
|
+
lines.push(`- **Scope**: ${spec.scope}`);
|
|
118
|
+
lines.push(`- **Target**: ${spec.target}`);
|
|
119
|
+
lines.push(`- **Estimated Hours**: ${String(spec.estimation.devHours + spec.estimation.reviewHours)}`);
|
|
120
|
+
lines.push('');
|
|
121
|
+
// Project Structure from file plan
|
|
122
|
+
if (filePlan && filePlan.length > 0) {
|
|
123
|
+
lines.push('## Project Structure');
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push('```');
|
|
126
|
+
for (const file of filePlan) {
|
|
127
|
+
lines.push(file);
|
|
128
|
+
}
|
|
129
|
+
lines.push('```');
|
|
130
|
+
lines.push('');
|
|
131
|
+
}
|
|
132
|
+
// Constitution Check
|
|
133
|
+
if (constitution) {
|
|
134
|
+
lines.push('## Constitution Check');
|
|
135
|
+
lines.push('');
|
|
136
|
+
lines.push(constitution);
|
|
137
|
+
lines.push('');
|
|
138
|
+
}
|
|
139
|
+
lines.push(`<!-- planu:${spec.id} -->`);
|
|
140
|
+
return lines.join('\n');
|
|
141
|
+
}
|
|
142
|
+
export function generateSpecKitTasks(spec, specContent) {
|
|
143
|
+
const lines = [];
|
|
144
|
+
lines.push(`# Tasks: ${spec.title}`);
|
|
145
|
+
lines.push('');
|
|
146
|
+
const criteria = extractAcceptanceCriteria(specContent);
|
|
147
|
+
if (criteria.length === 0) {
|
|
148
|
+
lines.push('No tasks derived — no acceptance criteria found.');
|
|
149
|
+
lines.push('');
|
|
150
|
+
lines.push(`<!-- planu:${spec.id} -->`);
|
|
151
|
+
return lines.join('\n');
|
|
152
|
+
}
|
|
153
|
+
// Phase organization
|
|
154
|
+
const phases = categorizeTasks(criteria);
|
|
155
|
+
for (const phase of phases) {
|
|
156
|
+
lines.push(`## ${phase.name}`);
|
|
157
|
+
lines.push('');
|
|
158
|
+
for (const task of phase.tasks) {
|
|
159
|
+
const parallel = phase.name !== 'Setup' ? ' [P]' : '';
|
|
160
|
+
lines.push(`- [ ] ${task.text} [${task.id}]${parallel}`);
|
|
161
|
+
}
|
|
162
|
+
lines.push('');
|
|
163
|
+
}
|
|
164
|
+
lines.push(`<!-- planu:${spec.id} -->`);
|
|
165
|
+
return lines.join('\n');
|
|
166
|
+
}
|
|
167
|
+
export function exportToSpecKit(spec, specContent, options = {}) {
|
|
168
|
+
return {
|
|
169
|
+
specMd: generateSpecKitSpec(spec, specContent, options.constitution),
|
|
170
|
+
planMd: generateSpecKitPlan(spec, options.technicalContent, options.constitution, options.filePlan),
|
|
171
|
+
tasksMd: generateSpecKitTasks(spec, specContent),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// --- Internal helpers ---
|
|
175
|
+
function extractSections(content) {
|
|
176
|
+
const sections = {};
|
|
177
|
+
const lines = content.split('\n');
|
|
178
|
+
let currentHeading = '';
|
|
179
|
+
let currentContent = [];
|
|
180
|
+
for (const line of lines) {
|
|
181
|
+
const headingMatch = /^#{1,3}\s+(.+)/.exec(line);
|
|
182
|
+
if (headingMatch?.[1]) {
|
|
183
|
+
if (currentHeading && currentContent.length > 0) {
|
|
184
|
+
sections[currentHeading] = currentContent.join('\n').trim();
|
|
185
|
+
}
|
|
186
|
+
currentHeading = headingMatch[1].toLowerCase();
|
|
187
|
+
currentContent = [];
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
currentContent.push(line);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (currentHeading && currentContent.length > 0) {
|
|
194
|
+
sections[currentHeading] = currentContent.join('\n').trim();
|
|
195
|
+
}
|
|
196
|
+
return sections;
|
|
197
|
+
}
|
|
198
|
+
function categorizeTasks(criteria) {
|
|
199
|
+
const setup = { name: 'Setup', tasks: [] };
|
|
200
|
+
const core = { name: 'Core', tasks: [] };
|
|
201
|
+
const integration = { name: 'Integration', tasks: [] };
|
|
202
|
+
const polish = { name: 'Polish', tasks: [] };
|
|
203
|
+
for (const ac of criteria) {
|
|
204
|
+
const lower = ac.text.toLowerCase();
|
|
205
|
+
if (lower.includes('register') ||
|
|
206
|
+
lower.includes('create') ||
|
|
207
|
+
lower.includes('setup') ||
|
|
208
|
+
lower.includes('schema')) {
|
|
209
|
+
setup.tasks.push(ac);
|
|
210
|
+
}
|
|
211
|
+
else if (lower.includes('test') || lower.includes('document') || lower.includes('clean')) {
|
|
212
|
+
polish.tasks.push(ac);
|
|
213
|
+
}
|
|
214
|
+
else if (lower.includes('output') ||
|
|
215
|
+
lower.includes('write') ||
|
|
216
|
+
lower.includes('return') ||
|
|
217
|
+
lower.includes('directory')) {
|
|
218
|
+
integration.tasks.push(ac);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
core.tasks.push(ac);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Filter out empty phases
|
|
225
|
+
return [setup, core, integration, polish].filter((p) => p.tasks.length > 0);
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=spec-kit-exporter.js.map
|