voidforge-build 23.11.0 → 23.11.1

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.
Files changed (32) hide show
  1. package/dist/.claude/commands/git.md +36 -3
  2. package/dist/CHANGELOG.md +21 -0
  3. package/dist/VERSION.md +2 -1
  4. package/dist/docs/methods/RELEASE_MANAGER.md +26 -0
  5. package/dist/scripts/voidforge.js +0 -0
  6. package/package.json +1 -1
  7. package/dist/wizard/lib/anomaly-detection.d.ts +0 -59
  8. package/dist/wizard/lib/anomaly-detection.js +0 -122
  9. package/dist/wizard/lib/asset-scanner.d.ts +0 -23
  10. package/dist/wizard/lib/asset-scanner.js +0 -107
  11. package/dist/wizard/lib/build-analytics.d.ts +0 -39
  12. package/dist/wizard/lib/build-analytics.js +0 -91
  13. package/dist/wizard/lib/codegen/erd-gen.d.ts +0 -16
  14. package/dist/wizard/lib/codegen/erd-gen.js +0 -98
  15. package/dist/wizard/lib/codegen/openapi-gen.d.ts +0 -15
  16. package/dist/wizard/lib/codegen/openapi-gen.js +0 -79
  17. package/dist/wizard/lib/codegen/prisma-types.d.ts +0 -15
  18. package/dist/wizard/lib/codegen/prisma-types.js +0 -44
  19. package/dist/wizard/lib/codegen/seed-gen.d.ts +0 -16
  20. package/dist/wizard/lib/codegen/seed-gen.js +0 -128
  21. package/dist/wizard/lib/correlation-engine.d.ts +0 -59
  22. package/dist/wizard/lib/correlation-engine.js +0 -152
  23. package/dist/wizard/lib/desktop-notify.d.ts +0 -27
  24. package/dist/wizard/lib/desktop-notify.js +0 -98
  25. package/dist/wizard/lib/image-gen.d.ts +0 -56
  26. package/dist/wizard/lib/image-gen.js +0 -159
  27. package/dist/wizard/lib/natural-language-deploy.d.ts +0 -30
  28. package/dist/wizard/lib/natural-language-deploy.js +0 -186
  29. package/dist/wizard/lib/route-optimizer.d.ts +0 -28
  30. package/dist/wizard/lib/route-optimizer.js +0 -93
  31. package/dist/wizard/lib/service-install.d.ts +0 -18
  32. package/dist/wizard/lib/service-install.js +0 -182
@@ -1,98 +0,0 @@
1
- /**
2
- * Desktop Notifications — macOS/Linux native notifications for daemon events.
3
- *
4
- * Uses osascript on macOS and notify-send on Linux. No dependencies.
5
- * Notifications are non-blocking and failure-tolerant (notification failure
6
- * should never crash the daemon).
7
- *
8
- * PRD Reference: §9.7 (Danger Room shows warning), v11.3 deliverables
9
- */
10
- import { execFileSync } from 'node:child_process';
11
- import { platform } from 'node:os';
12
- /**
13
- * Send a desktop notification. Fails silently — never throws.
14
- */
15
- export function notify(opts) {
16
- try {
17
- if (platform() === 'darwin') {
18
- notifyMacOS(opts);
19
- }
20
- else if (platform() === 'linux') {
21
- notifyLinux(opts);
22
- }
23
- // Windows: notifications deferred — WSL2 path recommended
24
- }
25
- catch {
26
- // Notification failure is never fatal
27
- }
28
- }
29
- function notifyMacOS(opts) {
30
- // SEC-005: Use execFileSync with args array to prevent shell injection
31
- const sound = opts.sound !== false ? ' sound name "Submarine"' : '';
32
- const script = `display notification "${sanitize(opts.message)}" with title "VoidForge"${sound} subtitle "${sanitize(opts.title)}"`;
33
- try {
34
- execFileSync('osascript', ['-e', script], { timeout: 5000, stdio: 'ignore' });
35
- }
36
- catch { /* notification failure is never fatal */ }
37
- }
38
- function notifyLinux(opts) {
39
- // SEC-006: Use execFileSync with args array, validate urgency enum
40
- const validUrgencies = ['low', 'normal', 'critical'];
41
- const urgency = validUrgencies.includes(opts.urgency || '') ? opts.urgency : 'normal';
42
- try {
43
- execFileSync('notify-send', ['-u', urgency, '-a', 'VoidForge', sanitize(opts.title), sanitize(opts.message)], { timeout: 5000, stdio: 'ignore' });
44
- }
45
- catch { /* notification failure is never fatal */ }
46
- }
47
- /** Strip characters that could be dangerous in shell/AppleScript contexts */
48
- function sanitize(input) {
49
- return input.replace(/[`$\\"\n\r\0]/g, '').slice(0, 200);
50
- }
51
- // ── Daemon Event Notifications ────────────────────────
52
- // Pre-built notifications for common daemon events (§9.20.7 agent voice)
53
- export function notifySpendSpike(platform, amount) {
54
- notify({
55
- title: `Spend Spike — ${platform}`,
56
- message: `Wax reports: ${platform} spend is ${amount} above average this hour.`,
57
- urgency: 'critical',
58
- sound: true,
59
- });
60
- }
61
- export function notifyCampaignKilled(name, reason) {
62
- notify({
63
- title: 'Campaign Paused',
64
- message: `Wax pulled the trigger on "${name}" — ${reason}.`,
65
- urgency: 'normal',
66
- });
67
- }
68
- export function notifyTokenExpiring(platform, hoursLeft) {
69
- notify({
70
- title: `Token Expiring — ${platform}`,
71
- message: `Breeze warns: ${platform} token expires in ${hoursLeft} hours. Refresh needed.`,
72
- urgency: hoursLeft < 2 ? 'critical' : 'normal',
73
- sound: hoursLeft < 2,
74
- });
75
- }
76
- export function notifyReconciliationDiscrepancy(platform, amount) {
77
- notify({
78
- title: 'Reconciliation Alert',
79
- message: `Dockson: Numbers don't match on ${platform} — ${amount} discrepancy.`,
80
- urgency: 'critical',
81
- sound: true,
82
- });
83
- }
84
- export function notifyVaultExpiring(hoursLeft) {
85
- notify({
86
- title: 'Vault Session Expiring',
87
- message: `Vault session expires in ${hoursLeft} hour(s). Run \`voidforge heartbeat unlock\` to extend.`,
88
- urgency: 'critical',
89
- sound: true,
90
- });
91
- }
92
- export function notifyRevenueMilestone(amount) {
93
- notify({
94
- title: 'Revenue Milestone!',
95
- message: `Dockson: ${amount} total revenue. Every coin has a story — this one's a good chapter.`,
96
- urgency: 'low',
97
- });
98
- }
@@ -1,56 +0,0 @@
1
- /**
2
- * Image generation provider abstraction — Celebrimbor's forge tools.
3
- * Default: OpenAI (gpt-image-1). Extensible to other providers.
4
- * Uses the same vault system as other VoidForge credentials.
5
- */
6
- import type { ProvisionEmitter } from './provisioners/types.js';
7
- export interface ImageGenerationOptions {
8
- prompt: string;
9
- width: number;
10
- height: number;
11
- model?: string;
12
- quality?: 'low' | 'medium' | 'high';
13
- }
14
- export interface GeneratedAsset {
15
- name: string;
16
- filename: string;
17
- prompt: string;
18
- size: string;
19
- generatedAt: string;
20
- hash: string;
21
- }
22
- export interface AssetManifest {
23
- generated: string;
24
- model: string;
25
- style: string;
26
- assets: GeneratedAsset[];
27
- }
28
- /**
29
- * Generate an image via OpenAI's API.
30
- * Returns the raw image bytes as a Buffer.
31
- */
32
- export declare function generateImage(apiKey: string, options: ImageGenerationOptions, emit: ProvisionEmitter): Promise<Buffer | null>;
33
- /**
34
- * Validate an OpenAI API key by making a lightweight models list request.
35
- */
36
- export declare function validateOpenAIKey(apiKey: string): Promise<boolean>;
37
- /**
38
- * Estimate the cost of generating N images.
39
- */
40
- export declare function estimateImageCost(count: number, model?: string): number;
41
- /**
42
- * Read the asset manifest from disk.
43
- */
44
- export declare function readManifest(imagesDir: string): Promise<AssetManifest | null>;
45
- /**
46
- * Write the asset manifest to disk.
47
- */
48
- export declare function writeManifest(imagesDir: string, manifest: AssetManifest): Promise<void>;
49
- /**
50
- * Save a generated image to disk and update the manifest.
51
- */
52
- export declare function saveGeneratedImage(imagesDir: string, category: string, name: string, imageBuffer: Buffer, prompt: string, size: string, manifest: AssetManifest): Promise<string>;
53
- /**
54
- * Check if an asset already exists on disk.
55
- */
56
- export declare function assetExists(imagesDir: string, category: string, name: string): boolean;
@@ -1,159 +0,0 @@
1
- /**
2
- * Image generation provider abstraction — Celebrimbor's forge tools.
3
- * Default: OpenAI (gpt-image-1). Extensible to other providers.
4
- * Uses the same vault system as other VoidForge credentials.
5
- */
6
- import { writeFile, readFile, mkdir } from 'node:fs/promises';
7
- import { existsSync } from 'node:fs';
8
- import { join } from 'node:path';
9
- import { createHash } from 'node:crypto';
10
- import { httpsPost, httpsGet, safeJsonParse } from './provisioners/http-client.js';
11
- // ── OpenAI Provider ──────────────────────────────────────
12
- const OPENAI_API = 'api.openai.com';
13
- /**
14
- * Generate an image via OpenAI's API.
15
- * Returns the raw image bytes as a Buffer.
16
- */
17
- export async function generateImage(apiKey, options, emit) {
18
- const model = options.model || 'gpt-image-1';
19
- const size = `${options.width}x${options.height}`;
20
- // OpenAI only supports specific sizes — map to nearest and warn
21
- const validSizes = ['1024x1024', '1792x1024', '1024x1792'];
22
- const actualSize = validSizes.includes(size) ? size : '1024x1024';
23
- if (actualSize !== size) {
24
- emit({ step: 'image-gen', status: 'started', message: `Requested ${size} → using ${actualSize} (API constraint)` });
25
- }
26
- const body = JSON.stringify({
27
- model,
28
- prompt: options.prompt,
29
- n: 1,
30
- size: actualSize,
31
- quality: options.quality || 'medium',
32
- response_format: 'b64_json',
33
- });
34
- // Retry logic: 3 attempts with exponential backoff (1s, 3s, 9s)
35
- // DALL-E 3 returns 500 errors on ~15% of requests (field report #1)
36
- const MAX_RETRIES = 3;
37
- const BACKOFF_BASE_MS = 1000;
38
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
39
- try {
40
- const res = await httpsPost(OPENAI_API, '/v1/images/generations', {
41
- 'Authorization': `Bearer ${apiKey}`,
42
- 'Content-Type': 'application/json',
43
- }, body, 120_000); // 2 min timeout for image generation
44
- if (res.status === 500 || res.status === 502 || res.status === 503) {
45
- // Server error — retry with backoff
46
- if (attempt < MAX_RETRIES) {
47
- const delay = BACKOFF_BASE_MS * Math.pow(3, attempt - 1);
48
- emit({ step: 'image-gen', status: 'started', message: `Server error (${res.status}), retrying in ${delay / 1000}s (attempt ${attempt}/${MAX_RETRIES})` });
49
- await new Promise((resolve) => setTimeout(resolve, delay));
50
- continue;
51
- }
52
- emit({ step: 'image-gen', status: 'error', message: `Server error (${res.status}) after ${MAX_RETRIES} attempts` });
53
- return null;
54
- }
55
- if (res.status !== 200) {
56
- const errData = safeJsonParse(res.body);
57
- const errMsg = errData?.error?.message || `API returned ${res.status}`;
58
- emit({ step: 'image-gen', status: 'error', message: `Generation failed: ${errMsg}` });
59
- return null;
60
- }
61
- const data = safeJsonParse(res.body);
62
- const b64 = data?.data?.[0]?.b64_json;
63
- if (!b64) {
64
- emit({ step: 'image-gen', status: 'error', message: 'No image data in API response' });
65
- return null;
66
- }
67
- if (attempt > 1) {
68
- emit({ step: 'image-gen', status: 'done', message: `Succeeded on attempt ${attempt}` });
69
- }
70
- return Buffer.from(b64, 'base64');
71
- }
72
- catch (err) {
73
- if (attempt < MAX_RETRIES) {
74
- const delay = BACKOFF_BASE_MS * Math.pow(3, attempt - 1);
75
- emit({ step: 'image-gen', status: 'started', message: `Request failed, retrying in ${delay / 1000}s (attempt ${attempt}/${MAX_RETRIES})`, detail: err.message });
76
- await new Promise((resolve) => setTimeout(resolve, delay));
77
- continue;
78
- }
79
- emit({ step: 'image-gen', status: 'error', message: `Image generation failed after ${MAX_RETRIES} attempts`, detail: err.message });
80
- return null;
81
- }
82
- }
83
- return null;
84
- }
85
- /**
86
- * Validate an OpenAI API key by making a lightweight models list request.
87
- */
88
- export async function validateOpenAIKey(apiKey) {
89
- try {
90
- const res = await httpsGet(OPENAI_API, '/v1/models', {
91
- 'Authorization': `Bearer ${apiKey}`,
92
- }, 10_000);
93
- return res.status === 200;
94
- }
95
- catch {
96
- return false;
97
- }
98
- }
99
- /**
100
- * Estimate the cost of generating N images.
101
- */
102
- export function estimateImageCost(count, model = 'gpt-image-1') {
103
- const costPerImage = {
104
- 'gpt-image-1': 0.04,
105
- 'dall-e-3': 0.08,
106
- };
107
- return count * (costPerImage[model] || 0.04);
108
- }
109
- // ── Asset Manifest ──────────────────────────────────────
110
- const MANIFEST_FILENAME = 'manifest.json';
111
- /**
112
- * Read the asset manifest from disk.
113
- */
114
- export async function readManifest(imagesDir) {
115
- const manifestPath = join(imagesDir, MANIFEST_FILENAME);
116
- try {
117
- const content = await readFile(manifestPath, 'utf-8');
118
- return JSON.parse(content);
119
- }
120
- catch {
121
- return null;
122
- }
123
- }
124
- /**
125
- * Write the asset manifest to disk.
126
- */
127
- export async function writeManifest(imagesDir, manifest) {
128
- await mkdir(imagesDir, { recursive: true });
129
- await writeFile(join(imagesDir, MANIFEST_FILENAME), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
130
- }
131
- /**
132
- * Save a generated image to disk and update the manifest.
133
- */
134
- export async function saveGeneratedImage(imagesDir, category, name, imageBuffer, prompt, size, manifest) {
135
- const categoryDir = join(imagesDir, category);
136
- await mkdir(categoryDir, { recursive: true });
137
- const filename = `${category}/${name}.png`;
138
- const filepath = join(imagesDir, filename);
139
- await writeFile(filepath, imageBuffer);
140
- const hash = createHash('sha256').update(imageBuffer).digest('hex');
141
- // Remove any existing entry with the same filename (dedup for --regen)
142
- manifest.assets = manifest.assets.filter(a => a.filename !== filename);
143
- manifest.assets.push({
144
- name,
145
- filename,
146
- prompt,
147
- size,
148
- generatedAt: new Date().toISOString(),
149
- hash: `sha256:${hash}`,
150
- });
151
- await writeManifest(imagesDir, manifest);
152
- return filepath;
153
- }
154
- /**
155
- * Check if an asset already exists on disk.
156
- */
157
- export function assetExists(imagesDir, category, name) {
158
- return existsSync(join(imagesDir, category, `${name}.png`));
159
- }
@@ -1,30 +0,0 @@
1
- /**
2
- * Natural Language Deploy — resolve prose deployment descriptions to YAML frontmatter.
3
- *
4
- * Parse: "I want a $20/month server with SSL and daily backups"
5
- * → { deploy: 'vps', instanceType: 't3.small', hostname: '', resilience: { backups: 'daily', ... } }
6
- *
7
- * Uses keyword matching and heuristics — no AI API call required.
8
- */
9
- export interface DeployConfig {
10
- deploy: 'vps' | 'vercel' | 'railway' | 'cloudflare' | 'static' | 'docker';
11
- instanceType: string;
12
- hostname: string;
13
- estimatedMonthlyCost: string;
14
- resilience: {
15
- multiEnv: boolean;
16
- previewDeploys: boolean;
17
- rollback: boolean;
18
- migrations: 'auto' | 'manual' | 'no';
19
- backups: 'daily' | 'weekly' | 'no';
20
- healthCheck: boolean;
21
- gracefulShutdown: boolean;
22
- errorBoundaries: boolean;
23
- rateLimiting: boolean;
24
- deadLetterQueue: boolean;
25
- };
26
- reasoning: string[];
27
- }
28
- export declare function resolveDeployConfig(prose: string): DeployConfig | null;
29
- /** Convert a DeployConfig to YAML frontmatter fragment. */
30
- export declare function toFrontmatter(config: DeployConfig): string;
@@ -1,186 +0,0 @@
1
- /**
2
- * Natural Language Deploy — resolve prose deployment descriptions to YAML frontmatter.
3
- *
4
- * Parse: "I want a $20/month server with SSL and daily backups"
5
- * → { deploy: 'vps', instanceType: 't3.small', hostname: '', resilience: { backups: 'daily', ... } }
6
- *
7
- * Uses keyword matching and heuristics — no AI API call required.
8
- */
9
- const BUDGET_TIERS = [
10
- { maxMonthly: 10, instanceType: 't3.micro', label: '~$8/mo' },
11
- { maxMonthly: 25, instanceType: 't3.small', label: '~$17/mo' },
12
- { maxMonthly: 50, instanceType: 't3.medium', label: '~$34/mo' },
13
- { maxMonthly: 100, instanceType: 't3.large', label: '~$68/mo' },
14
- { maxMonthly: Infinity, instanceType: 't3.xlarge', label: '~$136/mo' },
15
- ];
16
- function resolveInstanceFromBudget(budget) {
17
- return BUDGET_TIERS.find(t => budget <= t.maxMonthly) ?? BUDGET_TIERS[BUDGET_TIERS.length - 1];
18
- }
19
- // ── Keyword patterns ────────────────────────────
20
- const PLATFORM_KEYWORDS = [
21
- { pattern: /\bvercel\b/i, target: 'vercel', reason: 'Vercel mentioned explicitly' },
22
- { pattern: /\brailway\b/i, target: 'railway', reason: 'Railway mentioned explicitly' },
23
- { pattern: /\bcloudflare\b/i, target: 'cloudflare', reason: 'Cloudflare mentioned explicitly' },
24
- { pattern: /\bdocker\b|\bcontainer\b/i, target: 'docker', reason: 'Docker/container mentioned' },
25
- { pattern: /\bstatic\s*(?:site|hosting|files?)\b/i, target: 'static', reason: 'Static site hosting' },
26
- { pattern: /\bvps\b|\bserver\b|\bec2\b|\baws\b|\bssh\b/i, target: 'vps', reason: 'Server/VPS/AWS mentioned' },
27
- { pattern: /\bserverless\b|\bedge\b/i, target: 'vercel', reason: 'Serverless/edge → Vercel' },
28
- { pattern: /\bfree\s*tier\b|\bno\s*cost\b|\bfree\b/i, target: 'railway', reason: 'Free tier → Railway' },
29
- ];
30
- const FEATURE_KEYWORDS = [
31
- { pattern: /\bbackup/i, key: 'backups', reason: 'Backups requested' },
32
- { pattern: /\bssl\b|\bhttps\b|\btls\b/i, key: 'ssl', reason: 'SSL/TLS requested' },
33
- { pattern: /\bcustom\s*domain\b|\bmy\s*domain\b/i, key: 'customDomain', reason: 'Custom domain' },
34
- { pattern: /\brollback\b|\brevert\b/i, key: 'rollback', reason: 'Rollback requested' },
35
- { pattern: /\bpreview\b|\bpr\s*deploy/i, key: 'previewDeploys', reason: 'Preview deploys' },
36
- { pattern: /\bhealth\s*check\b|\bmonitoring\b|\buptime\b/i, key: 'healthCheck', reason: 'Health monitoring' },
37
- { pattern: /\brate\s*limit/i, key: 'rateLimiting', reason: 'Rate limiting' },
38
- { pattern: /\bgraceful\b|\bzero\s*downtime\b/i, key: 'gracefulShutdown', reason: 'Zero-downtime' },
39
- { pattern: /\bmulti\s*(?:env|environment)\b|\bstaging\b/i, key: 'multiEnv', reason: 'Multi-environment' },
40
- { pattern: /\berror\s*boundar/i, key: 'errorBoundaries', reason: 'Error boundaries' },
41
- { pattern: /\bdead\s*letter\b|\bdlq\b|\bretry\s*queue\b/i, key: 'deadLetterQueue', reason: 'Dead letter queue' },
42
- { pattern: /\bmigration/i, key: 'migrations', reason: 'Database migrations' },
43
- ];
44
- const SCALE_KEYWORDS = [
45
- { pattern: /\bsmall\b|\bsimple\b|\bblog\b|\bpersonal\b|\bside\s*project\b|\bmvp\b|\bprototype\b/i, scale: 'small', reason: 'Small/simple project' },
46
- { pattern: /\bmedium\b|\bstartup\b|\bsaas\b|\bteam\b|\bgrow/i, scale: 'medium', reason: 'Medium/startup scale' },
47
- { pattern: /\blarge\b|\benterprise\b|\bthousands\b|\bhigh\s*traffic\b|\bscale\b|\bproduction\b/i, scale: 'large', reason: 'Large/production scale' },
48
- ];
49
- // ── Main resolver ───────────────────────────────
50
- export function resolveDeployConfig(prose) {
51
- if (!prose.trim())
52
- return null;
53
- const reasoning = [];
54
- const features = new Set();
55
- // Extract budget — prefer amounts near cost keywords, fall back to first $N
56
- const costContextMatch = prose.match(/(?:budget|spend|cost|month|mo)[^$]*\$(\d+(?:\.\d+)?)/i)
57
- ?? prose.match(/\$(\d+(?:\.\d+)?)(?:\s*\/\s*mo(?:nth)?)/i)
58
- ?? prose.match(/\$(\d+(?:\.\d+)?)/i);
59
- const budget = costContextMatch ? Math.round(parseFloat(costContextMatch[1])) : -1;
60
- // Detect explicit platform
61
- let deploy = 'vps'; // default
62
- let platformDetected = false;
63
- for (const kw of PLATFORM_KEYWORDS) {
64
- if (kw.pattern.test(prose)) {
65
- deploy = kw.target;
66
- reasoning.push(kw.reason);
67
- platformDetected = true;
68
- break;
69
- }
70
- }
71
- // Detect features
72
- for (const kw of FEATURE_KEYWORDS) {
73
- if (kw.pattern.test(prose)) {
74
- features.add(kw.key);
75
- reasoning.push(kw.reason);
76
- }
77
- }
78
- // Detect scale
79
- let scale = 'small';
80
- for (const kw of SCALE_KEYWORDS) {
81
- if (kw.pattern.test(prose)) {
82
- scale = kw.scale;
83
- reasoning.push(kw.reason);
84
- break;
85
- }
86
- }
87
- // If no platform detected, infer from features and scale
88
- if (!platformDetected) {
89
- if (features.has('previewDeploys') || features.has('errorBoundaries')) {
90
- deploy = 'vercel';
91
- reasoning.push('Preview deploys/error boundaries → Vercel (best support)');
92
- }
93
- else if (scale === 'large' || features.has('customDomain') || budget > 30) {
94
- deploy = 'vps';
95
- reasoning.push('Large scale or custom domain with budget → VPS');
96
- }
97
- else if (scale === 'small' && budget < 0) {
98
- deploy = 'railway';
99
- reasoning.push('Small project, no budget specified → Railway (easiest start)');
100
- }
101
- else {
102
- deploy = 'vps';
103
- reasoning.push('Default → VPS (most flexible)');
104
- }
105
- }
106
- // Resolve instance type from budget or scale
107
- let instanceType = '';
108
- let estimatedCost = '';
109
- if (deploy === 'vps') {
110
- if (budget >= 0) {
111
- const tier = resolveInstanceFromBudget(budget);
112
- instanceType = tier.instanceType;
113
- estimatedCost = tier.label;
114
- reasoning.push(`Budget $${budget}/mo → ${tier.instanceType} (${tier.label})`);
115
- }
116
- else {
117
- const scaleMap = { small: 't3.micro', medium: 't3.small', large: 't3.medium' };
118
- const costMap = { small: '~$8/mo', medium: '~$17/mo', large: '~$34/mo' };
119
- instanceType = scaleMap[scale];
120
- estimatedCost = costMap[scale];
121
- reasoning.push(`${scale} scale → ${instanceType} (${estimatedCost})`);
122
- }
123
- }
124
- else {
125
- estimatedCost = deploy === 'railway' ? 'Free tier available' :
126
- deploy === 'vercel' ? 'Free tier available' :
127
- deploy === 'cloudflare' ? 'Free tier available' :
128
- deploy === 'static' ? 'Minimal (~$1/mo S3)' : 'Varies';
129
- }
130
- // Extract hostname if mentioned
131
- const hostnameMatch = prose.match(/(?:domain|hostname|url)[\s:]*([a-z0-9.-]+\.[a-z]{2,})/i);
132
- const hostname = hostnameMatch ? hostnameMatch[1] : '';
133
- if (hostname)
134
- reasoning.push(`Hostname detected: ${hostname}`);
135
- // Build resilience config — defaults based on deploy target + detected features
136
- const isVps = deploy === 'vps';
137
- const isPlatform = ['vercel', 'railway', 'cloudflare'].includes(deploy);
138
- const resilience = {
139
- multiEnv: features.has('multiEnv') || scale !== 'small',
140
- previewDeploys: features.has('previewDeploys') || (isPlatform && scale !== 'small'),
141
- rollback: features.has('rollback') || isPlatform,
142
- migrations: features.has('migrations') ? 'auto' : (isVps ? 'manual' : 'no'),
143
- backups: features.has('backups') ? 'daily' : (isVps && scale !== 'small' ? 'weekly' : 'no'),
144
- healthCheck: features.has('healthCheck') || isVps || scale !== 'small',
145
- gracefulShutdown: features.has('gracefulShutdown') || isVps,
146
- errorBoundaries: features.has('errorBoundaries'),
147
- rateLimiting: features.has('rateLimiting') || scale === 'large',
148
- deadLetterQueue: features.has('deadLetterQueue'),
149
- };
150
- return {
151
- deploy,
152
- instanceType,
153
- hostname,
154
- estimatedMonthlyCost: estimatedCost,
155
- resilience,
156
- reasoning,
157
- };
158
- }
159
- /** Sanitize a string for safe YAML double-quoted interpolation. */
160
- function yamlSafe(value) {
161
- return value.replace(/[\\"]/g, '');
162
- }
163
- /** Convert a DeployConfig to YAML frontmatter fragment. */
164
- export function toFrontmatter(config) {
165
- const lines = [
166
- `deploy: "${yamlSafe(config.deploy)}"`,
167
- ];
168
- if (config.instanceType) {
169
- lines.push(`instance_type: "${yamlSafe(config.instanceType)}"`);
170
- }
171
- if (config.hostname) {
172
- lines.push(`hostname: "${yamlSafe(config.hostname.toLowerCase())}"`);
173
- }
174
- lines.push('resilience:');
175
- lines.push(` multi-env: ${config.resilience.multiEnv ? 'yes' : 'no'}`);
176
- lines.push(` preview-deploys: ${config.resilience.previewDeploys ? 'yes' : 'no'}`);
177
- lines.push(` rollback: ${config.resilience.rollback ? 'yes' : 'no'}`);
178
- lines.push(` migrations: "${yamlSafe(config.resilience.migrations)}"`);
179
- lines.push(` backups: "${yamlSafe(config.resilience.backups)}"`);
180
- lines.push(` health-check: ${config.resilience.healthCheck ? 'yes' : 'no'}`);
181
- lines.push(` graceful-shutdown: ${config.resilience.gracefulShutdown ? 'yes' : 'no'}`);
182
- lines.push(` error-boundaries: ${config.resilience.errorBoundaries ? 'yes' : 'no'}`);
183
- lines.push(` rate-limiting: ${config.resilience.rateLimiting ? 'yes' : 'no'}`);
184
- lines.push(` dead-letter-queue: ${config.resilience.deadLetterQueue ? 'yes' : 'no'}`);
185
- return lines.join('\n');
186
- }
@@ -1,28 +0,0 @@
1
- /**
2
- * Paris's Route Optimizer — ROI-weighted campaign sequencing (v12.3).
3
- *
4
- * Given multiple possible campaigns (from the proposal generator), Paris
5
- * computes the optimal execution order based on estimated ROI, dependencies,
6
- * risk, and urgency.
7
- *
8
- * PRD Reference: ROADMAP v12.3, DEEP_CURRENT.md Paris's role
9
- */
10
- import type { CampaignProposal } from './campaign-proposer.js';
11
- import type { SituationModel } from './deep-current.js';
12
- interface RouteScore {
13
- proposal: CampaignProposal;
14
- roiScore: number;
15
- urgencyScore: number;
16
- riskScore: number;
17
- totalScore: number;
18
- }
19
- /**
20
- * Score and rank campaign proposals by optimal execution order.
21
- * Returns proposals sorted by total score (highest first = execute first).
22
- */
23
- export declare function optimizeRoute(proposals: CampaignProposal[], model: SituationModel): RouteScore[];
24
- /**
25
- * Pick the single best campaign to execute next.
26
- */
27
- export declare function pickBestCampaign(proposals: CampaignProposal[], model: SituationModel): CampaignProposal | null;
28
- export type { RouteScore };
@@ -1,93 +0,0 @@
1
- /**
2
- * Paris's Route Optimizer — ROI-weighted campaign sequencing (v12.3).
3
- *
4
- * Given multiple possible campaigns (from the proposal generator), Paris
5
- * computes the optimal execution order based on estimated ROI, dependencies,
6
- * risk, and urgency.
7
- *
8
- * PRD Reference: ROADMAP v12.3, DEEP_CURRENT.md Paris's role
9
- */
10
- // ── Scoring Weights ───────────────────────────────────
11
- const WEIGHTS = {
12
- roi: 0.40,
13
- urgency: 0.35,
14
- risk: 0.25, // Inverted — lower risk gets higher score
15
- };
16
- // ── ROI Estimation ────────────────────────────────────
17
- function estimateRoi(proposal, model) {
18
- // ROI = (dimension improvement potential) / (estimated effort)
19
- const currentScore = proposal.dimensionScore;
20
- const potentialGain = Math.min(30, 100 - currentScore); // Cap at 30-point improvement
21
- const effort = proposal.estimatedSessions;
22
- // Higher gain per session = higher ROI
23
- const roiRatio = potentialGain / Math.max(effort, 1);
24
- return Math.min(100, Math.round(roiRatio * 10)); // Scale to 0-100
25
- }
26
- // ── Urgency Scoring ───────────────────────────────────
27
- function scoreUrgency(proposal, model) {
28
- const dim = proposal.dimension;
29
- const score = proposal.dimensionScore;
30
- // Critical defects are always urgent
31
- if (dim === 'quality' && score < 30)
32
- return 100;
33
- // Security issues are urgent
34
- if (dim === 'performance' && model.lastSiteScan && !model.lastSiteScan.security.https)
35
- return 90;
36
- // Low scores are more urgent
37
- if (score < 20)
38
- return 80;
39
- if (score < 40)
40
- return 60;
41
- if (score < 60)
42
- return 40;
43
- // Revenue is urgent for OPERATING projects
44
- if (dim === 'revenuePotential' && model.projectState === 'OPERATING')
45
- return 70;
46
- return 20; // Low urgency by default
47
- }
48
- // ── Risk Scoring ──────────────────────────────────────
49
- function scoreRisk(proposal) {
50
- // Revenue/payment campaigns are higher risk (real money)
51
- if (proposal.dimension === 'revenuePotential')
52
- return 70;
53
- // Feature campaigns have moderate risk (new code)
54
- if (proposal.dimension === 'featureCompleteness')
55
- return 50;
56
- // Quality and performance campaigns are low risk
57
- if (proposal.dimension === 'quality')
58
- return 20;
59
- if (proposal.dimension === 'performance')
60
- return 25;
61
- // Growth foundation is low risk (additive, no existing code modified)
62
- if (proposal.dimension === 'growthReadiness')
63
- return 30;
64
- return 40;
65
- }
66
- // ── Route Optimization ────────────────────────────────
67
- /**
68
- * Score and rank campaign proposals by optimal execution order.
69
- * Returns proposals sorted by total score (highest first = execute first).
70
- */
71
- export function optimizeRoute(proposals, model) {
72
- const scored = proposals.map(proposal => {
73
- const roiScore = estimateRoi(proposal, model);
74
- const urgencyScore = scoreUrgency(proposal, model);
75
- const riskScore = 100 - scoreRisk(proposal); // Invert: low risk = high score
76
- const totalScore = Math.round(roiScore * WEIGHTS.roi +
77
- urgencyScore * WEIGHTS.urgency +
78
- riskScore * WEIGHTS.risk);
79
- return { proposal, roiScore, urgencyScore, riskScore, totalScore };
80
- });
81
- // Sort by total score descending (best first)
82
- scored.sort((a, b) => b.totalScore - a.totalScore);
83
- return scored;
84
- }
85
- /**
86
- * Pick the single best campaign to execute next.
87
- */
88
- export function pickBestCampaign(proposals, model) {
89
- if (proposals.length === 0)
90
- return null;
91
- const ranked = optimizeRoute(proposals, model);
92
- return ranked[0].proposal;
93
- }
@@ -1,18 +0,0 @@
1
- /**
2
- * Service Install — launchd/systemd/Task Scheduler integration (§9.18, §9.19.2).
3
- *
4
- * Creates system services for:
5
- * 1. Heartbeat daemon (com.voidforge.heartbeat)
6
- * 2. Wizard server (com.voidforge.server) — persistent when Cultivation is installed
7
- *
8
- * PRD Reference: §9.18 (macOS LaunchAgent), §9.19.2 (two services)
9
- */
10
- export declare function installHeartbeatService(): Promise<{
11
- method: string;
12
- path: string;
13
- }>;
14
- export declare function installServerService(port?: number): Promise<{
15
- method: string;
16
- path: string;
17
- }>;
18
- export declare function uninstallServices(): Promise<void>;