voidforge-build 23.9.1 → 23.9.2

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 (30) hide show
  1. package/dist/CHANGELOG.md +20 -0
  2. package/dist/VERSION.md +2 -1
  3. package/dist/scripts/voidforge.js +0 -0
  4. package/package.json +1 -1
  5. package/dist/wizard/lib/anomaly-detection.d.ts +0 -59
  6. package/dist/wizard/lib/anomaly-detection.js +0 -122
  7. package/dist/wizard/lib/asset-scanner.d.ts +0 -23
  8. package/dist/wizard/lib/asset-scanner.js +0 -107
  9. package/dist/wizard/lib/build-analytics.d.ts +0 -39
  10. package/dist/wizard/lib/build-analytics.js +0 -91
  11. package/dist/wizard/lib/codegen/erd-gen.d.ts +0 -16
  12. package/dist/wizard/lib/codegen/erd-gen.js +0 -98
  13. package/dist/wizard/lib/codegen/openapi-gen.d.ts +0 -15
  14. package/dist/wizard/lib/codegen/openapi-gen.js +0 -79
  15. package/dist/wizard/lib/codegen/prisma-types.d.ts +0 -15
  16. package/dist/wizard/lib/codegen/prisma-types.js +0 -44
  17. package/dist/wizard/lib/codegen/seed-gen.d.ts +0 -16
  18. package/dist/wizard/lib/codegen/seed-gen.js +0 -128
  19. package/dist/wizard/lib/correlation-engine.d.ts +0 -59
  20. package/dist/wizard/lib/correlation-engine.js +0 -152
  21. package/dist/wizard/lib/desktop-notify.d.ts +0 -27
  22. package/dist/wizard/lib/desktop-notify.js +0 -98
  23. package/dist/wizard/lib/image-gen.d.ts +0 -56
  24. package/dist/wizard/lib/image-gen.js +0 -159
  25. package/dist/wizard/lib/natural-language-deploy.d.ts +0 -30
  26. package/dist/wizard/lib/natural-language-deploy.js +0 -186
  27. package/dist/wizard/lib/route-optimizer.d.ts +0 -28
  28. package/dist/wizard/lib/route-optimizer.js +0 -93
  29. package/dist/wizard/lib/service-install.d.ts +0 -18
  30. package/dist/wizard/lib/service-install.js +0 -182
package/dist/CHANGELOG.md CHANGED
@@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
6
6
 
7
7
  ---
8
8
 
9
+ ## [23.9.2] - 2026-04-20
10
+
11
+ ### CI workflow idempotency + provenance baseline
12
+
13
+ v23.9.1 was published manually from a maintainer laptop and thus ships without npm provenance attestation (OIDC is CI-only). v23.9.2 re-publishes via CI tag-push to establish provenance as the baseline for future releases, and hardens the workflow so future accidental manual-then-tag sequences are non-destructive.
14
+
15
+ ### Changed
16
+ - **`.github/workflows/publish.yml`** — both publish jobs now run a `check-*` step first (`npm view <pkg>@<version>`) and set `skip=true` if the current version is already on the registry. The actual `npm publish` step runs conditionally on `skip == 'false'`. This makes the workflow idempotent: re-triggering a tag, or tagging after a manual publish, is a no-op instead of a failure.
17
+
18
+ ### Release notes
19
+ - If CI fails on this tag due to NPM_TOKEN scope (SEC-002 from ADR-061 — token was issued for `thevoidforge` and may not have write access on `voidforge-build`), rotate the token per npm account → Access Tokens. Create a new Automation token with publish scope for `voidforge-build` AND `voidforge-build-methodology` AND legacy `thevoidforge` + `thevoidforge-methodology` (for any future deprecate or owner-management operations). Update GitHub repo Secret `NPM_TOKEN` and re-run the workflow.
20
+ - Provenance verification once attached: `npm view voidforge-build@23.9.2 --json | jq '.dist.attestations'` returns non-null.
21
+
22
+ ### Verification
23
+ - `npm test` — 1384/1384 pass
24
+ - `bash scripts/surfer-gate/test.sh` — 20/20 pass
25
+ - Local `npm publish --dry-run -w packages/voidforge` confirms packable state.
26
+
27
+ ---
28
+
9
29
  ## [23.9.1] - 2026-04-20
10
30
 
11
31
  ### Publish-target pivot — `voidforge-build` supersedes `@voidforge/cli`
package/dist/VERSION.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Version
2
2
 
3
- **Current:** 23.9.1
3
+ **Current:** 23.9.2
4
4
 
5
5
  ## Versioning Scheme
6
6
 
@@ -14,6 +14,7 @@ This project uses [Semantic Versioning](https://semver.org/):
14
14
 
15
15
  | Version | Date | Summary |
16
16
  |---------|------|---------|
17
+ | 23.9.2 | 2026-04-20 | CI workflow idempotency + provenance baseline — `publish.yml` guards each publish with "already-published" check so re-runs skip cleanly. Tag-push re-publishes via CI to attach npm provenance attestation (absent on v23.9.1's manual publish). |
17
18
  | 23.9.1 | 2026-04-20 | ADR-061 pivot — `@voidforge` npm org unavailable (squat-adjacent). Rebranded publish target to `voidforge-build` / `voidforge-build-methodology` matching the voidforge.build domain. Migration banner for legacy `thevoidforge` / `@voidforge/cli` installs. Farewell releases + npm deprecate for smooth transition. |
18
19
  | 23.9.0 | 2026-04-20 | Campaign 42 — @voidforge scoped npm rename (ADR-061), gauntlet --fast 3-round mandate, README value-prop + first-command pointer, LEARNINGS.md 4 entries. Victory Gauntlet 3 fix batches: methodology runtime dep, registry-pin + env-stripping, BLOCK absolute paths. Publish gated on user scope claim + NPM_TOKEN rotation. |
19
20
  | 23.8.12 | 2026-04-12 | Field report triage (#299, #300) — campaign autonomy fix, ToS checks, deploy type-check gate, 3 operational learnings |
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voidforge-build",
3
- "version": "23.9.1",
3
+ "version": "23.9.2",
4
4
  "description": "From nothing, everything. A methodology framework for building with Claude Code.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,59 +0,0 @@
1
- /**
2
- * Anomaly Detection — Spend spikes, traffic drops, conversion changes (§9.17).
3
- *
4
- * Runs hourly as a heartbeat daemon scheduled job.
5
- * Compares current metrics against rolling averages.
6
- * Alerts when deviations exceed thresholds.
7
- *
8
- * PRD Reference: §9.7 (hourly anomaly detection), §9.17 (thresholds)
9
- */
10
- type Cents = number & {
11
- readonly __brand: 'Cents';
12
- };
13
- type AnomalyType = 'spend_spike' | 'traffic_drop' | 'conversion_change' | 'roas_drop';
14
- type AnomalySeverity = 'warning' | 'alert' | 'critical';
15
- interface Anomaly {
16
- type: AnomalyType;
17
- severity: AnomalySeverity;
18
- platform?: string;
19
- metric: string;
20
- currentValue: number;
21
- expectedValue: number;
22
- deviationPercent: number;
23
- message: string;
24
- timestamp: string;
25
- }
26
- declare const THRESHOLDS: {
27
- spendSpikeWarning: number;
28
- spendSpikeAlert: number;
29
- spendSpikeCritical: number;
30
- trafficDropWarning: number;
31
- trafficDropAlert: number;
32
- trafficDropCritical: number;
33
- conversionChangeThreshold: number;
34
- roasDropWarning: number;
35
- roasDropAlert: number;
36
- };
37
- /** Run all anomaly checks for the current period */
38
- export declare function runAnomalyDetection(metrics: {
39
- spendByPlatform: Array<{
40
- platform: string;
41
- currentHour: Cents;
42
- avgHourly: Cents;
43
- }>;
44
- traffic: {
45
- currentDay: number;
46
- avgDaily: number;
47
- };
48
- conversion: {
49
- currentRate: number;
50
- avgRate: number;
51
- };
52
- roasByPlatform: Array<{
53
- platform: string;
54
- current: number;
55
- avg: number;
56
- }>;
57
- }): Anomaly[];
58
- export type { Anomaly, AnomalyType, AnomalySeverity };
59
- export { THRESHOLDS };
@@ -1,122 +0,0 @@
1
- /**
2
- * Anomaly Detection — Spend spikes, traffic drops, conversion changes (§9.17).
3
- *
4
- * Runs hourly as a heartbeat daemon scheduled job.
5
- * Compares current metrics against rolling averages.
6
- * Alerts when deviations exceed thresholds.
7
- *
8
- * PRD Reference: §9.7 (hourly anomaly detection), §9.17 (thresholds)
9
- */
10
- // ── Thresholds ────────────────────────────────────────
11
- const THRESHOLDS = {
12
- // Spend spike: current hour spend > X% of daily average hourly spend
13
- spendSpikeWarning: 50, // 50% above average
14
- spendSpikeAlert: 100, // 100% above average (double)
15
- spendSpikeCritical: 200, // 200% above average (triple)
16
- // Traffic drop: current day traffic > X% below 7-day average
17
- trafficDropWarning: 20, // 20% below average
18
- trafficDropAlert: 40, // 40% below average
19
- trafficDropCritical: 60, // 60% below average
20
- // Conversion rate change: > X% from 7-day average
21
- conversionChangeThreshold: 20, // 20% change in either direction
22
- // ROAS drop: current < X% of 7-day average
23
- roasDropWarning: 20, // 20% below average
24
- roasDropAlert: 40, // 40% below average
25
- };
26
- // ── Detection Functions ───────────────────────────────
27
- function detectSpendSpike(currentHourSpend, avgHourlySpend, platform) {
28
- if (avgHourlySpend === 0)
29
- return null;
30
- const deviation = ((currentHourSpend - avgHourlySpend) / avgHourlySpend) * 100;
31
- if (deviation < THRESHOLDS.spendSpikeWarning)
32
- return null;
33
- const severity = deviation >= THRESHOLDS.spendSpikeCritical ? 'critical' :
34
- deviation >= THRESHOLDS.spendSpikeAlert ? 'alert' : 'warning';
35
- return {
36
- type: 'spend_spike',
37
- severity,
38
- platform,
39
- metric: 'hourly_spend',
40
- currentValue: currentHourSpend,
41
- expectedValue: avgHourlySpend,
42
- deviationPercent: Math.round(deviation),
43
- message: `Spend spike on ${platform}: $${(currentHourSpend / 100).toFixed(2)}/hr vs $${(avgHourlySpend / 100).toFixed(2)}/hr average (+${Math.round(deviation)}%)`,
44
- timestamp: new Date().toISOString(),
45
- };
46
- }
47
- function detectTrafficDrop(currentDayTraffic, avgDailyTraffic) {
48
- if (avgDailyTraffic === 0)
49
- return null;
50
- const deviation = ((avgDailyTraffic - currentDayTraffic) / avgDailyTraffic) * 100;
51
- if (deviation < THRESHOLDS.trafficDropWarning)
52
- return null;
53
- const severity = deviation >= THRESHOLDS.trafficDropCritical ? 'critical' :
54
- deviation >= THRESHOLDS.trafficDropAlert ? 'alert' : 'warning';
55
- return {
56
- type: 'traffic_drop',
57
- severity,
58
- metric: 'daily_traffic',
59
- currentValue: currentDayTraffic,
60
- expectedValue: avgDailyTraffic,
61
- deviationPercent: -Math.round(deviation),
62
- message: `Traffic drop: ${currentDayTraffic} visitors today vs ${avgDailyTraffic} average (-${Math.round(deviation)}%)`,
63
- timestamp: new Date().toISOString(),
64
- };
65
- }
66
- function detectConversionChange(currentRate, avgRate) {
67
- if (avgRate === 0)
68
- return null;
69
- const deviation = ((currentRate - avgRate) / avgRate) * 100;
70
- if (Math.abs(deviation) < THRESHOLDS.conversionChangeThreshold)
71
- return null;
72
- return {
73
- type: 'conversion_change',
74
- severity: Math.abs(deviation) >= 40 ? 'alert' : 'warning',
75
- metric: 'conversion_rate',
76
- currentValue: currentRate,
77
- expectedValue: avgRate,
78
- deviationPercent: Math.round(deviation),
79
- message: `Conversion rate ${deviation > 0 ? 'increase' : 'decrease'}: ${currentRate.toFixed(1)}% vs ${avgRate.toFixed(1)}% average (${deviation > 0 ? '+' : ''}${Math.round(deviation)}%)`,
80
- timestamp: new Date().toISOString(),
81
- };
82
- }
83
- function detectRoasDrop(currentRoas, avgRoas, platform) {
84
- if (avgRoas === 0)
85
- return null;
86
- const deviation = ((avgRoas - currentRoas) / avgRoas) * 100;
87
- if (deviation < THRESHOLDS.roasDropWarning)
88
- return null;
89
- return {
90
- type: 'roas_drop',
91
- severity: deviation >= THRESHOLDS.roasDropAlert ? 'alert' : 'warning',
92
- platform,
93
- metric: 'roas',
94
- currentValue: currentRoas,
95
- expectedValue: avgRoas,
96
- deviationPercent: -Math.round(deviation),
97
- message: `ROAS drop on ${platform}: ${currentRoas.toFixed(1)}x vs ${avgRoas.toFixed(1)}x average (-${Math.round(deviation)}%)`,
98
- timestamp: new Date().toISOString(),
99
- };
100
- }
101
- /** Run all anomaly checks for the current period */
102
- export function runAnomalyDetection(metrics) {
103
- const anomalies = [];
104
- for (const s of metrics.spendByPlatform) {
105
- const a = detectSpendSpike(s.currentHour, s.avgHourly, s.platform);
106
- if (a)
107
- anomalies.push(a);
108
- }
109
- const td = detectTrafficDrop(metrics.traffic.currentDay, metrics.traffic.avgDaily);
110
- if (td)
111
- anomalies.push(td);
112
- const cc = detectConversionChange(metrics.conversion.currentRate, metrics.conversion.avgRate);
113
- if (cc)
114
- anomalies.push(cc);
115
- for (const r of metrics.roasByPlatform) {
116
- const a = detectRoasDrop(r.current, r.avg, r.platform);
117
- if (a)
118
- anomalies.push(a);
119
- }
120
- return anomalies;
121
- }
122
- export { THRESHOLDS };
@@ -1,23 +0,0 @@
1
- /**
2
- * PRD asset scanner — identifies image/visual requirements from PRD prose.
3
- * Used by Celebrimbor's /imagine command to find what needs generating.
4
- * Pure text analysis — no API calls, no side effects.
5
- */
6
- export interface AssetRequirement {
7
- description: string;
8
- category: string;
9
- context: string;
10
- width: number;
11
- height: number;
12
- section: string;
13
- }
14
- /**
15
- * Scan a PRD document for visual asset requirements.
16
- * Returns a list of assets that need generating.
17
- */
18
- export declare function scanPrdForAssets(prdContent: string): AssetRequirement[];
19
- /**
20
- * Extract brand/style keywords from the PRD for style prefix generation.
21
- * Looks for Section 14 (Brand) or any section mentioning "brand", "style", "aesthetic".
22
- */
23
- export declare function extractBrandStyle(prdContent: string): string[];
@@ -1,107 +0,0 @@
1
- /**
2
- * PRD asset scanner — identifies image/visual requirements from PRD prose.
3
- * Used by Celebrimbor's /imagine command to find what needs generating.
4
- * Pure text analysis — no API calls, no side effects.
5
- */
6
- /** Patterns that indicate a visual asset requirement in PRD prose. */
7
- const ASSET_PATTERNS = [
8
- { pattern: /illustrat(?:ion|ed|e)/i, category: 'illustration' },
9
- { pattern: /portrait/i, category: 'portrait' },
10
- { pattern: /silhouette/i, category: 'portrait' },
11
- { pattern: /avatar/i, category: 'portrait' },
12
- { pattern: /(?:custom\s+)?\bicon\b/i, category: 'icon' },
13
- { pattern: /og[:\s-]image/i, category: 'og-image' },
14
- { pattern: /social\s+(?:sharing\s+)?image/i, category: 'og-image' },
15
- { pattern: /hero\s+(?:image|banner|art)/i, category: 'hero' },
16
- { pattern: /splash\s+(?:page|screen)/i, category: 'hero' },
17
- { pattern: /background\s+image/i, category: 'background' },
18
- { pattern: /cover\s+image/i, category: 'background' },
19
- { pattern: /\blogo\b/i, category: 'logo' },
20
- { pattern: /\bfavicon\b/i, category: 'icon' },
21
- { pattern: /comic\s+strip/i, category: 'illustration' },
22
- { pattern: /comic\s+panel/i, category: 'illustration' },
23
- { pattern: /screenshot/i, category: 'screenshot' },
24
- { pattern: /mockup/i, category: 'screenshot' },
25
- ];
26
- /** Default dimensions per asset category. */
27
- const CATEGORY_DIMENSIONS = {
28
- 'portrait': { width: 1024, height: 1024 },
29
- 'illustration': { width: 1024, height: 1024 },
30
- 'og-image': { width: 1200, height: 630 },
31
- 'hero': { width: 1792, height: 1024 },
32
- 'background': { width: 1792, height: 1024 },
33
- 'logo': { width: 512, height: 512 },
34
- 'icon': { width: 512, height: 512 },
35
- 'screenshot': { width: 1280, height: 720 },
36
- };
37
- /**
38
- * Scan a PRD document for visual asset requirements.
39
- * Returns a list of assets that need generating.
40
- */
41
- export function scanPrdForAssets(prdContent) {
42
- const assets = [];
43
- const lines = prdContent.split('\n');
44
- let currentSection = '';
45
- for (let i = 0; i < lines.length; i++) {
46
- const line = lines[i];
47
- // Track section headers
48
- const headerMatch = line.match(/^#{1,4}\s+(.+)/);
49
- if (headerMatch) {
50
- currentSection = headerMatch[1].trim();
51
- continue;
52
- }
53
- // Check each line against asset patterns
54
- for (const { pattern, category } of ASSET_PATTERNS) {
55
- if (pattern.test(line)) {
56
- // Extract surrounding context (current line + next line for description)
57
- const contextLines = lines.slice(Math.max(0, i - 1), Math.min(lines.length, i + 3));
58
- const context = contextLines.join(' ').trim();
59
- const dims = CATEGORY_DIMENSIONS[category] || { width: 1024, height: 1024 };
60
- assets.push({
61
- description: line.trim(),
62
- category,
63
- context,
64
- width: dims.width,
65
- height: dims.height,
66
- section: currentSection,
67
- });
68
- break; // One match per line is enough
69
- }
70
- }
71
- }
72
- // Deduplicate by description similarity
73
- const seen = new Set();
74
- return assets.filter(a => {
75
- const key = a.description.toLowerCase().slice(0, 60);
76
- if (seen.has(key))
77
- return false;
78
- seen.add(key);
79
- return true;
80
- });
81
- }
82
- /**
83
- * Extract brand/style keywords from the PRD for style prefix generation.
84
- * Looks for Section 14 (Brand) or any section mentioning "brand", "style", "aesthetic".
85
- */
86
- export function extractBrandStyle(prdContent) {
87
- const keywords = [];
88
- const lines = prdContent.split('\n');
89
- let inBrandSection = false;
90
- for (const line of lines) {
91
- const headerMatch = line.match(/^#{1,4}\s+(.+)/);
92
- if (headerMatch) {
93
- const title = headerMatch[1].toLowerCase();
94
- inBrandSection = title.includes('brand') || title.includes('style') || title.includes('aesthetic') || title.includes('design') || title.includes('personality');
95
- continue;
96
- }
97
- if (inBrandSection && line.trim()) {
98
- // Extract adjectives and style keywords
99
- const styleWords = line.match(/\b(minimal|bold|playful|professional|elegant|modern|retro|vintage|comic|pulp|neon|dark|light|cinematic|warm|cool|vibrant|muted|halftone|watercolor|photorealistic|illustration|flat|gradient|geometric|organic)\b/gi);
100
- if (styleWords) {
101
- keywords.push(...styleWords.map(w => w.toLowerCase()));
102
- }
103
- }
104
- }
105
- // Deduplicate
106
- return [...new Set(keywords)];
107
- }
@@ -1,39 +0,0 @@
1
- /**
2
- * Build analytics — tracks metrics across projects for trend analysis.
3
- * Stored at ~/.voidforge/analytics.json. No external dependencies.
4
- *
5
- * Wong guards the knowledge. The Sanctum grows.
6
- */
7
- export interface PhaseMetric {
8
- phase: string;
9
- findingsCount: number;
10
- fixesApplied: number;
11
- /** Duration in seconds (optional — only if measurable) */
12
- durationSeconds?: number;
13
- }
14
- export interface BuildRecord {
15
- projectName: string;
16
- framework: string;
17
- database: string;
18
- deployTarget: string;
19
- timestamp: string;
20
- version: string;
21
- phases: PhaseMetric[];
22
- totalFindings: number;
23
- totalFixes: number;
24
- testCount?: number;
25
- lessonsExtracted: number;
26
- }
27
- export interface AnalyticsStore {
28
- builds: BuildRecord[];
29
- }
30
- /** Record a completed build. */
31
- export declare function recordBuild(record: BuildRecord): Promise<void>;
32
- /** Surface trends across past builds. Returns human-readable insights. */
33
- export declare function surfaceTrends(currentFramework?: string): Promise<string[]>;
34
- /** Get a summary of all recorded builds. */
35
- export declare function getBuildHistory(): Promise<{
36
- count: number;
37
- frameworks: string[];
38
- latestBuild: string | null;
39
- }>;
@@ -1,91 +0,0 @@
1
- /**
2
- * Build analytics — tracks metrics across projects for trend analysis.
3
- * Stored at ~/.voidforge/analytics.json. No external dependencies.
4
- *
5
- * Wong guards the knowledge. The Sanctum grows.
6
- */
7
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
8
- import { join } from 'node:path';
9
- import { homedir } from 'node:os';
10
- const ANALYTICS_DIR = join(homedir(), '.voidforge');
11
- const ANALYTICS_FILE = join(ANALYTICS_DIR, 'analytics.json');
12
- async function ensureDir() {
13
- try {
14
- await mkdir(ANALYTICS_DIR, { recursive: true });
15
- }
16
- catch { /* exists */ }
17
- }
18
- async function loadStore() {
19
- try {
20
- const raw = await readFile(ANALYTICS_FILE, 'utf-8');
21
- return JSON.parse(raw);
22
- }
23
- catch {
24
- return { builds: [] };
25
- }
26
- }
27
- async function saveStore(store) {
28
- await ensureDir();
29
- await writeFile(ANALYTICS_FILE, JSON.stringify(store, null, 2), 'utf-8');
30
- }
31
- /** Record a completed build. */
32
- export async function recordBuild(record) {
33
- const store = await loadStore();
34
- store.builds.push(record);
35
- // Keep last 100 builds to prevent unbounded growth
36
- if (store.builds.length > 100) {
37
- store.builds = store.builds.slice(-100);
38
- }
39
- await saveStore(store);
40
- }
41
- /** Surface trends across past builds. Returns human-readable insights. */
42
- export async function surfaceTrends(currentFramework) {
43
- const store = await loadStore();
44
- const builds = store.builds;
45
- if (builds.length < 2)
46
- return [];
47
- const insights = [];
48
- // Finding hotspots — which phases consistently produce the most findings?
49
- const phaseFindings = {};
50
- for (const build of builds) {
51
- for (const phase of build.phases) {
52
- if (!phaseFindings[phase.phase])
53
- phaseFindings[phase.phase] = [];
54
- phaseFindings[phase.phase].push(phase.findingsCount);
55
- }
56
- }
57
- for (const [phase, counts] of Object.entries(phaseFindings)) {
58
- const avg = counts.reduce((a, b) => a + b, 0) / counts.length;
59
- if (avg > 5 && counts.length >= 2) {
60
- insights.push(`Phase "${phase}" averages ${avg.toFixed(1)} findings across ${counts.length} builds — consider proactive checks in earlier phases.`);
61
- }
62
- }
63
- // Framework-specific patterns
64
- if (currentFramework) {
65
- const frameworkBuilds = builds.filter(b => b.framework === currentFramework);
66
- if (frameworkBuilds.length >= 2) {
67
- const avgFindings = frameworkBuilds.reduce((a, b) => a + b.totalFindings, 0) / frameworkBuilds.length;
68
- insights.push(`Your ${currentFramework} projects average ${avgFindings.toFixed(0)} findings per build (${frameworkBuilds.length} builds).`);
69
- }
70
- }
71
- // Fix-to-finding ratio trend
72
- const ratios = builds.map(b => b.totalFindings > 0 ? b.totalFixes / b.totalFindings : 1);
73
- const recentRatios = ratios.slice(-5);
74
- const avgRatio = recentRatios.reduce((a, b) => a + b, 0) / recentRatios.length;
75
- if (avgRatio < 0.8) {
76
- insights.push(`Fix-to-finding ratio is ${(avgRatio * 100).toFixed(0)}% — some findings are being deferred. Consider addressing all findings in each build.`);
77
- }
78
- // Lessons trend
79
- const totalLessons = builds.reduce((a, b) => a + b.lessonsExtracted, 0);
80
- if (totalLessons > 0) {
81
- insights.push(`${totalLessons} lessons extracted across ${builds.length} builds. The forge is learning.`);
82
- }
83
- return insights;
84
- }
85
- /** Get a summary of all recorded builds. */
86
- export async function getBuildHistory() {
87
- const store = await loadStore();
88
- const frameworks = [...new Set(store.builds.map(b => b.framework))];
89
- const latest = store.builds.length > 0 ? store.builds[store.builds.length - 1].timestamp : null;
90
- return { count: store.builds.length, frameworks, latestBuild: latest };
91
- }
@@ -1,16 +0,0 @@
1
- /**
2
- * Database ERD generation from Prisma schema (ADR-025).
3
- * Parses prisma/schema.prisma and produces a Mermaid entity-relationship diagram.
4
- * Conditional — only runs if prisma/schema.prisma exists.
5
- */
6
- import type { ProvisionEmitter } from '../provisioners/types.js';
7
- export interface ERDResult {
8
- success: boolean;
9
- file: string;
10
- modelCount: number;
11
- error?: string;
12
- }
13
- /**
14
- * Generate a Mermaid ERD from the Prisma schema.
15
- */
16
- export declare function generateERD(projectDir: string, emit: ProvisionEmitter): Promise<ERDResult>;
@@ -1,98 +0,0 @@
1
- /**
2
- * Database ERD generation from Prisma schema (ADR-025).
3
- * Parses prisma/schema.prisma and produces a Mermaid entity-relationship diagram.
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
- /**
10
- * Minimal Prisma schema parser — extracts model names and fields.
11
- * Not a full parser — handles the common cases for ERD generation.
12
- */
13
- function parsePrismaSchema(content) {
14
- const models = [];
15
- const modelRegex = /model\s+(\w+)\s*\{([^}]+)\}/g;
16
- let match;
17
- while ((match = modelRegex.exec(content)) !== null) {
18
- const name = match[1];
19
- const body = match[2];
20
- const fields = [];
21
- for (const line of body.split('\n')) {
22
- const trimmed = line.trim();
23
- if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@'))
24
- continue;
25
- const fieldMatch = trimmed.match(/^(\w+)\s+(\w+)(\[\])?\s*(\?)?\s*/);
26
- if (fieldMatch) {
27
- const fieldName = fieldMatch[1];
28
- const fieldType = fieldMatch[2];
29
- const isArray = !!fieldMatch[3];
30
- const isOptional = !!fieldMatch[4];
31
- // Skip Prisma directives like @id, @default, etc. — those are on the same line
32
- const builtinTypes = ['String', 'Int', 'Float', 'Boolean', 'DateTime', 'Json', 'BigInt', 'Decimal', 'Bytes'];
33
- const isRelation = !builtinTypes.includes(fieldType);
34
- fields.push({ name: fieldName, type: fieldType, isRelation, isOptional, isArray });
35
- }
36
- }
37
- models.push({ name, fields });
38
- }
39
- return models;
40
- }
41
- function generateMermaidERD(models) {
42
- const lines = [
43
- '```mermaid',
44
- 'erDiagram',
45
- ];
46
- // Generate entity definitions
47
- for (const model of models) {
48
- lines.push(` ${model.name} {`);
49
- for (const field of model.fields) {
50
- if (!field.isRelation) {
51
- const optional = field.isOptional ? '?' : '';
52
- lines.push(` ${field.type}${optional} ${field.name}`);
53
- }
54
- }
55
- lines.push(' }');
56
- }
57
- // Generate relationships
58
- for (const model of models) {
59
- for (const field of model.fields) {
60
- if (field.isRelation) {
61
- const cardinality = field.isArray ? '}o--||' : '}o--o|';
62
- lines.push(` ${model.name} ${cardinality} ${field.type} : "${field.name}"`);
63
- }
64
- }
65
- }
66
- lines.push('```');
67
- return lines.join('\n');
68
- }
69
- /**
70
- * Generate a Mermaid ERD from the Prisma schema.
71
- */
72
- export async function generateERD(projectDir, emit) {
73
- const schemaPath = join(projectDir, 'prisma', 'schema.prisma');
74
- if (!existsSync(schemaPath)) {
75
- emit({ step: 'erd', status: 'skipped', message: 'No prisma/schema.prisma found — ERD generation skipped' });
76
- return { success: true, file: '', modelCount: 0 };
77
- }
78
- emit({ step: 'erd', status: 'started', message: 'Generating database ERD from Prisma schema' });
79
- try {
80
- const schema = await readFile(schemaPath, 'utf-8');
81
- const models = parsePrismaSchema(schema);
82
- if (models.length === 0) {
83
- emit({ step: 'erd', status: 'skipped', message: 'No models found in Prisma schema' });
84
- return { success: true, file: '', modelCount: 0 };
85
- }
86
- const mermaid = generateMermaidERD(models);
87
- const content = `# Database Schema\n\nAuto-generated from \`prisma/schema.prisma\` by VoidForge (ADR-025).\n\n${mermaid}\n`;
88
- const docsDir = join(projectDir, 'docs');
89
- await mkdir(docsDir, { recursive: true });
90
- await writeFile(join(docsDir, 'schema.md'), content, 'utf-8');
91
- emit({ step: 'erd', status: 'done', message: `Generated docs/schema.md — ${models.length} models mapped` });
92
- return { success: true, file: 'docs/schema.md', modelCount: models.length };
93
- }
94
- catch (err) {
95
- emit({ step: 'erd', status: 'error', message: 'Failed to generate ERD', detail: err.message });
96
- return { success: false, file: '', modelCount: 0, error: err.message };
97
- }
98
- }
@@ -1,15 +0,0 @@
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 type { ProvisionEmitter } from '../provisioners/types.js';
7
- export interface OpenAPIResult {
8
- success: boolean;
9
- file: string;
10
- error?: string;
11
- }
12
- /**
13
- * Generate an OpenAPI spec file for the project.
14
- */
15
- export declare function generateOpenAPIDoc(projectDir: string, projectName: string, framework: string, emit: ProvisionEmitter): Promise<OpenAPIResult>;