voidforge-build 23.10.0 → 23.11.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/.claude/agents/bashir-field-medic.md +1 -0
- package/dist/.claude/agents/coulson-release.md +3 -0
- package/dist/.claude/agents/irulan-historian.md +3 -0
- package/dist/.claude/agents/loki-chaos.md +1 -0
- package/dist/.claude/agents/picard-architecture.md +3 -0
- package/dist/.claude/agents/silver-surfer-herald.md +3 -0
- package/dist/.claude/agents/sisko-campaign.md +3 -0
- package/dist/.claude/commands/architect.md +38 -0
- package/dist/.claude/commands/campaign.md +2 -0
- package/dist/.claude/commands/gauntlet.md +11 -0
- package/dist/.claude/commands/git.md +13 -3
- package/dist/CHANGELOG.md +63 -0
- package/dist/CLAUDE.md +13 -4
- package/dist/VERSION.md +2 -1
- package/dist/docs/methods/AI_INTELLIGENCE.md +15 -0
- package/dist/docs/methods/BACKEND_ENGINEER.md +48 -0
- package/dist/docs/methods/CAMPAIGN.md +196 -1
- package/dist/docs/methods/DEVOPS_ENGINEER.md +16 -0
- package/dist/docs/methods/FORGE_KEEPER.md +18 -0
- package/dist/docs/methods/GAUNTLET.md +2 -0
- package/dist/docs/methods/QA_ENGINEER.md +46 -0
- package/dist/docs/methods/RELEASE_MANAGER.md +59 -0
- package/dist/docs/methods/SECURITY_AUDITOR.md +53 -0
- package/dist/docs/methods/SUB_AGENTS.md +90 -0
- package/dist/docs/methods/SYSTEMS_ARCHITECT.md +42 -2
- package/dist/docs/methods/TESTING.md +17 -0
- package/dist/docs/methods/TIME_VAULT.md +17 -0
- package/dist/docs/patterns/adr-verification-gate.md +80 -0
- package/dist/docs/patterns/ai-eval.ts +87 -0
- package/dist/docs/patterns/ai-prompt-safety.ts +242 -0
- package/dist/docs/patterns/audit-log.ts +132 -0
- package/dist/docs/patterns/llm-state-dedup.ts +246 -0
- package/dist/docs/patterns/middleware.ts +83 -0
- package/dist/docs/patterns/multi-tenant-pool-bypass.ts +134 -0
- package/dist/docs/patterns/multi-tenant-property-test.ts +127 -0
- package/dist/docs/patterns/refactor-extraction.md +96 -0
- package/dist/scripts/voidforge.js +0 -0
- package/dist/wizard/lib/anomaly-detection.d.ts +59 -0
- package/dist/wizard/lib/anomaly-detection.js +122 -0
- package/dist/wizard/lib/asset-scanner.d.ts +23 -0
- package/dist/wizard/lib/asset-scanner.js +107 -0
- package/dist/wizard/lib/build-analytics.d.ts +39 -0
- package/dist/wizard/lib/build-analytics.js +91 -0
- package/dist/wizard/lib/codegen/erd-gen.d.ts +16 -0
- package/dist/wizard/lib/codegen/erd-gen.js +98 -0
- package/dist/wizard/lib/codegen/openapi-gen.d.ts +15 -0
- package/dist/wizard/lib/codegen/openapi-gen.js +79 -0
- package/dist/wizard/lib/codegen/prisma-types.d.ts +15 -0
- package/dist/wizard/lib/codegen/prisma-types.js +44 -0
- package/dist/wizard/lib/codegen/seed-gen.d.ts +16 -0
- package/dist/wizard/lib/codegen/seed-gen.js +128 -0
- package/dist/wizard/lib/correlation-engine.d.ts +59 -0
- package/dist/wizard/lib/correlation-engine.js +152 -0
- package/dist/wizard/lib/desktop-notify.d.ts +27 -0
- package/dist/wizard/lib/desktop-notify.js +98 -0
- package/dist/wizard/lib/image-gen.d.ts +56 -0
- package/dist/wizard/lib/image-gen.js +159 -0
- package/dist/wizard/lib/natural-language-deploy.d.ts +30 -0
- package/dist/wizard/lib/natural-language-deploy.js +186 -0
- package/dist/wizard/lib/project-init.js +57 -0
- package/dist/wizard/lib/route-optimizer.d.ts +28 -0
- package/dist/wizard/lib/route-optimizer.js +93 -0
- package/dist/wizard/lib/service-install.d.ts +18 -0
- package/dist/wizard/lib/service-install.js +182 -0
- package/package.json +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI/Swagger spec generation (ADR-025).
|
|
3
|
+
* Generates a starter OpenAPI spec from framework conventions.
|
|
4
|
+
* Framework-aware: Express, Next.js API routes.
|
|
5
|
+
*/
|
|
6
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
function generateOpenAPISpec(projectName, framework) {
|
|
9
|
+
const isNextjs = framework === 'next.js';
|
|
10
|
+
// Strip YAML-unsafe characters — only allow alphanumeric, spaces, hyphens, underscores
|
|
11
|
+
const safeName = projectName.replace(/[^a-zA-Z0-9 _-]/g, '').slice(0, 100) || 'My Project';
|
|
12
|
+
return `# OpenAPI Specification
|
|
13
|
+
# Generated by VoidForge (ADR-025)
|
|
14
|
+
# Edit this file to document your API endpoints
|
|
15
|
+
# Serve with: npx swagger-ui-express (Express) or next-swagger-doc (Next.js)
|
|
16
|
+
|
|
17
|
+
openapi: "3.1.0"
|
|
18
|
+
info:
|
|
19
|
+
title: "${safeName} API"
|
|
20
|
+
version: "1.0.0"
|
|
21
|
+
description: "API documentation for ${safeName}"
|
|
22
|
+
|
|
23
|
+
servers:
|
|
24
|
+
- url: http://localhost:${isNextjs ? '3000' : '3001'}
|
|
25
|
+
description: Development server
|
|
26
|
+
|
|
27
|
+
paths:
|
|
28
|
+
/api/health:
|
|
29
|
+
get:
|
|
30
|
+
summary: Health check
|
|
31
|
+
responses:
|
|
32
|
+
"200":
|
|
33
|
+
description: Service is healthy
|
|
34
|
+
content:
|
|
35
|
+
application/json:
|
|
36
|
+
schema:
|
|
37
|
+
type: object
|
|
38
|
+
properties:
|
|
39
|
+
status:
|
|
40
|
+
type: string
|
|
41
|
+
example: "ok"
|
|
42
|
+
timestamp:
|
|
43
|
+
type: string
|
|
44
|
+
format: date-time
|
|
45
|
+
|
|
46
|
+
# Add your API endpoints below
|
|
47
|
+
# Example:
|
|
48
|
+
# /api/users:
|
|
49
|
+
# get:
|
|
50
|
+
# summary: List users
|
|
51
|
+
# parameters:
|
|
52
|
+
# - in: query
|
|
53
|
+
# name: page
|
|
54
|
+
# schema:
|
|
55
|
+
# type: integer
|
|
56
|
+
# default: 1
|
|
57
|
+
# responses:
|
|
58
|
+
# "200":
|
|
59
|
+
# description: List of users
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Generate an OpenAPI spec file for the project.
|
|
64
|
+
*/
|
|
65
|
+
export async function generateOpenAPIDoc(projectDir, projectName, framework, emit) {
|
|
66
|
+
emit({ step: 'openapi', status: 'started', message: 'Generating OpenAPI spec' });
|
|
67
|
+
try {
|
|
68
|
+
const docsDir = join(projectDir, 'docs');
|
|
69
|
+
await mkdir(docsDir, { recursive: true });
|
|
70
|
+
const spec = generateOpenAPISpec(projectName, framework);
|
|
71
|
+
await writeFile(join(docsDir, 'api.yaml'), spec, 'utf-8');
|
|
72
|
+
emit({ step: 'openapi', status: 'done', message: 'Generated docs/api.yaml — edit to document your API endpoints' });
|
|
73
|
+
return { success: true, file: 'docs/api.yaml' };
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
emit({ step: 'openapi', status: 'error', message: 'Failed to generate OpenAPI spec', detail: err.message });
|
|
77
|
+
return { success: false, file: '', error: err.message };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type generation from Prisma schema (ADR-025).
|
|
3
|
+
* After Prisma schema changes, runs prisma generate and creates a barrel export.
|
|
4
|
+
* Conditional — only runs if prisma/schema.prisma exists.
|
|
5
|
+
*/
|
|
6
|
+
import type { ProvisionEmitter } from '../provisioners/types.js';
|
|
7
|
+
export interface PrismaTypesResult {
|
|
8
|
+
success: boolean;
|
|
9
|
+
files: string[];
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Run prisma generate and create a barrel export for types.
|
|
14
|
+
*/
|
|
15
|
+
export declare function generatePrismaTypes(projectDir: string, emit: ProvisionEmitter): Promise<PrismaTypesResult>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type generation from Prisma schema (ADR-025).
|
|
3
|
+
* After Prisma schema changes, runs prisma generate and creates a barrel export.
|
|
4
|
+
* Conditional — only runs if prisma/schema.prisma exists.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
9
|
+
import { execCommand } from '../exec.js';
|
|
10
|
+
/**
|
|
11
|
+
* Run prisma generate and create a barrel export for types.
|
|
12
|
+
*/
|
|
13
|
+
export async function generatePrismaTypes(projectDir, emit) {
|
|
14
|
+
const schemaPath = join(projectDir, 'prisma', 'schema.prisma');
|
|
15
|
+
if (!existsSync(schemaPath)) {
|
|
16
|
+
emit({ step: 'prisma-types', status: 'skipped', message: 'No prisma/schema.prisma found — type generation skipped' });
|
|
17
|
+
return { success: true, files: [] };
|
|
18
|
+
}
|
|
19
|
+
emit({ step: 'prisma-types', status: 'started', message: 'Generating Prisma types' });
|
|
20
|
+
try {
|
|
21
|
+
// Run prisma generate
|
|
22
|
+
await execCommand('npx', ['prisma', 'generate'], {
|
|
23
|
+
cwd: projectDir,
|
|
24
|
+
timeout: 60_000,
|
|
25
|
+
});
|
|
26
|
+
// Create barrel export
|
|
27
|
+
const typesDir = join(projectDir, 'types');
|
|
28
|
+
await mkdir(typesDir, { recursive: true });
|
|
29
|
+
const barrelContent = `// Auto-generated barrel export for Prisma types
|
|
30
|
+
// Re-run: npx prisma generate
|
|
31
|
+
// Generated by VoidForge (ADR-025)
|
|
32
|
+
|
|
33
|
+
export * from '@prisma/client';
|
|
34
|
+
export type { Prisma } from '@prisma/client';
|
|
35
|
+
`;
|
|
36
|
+
await writeFile(join(typesDir, 'index.ts'), barrelContent, 'utf-8');
|
|
37
|
+
emit({ step: 'prisma-types', status: 'done', message: 'Prisma types generated — import from types/index.ts' });
|
|
38
|
+
return { success: true, files: ['types/index.ts'] };
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
emit({ step: 'prisma-types', status: 'error', message: 'Failed to generate Prisma types', detail: err.message });
|
|
42
|
+
return { success: false, files: [], error: err.message };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database seeding script generation (ADR-025).
|
|
3
|
+
* Generates seed.ts with factory functions for all Prisma models.
|
|
4
|
+
* Conditional — only runs if prisma/schema.prisma exists.
|
|
5
|
+
*/
|
|
6
|
+
import type { ProvisionEmitter } from '../provisioners/types.js';
|
|
7
|
+
export interface SeedResult {
|
|
8
|
+
success: boolean;
|
|
9
|
+
file: string;
|
|
10
|
+
modelCount: number;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Generate a database seed script from the Prisma schema.
|
|
15
|
+
*/
|
|
16
|
+
export declare function generateSeedScript(projectDir: string, emit: ProvisionEmitter): Promise<SeedResult>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database seeding script generation (ADR-025).
|
|
3
|
+
* Generates seed.ts with factory functions for all Prisma models.
|
|
4
|
+
* Conditional — only runs if prisma/schema.prisma exists.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
9
|
+
/** Map Prisma types to TypeScript factory values. */
|
|
10
|
+
const FACTORY_VALUES = {
|
|
11
|
+
'String': "'Test value'",
|
|
12
|
+
'Int': '1',
|
|
13
|
+
'Float': '1.0',
|
|
14
|
+
'Boolean': 'true',
|
|
15
|
+
'DateTime': 'new Date()',
|
|
16
|
+
'Json': '{}',
|
|
17
|
+
'BigInt': 'BigInt(1)',
|
|
18
|
+
'Decimal': '1.0',
|
|
19
|
+
};
|
|
20
|
+
function parseSeedModels(content) {
|
|
21
|
+
const models = [];
|
|
22
|
+
const modelRegex = /model\s+(\w+)\s*\{([^}]+)\}/g;
|
|
23
|
+
let match;
|
|
24
|
+
while ((match = modelRegex.exec(content)) !== null) {
|
|
25
|
+
const name = match[1];
|
|
26
|
+
const body = match[2];
|
|
27
|
+
const fields = [];
|
|
28
|
+
const builtinTypes = ['String', 'Int', 'Float', 'Boolean', 'DateTime', 'Json', 'BigInt', 'Decimal', 'Bytes'];
|
|
29
|
+
for (const line of body.split('\n')) {
|
|
30
|
+
const trimmed = line.trim();
|
|
31
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@'))
|
|
32
|
+
continue;
|
|
33
|
+
const fieldMatch = trimmed.match(/^(\w+)\s+(\w+)(\[\])?\s*(\?)?\s*(.*)/);
|
|
34
|
+
if (fieldMatch) {
|
|
35
|
+
const fieldName = fieldMatch[1];
|
|
36
|
+
const fieldType = fieldMatch[2];
|
|
37
|
+
const isOptional = !!fieldMatch[4];
|
|
38
|
+
const rest = fieldMatch[5] || '';
|
|
39
|
+
const hasDefault = rest.includes('@default');
|
|
40
|
+
const isId = rest.includes('@id');
|
|
41
|
+
const isRelation = !builtinTypes.includes(fieldType);
|
|
42
|
+
if (!isRelation && !fieldMatch[3]) { // Skip arrays (relations)
|
|
43
|
+
fields.push({ name: fieldName, type: fieldType, isOptional, hasDefault, isId, isRelation });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
models.push({ name, fields });
|
|
48
|
+
}
|
|
49
|
+
return models;
|
|
50
|
+
}
|
|
51
|
+
function buildSeedContent(models) {
|
|
52
|
+
const factories = [];
|
|
53
|
+
const seedCalls = [];
|
|
54
|
+
for (const model of models) {
|
|
55
|
+
const lowerName = model.name.charAt(0).toLowerCase() + model.name.slice(1);
|
|
56
|
+
const factoryFields = model.fields
|
|
57
|
+
.filter(f => !f.isId && !f.hasDefault && !f.isOptional)
|
|
58
|
+
.map(f => ` ${f.name}: ${FACTORY_VALUES[f.type] || "'TODO'"},`)
|
|
59
|
+
.join('\n');
|
|
60
|
+
factories.push(`function create${model.name}(overrides: Partial<Prisma.${model.name}CreateInput> = {}): Prisma.${model.name}CreateInput {
|
|
61
|
+
return {
|
|
62
|
+
${factoryFields}
|
|
63
|
+
...overrides,
|
|
64
|
+
};
|
|
65
|
+
}`);
|
|
66
|
+
seedCalls.push(` // ${model.name}
|
|
67
|
+
const ${lowerName} = await prisma.${lowerName}.create({ data: create${model.name}() });
|
|
68
|
+
console.log('Created ${model.name}:', ${lowerName}.id ?? '(no id)');`);
|
|
69
|
+
}
|
|
70
|
+
return `// Database seed script — generated by VoidForge (ADR-025)
|
|
71
|
+
// Run: npx tsx prisma/seed.ts
|
|
72
|
+
// Or: npm run seed (add "seed": "npx tsx prisma/seed.ts" to package.json scripts)
|
|
73
|
+
|
|
74
|
+
import { PrismaClient, Prisma } from '@prisma/client';
|
|
75
|
+
|
|
76
|
+
const prisma = new PrismaClient();
|
|
77
|
+
|
|
78
|
+
// ── Factory Functions ────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
${factories.join('\n\n')}
|
|
81
|
+
|
|
82
|
+
// ── Seed ─────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
async function seed() {
|
|
85
|
+
console.log('Seeding database...');
|
|
86
|
+
|
|
87
|
+
${seedCalls.join('\n\n')}
|
|
88
|
+
|
|
89
|
+
console.log('Seeding complete.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
seed()
|
|
93
|
+
.catch((e) => {
|
|
94
|
+
console.error('Seed failed:', e);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
})
|
|
97
|
+
.finally(() => prisma.$disconnect());
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Generate a database seed script from the Prisma schema.
|
|
102
|
+
*/
|
|
103
|
+
export async function generateSeedScript(projectDir, emit) {
|
|
104
|
+
const schemaPath = join(projectDir, 'prisma', 'schema.prisma');
|
|
105
|
+
if (!existsSync(schemaPath)) {
|
|
106
|
+
emit({ step: 'seed', status: 'skipped', message: 'No prisma/schema.prisma found — seed generation skipped' });
|
|
107
|
+
return { success: true, file: '', modelCount: 0 };
|
|
108
|
+
}
|
|
109
|
+
emit({ step: 'seed', status: 'started', message: 'Generating database seed script' });
|
|
110
|
+
try {
|
|
111
|
+
const schema = await readFile(schemaPath, 'utf-8');
|
|
112
|
+
const models = parseSeedModels(schema);
|
|
113
|
+
if (models.length === 0) {
|
|
114
|
+
emit({ step: 'seed', status: 'skipped', message: 'No models found in Prisma schema' });
|
|
115
|
+
return { success: true, file: '', modelCount: 0 };
|
|
116
|
+
}
|
|
117
|
+
const prismaDir = join(projectDir, 'prisma');
|
|
118
|
+
await mkdir(prismaDir, { recursive: true });
|
|
119
|
+
const script = buildSeedContent(models);
|
|
120
|
+
await writeFile(join(prismaDir, 'seed.ts'), script, 'utf-8');
|
|
121
|
+
emit({ step: 'seed', status: 'done', message: `Generated prisma/seed.ts — ${models.length} model factories. Run: npx tsx prisma/seed.ts` });
|
|
122
|
+
return { success: true, file: 'prisma/seed.ts', modelCount: models.length };
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
emit({ step: 'seed', status: 'error', message: 'Failed to generate seed script', detail: err.message });
|
|
126
|
+
return { success: false, file: '', modelCount: 0, error: err.message };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chakotay's Correlation Engine — connects product changes to metric outcomes (v12.2).
|
|
3
|
+
*
|
|
4
|
+
* Maintains an event log of product changes (campaigns, deploys) and metric
|
|
5
|
+
* observations (traffic, conversions, revenue, performance scores). Computes
|
|
6
|
+
* before/after comparisons to identify which changes drove which outcomes.
|
|
7
|
+
*
|
|
8
|
+
* PRD Reference: ROADMAP v12.2, DEEP_CURRENT.md LEARN step
|
|
9
|
+
*/
|
|
10
|
+
import type { CampaignProposal } from './campaign-proposer.js';
|
|
11
|
+
interface ChangeEvent {
|
|
12
|
+
type: 'campaign_complete' | 'deploy' | 'config_change';
|
|
13
|
+
date: string;
|
|
14
|
+
campaign?: string;
|
|
15
|
+
missions?: number;
|
|
16
|
+
dimension?: string;
|
|
17
|
+
description: string;
|
|
18
|
+
}
|
|
19
|
+
interface MetricObservation {
|
|
20
|
+
type: 'metric';
|
|
21
|
+
date: string;
|
|
22
|
+
metric: string;
|
|
23
|
+
value: number;
|
|
24
|
+
source: string;
|
|
25
|
+
}
|
|
26
|
+
interface Correlation {
|
|
27
|
+
type: 'correlation';
|
|
28
|
+
date: string;
|
|
29
|
+
productChange: string;
|
|
30
|
+
metric: string;
|
|
31
|
+
valueBefore: number;
|
|
32
|
+
valueAfter: number;
|
|
33
|
+
delta: string;
|
|
34
|
+
confidence: 'high' | 'medium' | 'low';
|
|
35
|
+
lagDays: number;
|
|
36
|
+
}
|
|
37
|
+
export declare function logCampaignComplete(name: string, missions: number, dimension: string): Promise<void>;
|
|
38
|
+
export declare function logMetric(metric: string, value: number, source: string): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Detect correlations between recent campaigns and metric changes.
|
|
41
|
+
* Uses before/after comparison with configurable lag windows.
|
|
42
|
+
*/
|
|
43
|
+
export declare function detectCorrelations(lagDays?: number): Promise<Correlation[]>;
|
|
44
|
+
interface PredictionRecord {
|
|
45
|
+
proposalId: string;
|
|
46
|
+
proposalName: string;
|
|
47
|
+
predictedImpact: string;
|
|
48
|
+
actualImpact?: string;
|
|
49
|
+
accuracy?: number;
|
|
50
|
+
recordedAt: string;
|
|
51
|
+
evaluatedAt?: string;
|
|
52
|
+
}
|
|
53
|
+
export declare function recordPrediction(proposal: CampaignProposal): Promise<void>;
|
|
54
|
+
export declare function evaluatePrediction(proposalId: string, actualImpact: string, accuracy: number): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Calculate average prediction accuracy across all evaluated predictions.
|
|
57
|
+
*/
|
|
58
|
+
export declare function getAveragePredictionAccuracy(): Promise<number>;
|
|
59
|
+
export type { ChangeEvent, MetricObservation, Correlation, PredictionRecord };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chakotay's Correlation Engine — connects product changes to metric outcomes (v12.2).
|
|
3
|
+
*
|
|
4
|
+
* Maintains an event log of product changes (campaigns, deploys) and metric
|
|
5
|
+
* observations (traffic, conversions, revenue, performance scores). Computes
|
|
6
|
+
* before/after comparisons to identify which changes drove which outcomes.
|
|
7
|
+
*
|
|
8
|
+
* PRD Reference: ROADMAP v12.2, DEEP_CURRENT.md LEARN step
|
|
9
|
+
*/
|
|
10
|
+
import { appendFile, readFile, mkdir } from 'node:fs/promises';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { correlationsPath, predictionsPath, deepCurrentDir } from './deep-current.js';
|
|
13
|
+
import { findProjectRoot } from './marker.js';
|
|
14
|
+
// ── Event Logging ─────────────────────────────────────
|
|
15
|
+
async function appendEvent(entry) {
|
|
16
|
+
await mkdir(deepCurrentDir(findProjectRoot() ?? process.cwd()), { recursive: true });
|
|
17
|
+
await appendFile(correlationsPath(findProjectRoot() ?? process.cwd()), JSON.stringify(entry) + '\n', 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
export async function logCampaignComplete(name, missions, dimension) {
|
|
20
|
+
await appendEvent({
|
|
21
|
+
type: 'campaign_complete',
|
|
22
|
+
date: new Date().toISOString(),
|
|
23
|
+
campaign: name,
|
|
24
|
+
missions,
|
|
25
|
+
dimension,
|
|
26
|
+
description: `Campaign "${name}" completed (${missions} missions, targeting ${dimension})`,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export async function logMetric(metric, value, source) {
|
|
30
|
+
await appendEvent({
|
|
31
|
+
type: 'metric',
|
|
32
|
+
date: new Date().toISOString(),
|
|
33
|
+
metric,
|
|
34
|
+
value,
|
|
35
|
+
source,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// ── Correlation Detection ─────────────────────────────
|
|
39
|
+
/**
|
|
40
|
+
* Detect correlations between recent campaigns and metric changes.
|
|
41
|
+
* Uses before/after comparison with configurable lag windows.
|
|
42
|
+
*/
|
|
43
|
+
export async function detectCorrelations(lagDays = 7) {
|
|
44
|
+
if (!existsSync(correlationsPath(findProjectRoot() ?? process.cwd())))
|
|
45
|
+
return [];
|
|
46
|
+
const content = await readFile(correlationsPath(findProjectRoot() ?? process.cwd()), 'utf-8');
|
|
47
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
48
|
+
const events = lines.map(l => { try {
|
|
49
|
+
return JSON.parse(l);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
} }).filter(Boolean);
|
|
54
|
+
const changes = events.filter(e => e.type === 'campaign_complete');
|
|
55
|
+
const metrics = events.filter(e => e.type === 'metric');
|
|
56
|
+
const correlations = [];
|
|
57
|
+
for (const change of changes) {
|
|
58
|
+
const changeDate = new Date(change.date).getTime();
|
|
59
|
+
// Find metrics of the same type before and after the change
|
|
60
|
+
const metricNames = [...new Set(metrics.map(m => m.metric))];
|
|
61
|
+
for (const metricName of metricNames) {
|
|
62
|
+
const metricsOfType = metrics.filter(m => m.metric === metricName);
|
|
63
|
+
// Before: last metric reading before the change
|
|
64
|
+
const before = metricsOfType
|
|
65
|
+
.filter(m => new Date(m.date).getTime() < changeDate)
|
|
66
|
+
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())[0];
|
|
67
|
+
// After: first metric reading lagDays after the change
|
|
68
|
+
const afterCutoff = changeDate + lagDays * 24 * 60 * 60 * 1000;
|
|
69
|
+
const after = metricsOfType
|
|
70
|
+
.filter(m => {
|
|
71
|
+
const t = new Date(m.date).getTime();
|
|
72
|
+
return t > changeDate && t <= afterCutoff;
|
|
73
|
+
})
|
|
74
|
+
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())[0];
|
|
75
|
+
if (before && after && before.value !== after.value) {
|
|
76
|
+
const delta = after.value - before.value;
|
|
77
|
+
const pctChange = before.value !== 0 ? Math.round((delta / before.value) * 100) : 0;
|
|
78
|
+
const significance = Math.abs(pctChange);
|
|
79
|
+
// Only record meaningful changes (>5%)
|
|
80
|
+
if (significance > 5) {
|
|
81
|
+
const correlation = {
|
|
82
|
+
type: 'correlation',
|
|
83
|
+
date: new Date().toISOString(),
|
|
84
|
+
productChange: change.campaign || change.description,
|
|
85
|
+
metric: metricName,
|
|
86
|
+
valueBefore: before.value,
|
|
87
|
+
valueAfter: after.value,
|
|
88
|
+
delta: `${delta > 0 ? '+' : ''}${pctChange}%`,
|
|
89
|
+
confidence: significance > 30 ? 'high' : significance > 15 ? 'medium' : 'low',
|
|
90
|
+
lagDays,
|
|
91
|
+
};
|
|
92
|
+
correlations.push(correlation);
|
|
93
|
+
await appendEvent(correlation); // Record the correlation in the log
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return correlations;
|
|
99
|
+
}
|
|
100
|
+
export async function recordPrediction(proposal) {
|
|
101
|
+
await mkdir(deepCurrentDir(findProjectRoot() ?? process.cwd()), { recursive: true });
|
|
102
|
+
const record = {
|
|
103
|
+
proposalId: proposal.id,
|
|
104
|
+
proposalName: proposal.name,
|
|
105
|
+
predictedImpact: proposal.expectedImpact,
|
|
106
|
+
recordedAt: new Date().toISOString(),
|
|
107
|
+
};
|
|
108
|
+
await appendFile(predictionsPath(findProjectRoot() ?? process.cwd()), JSON.stringify(record) + '\n', 'utf-8');
|
|
109
|
+
}
|
|
110
|
+
export async function evaluatePrediction(proposalId, actualImpact, accuracy) {
|
|
111
|
+
// Read existing predictions, find the matching one, update it
|
|
112
|
+
if (!existsSync(predictionsPath(findProjectRoot() ?? process.cwd())))
|
|
113
|
+
return;
|
|
114
|
+
const content = await readFile(predictionsPath(findProjectRoot() ?? process.cwd()), 'utf-8');
|
|
115
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
116
|
+
const updated = lines.map(line => {
|
|
117
|
+
try {
|
|
118
|
+
const record = JSON.parse(line);
|
|
119
|
+
if (record.proposalId === proposalId) {
|
|
120
|
+
record.actualImpact = actualImpact;
|
|
121
|
+
record.accuracy = accuracy;
|
|
122
|
+
record.evaluatedAt = new Date().toISOString();
|
|
123
|
+
}
|
|
124
|
+
return JSON.stringify(record);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return line;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
const { writeFile: wf } = await import('node:fs/promises');
|
|
131
|
+
await wf(predictionsPath(findProjectRoot() ?? process.cwd()), updated.join('\n') + '\n');
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Calculate average prediction accuracy across all evaluated predictions.
|
|
135
|
+
*/
|
|
136
|
+
export async function getAveragePredictionAccuracy() {
|
|
137
|
+
if (!existsSync(predictionsPath(findProjectRoot() ?? process.cwd())))
|
|
138
|
+
return 0;
|
|
139
|
+
const content = await readFile(predictionsPath(findProjectRoot() ?? process.cwd()), 'utf-8');
|
|
140
|
+
const records = content.trim().split('\n').filter(Boolean)
|
|
141
|
+
.map(l => { try {
|
|
142
|
+
return JSON.parse(l);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
} })
|
|
147
|
+
.filter(Boolean);
|
|
148
|
+
const evaluated = records.filter(r => r.accuracy !== undefined);
|
|
149
|
+
if (evaluated.length === 0)
|
|
150
|
+
return 0;
|
|
151
|
+
return evaluated.reduce((sum, r) => sum + (r.accuracy || 0), 0) / evaluated.length;
|
|
152
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
type NotificationUrgency = 'low' | 'normal' | 'critical';
|
|
11
|
+
interface NotificationOptions {
|
|
12
|
+
title: string;
|
|
13
|
+
message: string;
|
|
14
|
+
urgency?: NotificationUrgency;
|
|
15
|
+
sound?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Send a desktop notification. Fails silently — never throws.
|
|
19
|
+
*/
|
|
20
|
+
export declare function notify(opts: NotificationOptions): void;
|
|
21
|
+
export declare function notifySpendSpike(platform: string, amount: string): void;
|
|
22
|
+
export declare function notifyCampaignKilled(name: string, reason: string): void;
|
|
23
|
+
export declare function notifyTokenExpiring(platform: string, hoursLeft: number): void;
|
|
24
|
+
export declare function notifyReconciliationDiscrepancy(platform: string, amount: string): void;
|
|
25
|
+
export declare function notifyVaultExpiring(hoursLeft: number): void;
|
|
26
|
+
export declare function notifyRevenueMilestone(amount: string): void;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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;
|