voidforge-build 23.9.2 → 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/kusanagi-devops.md +8 -0
- package/dist/.claude/agents/leia-secrets.md +10 -0
- package/dist/.claude/agents/loki-chaos.md +1 -0
- package/dist/.claude/agents/picard-architecture.md +11 -0
- package/dist/.claude/agents/silver-surfer-herald.md +17 -0
- package/dist/.claude/agents/sisko-campaign.md +3 -0
- package/dist/.claude/agents/thufir-protocol-parsing.md +10 -0
- package/dist/.claude/commands/architect.md +56 -0
- package/dist/.claude/commands/campaign.md +26 -1
- package/dist/.claude/commands/deploy.md +31 -0
- package/dist/.claude/commands/gauntlet.md +11 -0
- package/dist/.claude/commands/git.md +13 -3
- package/dist/.claude/commands/prd.md +8 -0
- package/dist/CHANGELOG.md +107 -0
- package/dist/CLAUDE.md +13 -4
- package/dist/VERSION.md +3 -1
- package/dist/docs/methods/AI_INTELLIGENCE.md +15 -0
- package/dist/docs/methods/BACKEND_ENGINEER.md +48 -0
- package/dist/docs/methods/BUILD_PROTOCOL.md +19 -0
- package/dist/docs/methods/CAMPAIGN.md +204 -1
- package/dist/docs/methods/DEVOPS_ENGINEER.md +80 -0
- package/dist/docs/methods/FORGE_KEEPER.md +80 -3
- package/dist/docs/methods/GAUNTLET.md +2 -0
- package/dist/docs/methods/PRD_GENERATOR.md +15 -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/SPEC_HANDOFF.md +53 -0
- package/dist/docs/methods/SUB_AGENTS.md +90 -0
- package/dist/docs/methods/SYSTEMS_ARCHITECT.md +55 -2
- package/dist/docs/methods/TESTING.md +17 -0
- package/dist/docs/methods/TIME_VAULT.md +17 -0
- package/dist/docs/methods/TROUBLESHOOTING.md +27 -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/deploy-preflight.ts +195 -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,96 @@
|
|
|
1
|
+
# Pattern: Large-Refactor Extraction (8-commit per-entity)
|
|
2
|
+
|
|
3
|
+
**When to use:** A 1,000+ LOC router/service/handler file needs splitting and the project has an existing exit gate (e.g., max-LOC-per-file). Single-commit refactors of files this size cause review fatigue, hide bugs in the diff noise, and are nearly impossible to revert surgically.
|
|
4
|
+
|
|
5
|
+
**Source:** Field report #320 §1. M-10 (Union Station): `routers/crm.py` 1,861 → 597 LOC across 8 commits. 0 regressions. Test count grew 2,099 → 2,246 (+147). Exit gate met with 78 LOC of headroom. The 5th commit's IDOR matrix surfaced a route-shadow bug that had made `PATCH /people/batch-update` unreachable in production for an unknown duration.
|
|
6
|
+
|
|
7
|
+
This is the cleanest large-refactor template I've documented. Use it.
|
|
8
|
+
|
|
9
|
+
## Architecture-Quick (Picard)
|
|
10
|
+
|
|
11
|
+
Before any commit, write a 1-2 page architecture doc to `logs/reviews/<topic>-architecture.md`:
|
|
12
|
+
|
|
13
|
+
```markdown
|
|
14
|
+
# Refactor: <topic> — extraction plan
|
|
15
|
+
|
|
16
|
+
## Current state
|
|
17
|
+
- Source file: <path> at <LOC>
|
|
18
|
+
- Exit gate: <LOC limit>
|
|
19
|
+
- LOC delta needed: <gate - current>
|
|
20
|
+
|
|
21
|
+
## Entity inventory
|
|
22
|
+
| Entity | Endpoints | Estimated LOC delta |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| people | 7 | -167 |
|
|
25
|
+
| companies | 10 | -345 |
|
|
26
|
+
| ... | ... | ... |
|
|
27
|
+
| | **Total** | **−1264** |
|
|
28
|
+
|
|
29
|
+
## Commit plan (one per entity + scaffold + cleanup)
|
|
30
|
+
| # | Commit | Adds | Removes | Cumulative LOC |
|
|
31
|
+
|---|---|---|---|---|
|
|
32
|
+
| 1 | scaffold (service base, error types, shared helpers) | services/_base.py | — | 1861 |
|
|
33
|
+
| 2 | extract people | services/people_service.py | router code | 1694 |
|
|
34
|
+
| ... | ... | ... | ... | ... |
|
|
35
|
+
| 8 | cleanup (lift duplicated helpers, prune imports, lint) | — | router cleanup | 597 |
|
|
36
|
+
|
|
37
|
+
## Function-signature contract (per service)
|
|
38
|
+
- org_id: int (first), user_id: str (second)
|
|
39
|
+
- Returns plain dict (no FastAPI Response wrappers)
|
|
40
|
+
- Raises ApiError (no HTTPException — service knows nothing of HTTP)
|
|
41
|
+
- No FastAPI imports in service modules
|
|
42
|
+
|
|
43
|
+
## Roles
|
|
44
|
+
- Strange — lead, owns sequencing
|
|
45
|
+
- Stark — router-side rewrites (thin wrappers calling service)
|
|
46
|
+
- Batgirl — IDOR matrix tests per entity
|
|
47
|
+
- Coulson — version + commit per step
|
|
48
|
+
|
|
49
|
+
## IDOR contract
|
|
50
|
+
- Pattern A (primary): every service method takes org_id as first param,
|
|
51
|
+
every query is scoped, every test in matrix asserts cross-org denial
|
|
52
|
+
- Pattern B (fallback): if a method legitimately spans tenants, document
|
|
53
|
+
the policy and test cross-tenant authorization explicitly
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Per-Commit Shape
|
|
57
|
+
|
|
58
|
+
Each entity commit follows the same shape. Keep them mechanically uniform:
|
|
59
|
+
|
|
60
|
+
1. **Extract** to `services/<entity>_service.py` — pure business logic, no FastAPI imports
|
|
61
|
+
2. **Rewrite** the router file as thin wrappers: validate → call service → format response
|
|
62
|
+
3. **Add** IDOR matrix tests for parametric paths AND fixed-suffix paths under same entity prefix (see `/docs/methods/SECURITY_AUDITOR.md` IDOR Matrix section)
|
|
63
|
+
4. **Verify** LOC trajectory: `git diff --stat HEAD~1 -- routers/<file>.py` shows monotonic decrease; service module count grows by 1
|
|
64
|
+
5. **Run targeted pytest** on touched files only (`pytest tests/services/test_<entity>_service.py tests/routers/test_<entity>.py`) — full suite is the orchestrator's gate, not the agent's
|
|
65
|
+
6. **Commit** with a "Deviations from Contract" section in the build report (see SUB_AGENTS.md)
|
|
66
|
+
|
|
67
|
+
## Final Cleanup Commit
|
|
68
|
+
|
|
69
|
+
Commit 8 (or N for an N-entity refactor) is non-obvious and load-bearing:
|
|
70
|
+
|
|
71
|
+
- Lift duplicated helpers that emerged across entities into a shared module
|
|
72
|
+
- Prune unused imports in the router file (extraction leaves behind imports the wrappers no longer need)
|
|
73
|
+
- Add lint scaffold if missing (LOC limit, signature-contract assertion)
|
|
74
|
+
- Verify no test files were dropped (mock paths often need updating to follow the extracted code)
|
|
75
|
+
- Confirm exit gate met with documented headroom: `wc -l routers/<file>.py`
|
|
76
|
+
|
|
77
|
+
## What This Pattern Caught
|
|
78
|
+
|
|
79
|
+
The IDOR matrix test in commit 5 (M-10 batch.py) surfaced that `/people/{person_id}` was shadowing `/people/batch-update`. FastAPI dispatches first-matching-route; a parametric path declared first eats subsequent fixed-suffix paths. The fix is path-converter type hints (`{person_id:int}`), restricting the parametric route to integer paths.
|
|
80
|
+
|
|
81
|
+
This bug had been latent in production. No unit test exercised it. No previous Gauntlet caught it. Without the IDOR matrix discipline this pattern bakes in, it would still be unreachable. (Field report #320 §1.)
|
|
82
|
+
|
|
83
|
+
## Anti-Patterns
|
|
84
|
+
|
|
85
|
+
- **Single-commit refactor** for files >1,000 LOC. Review fatigue + impossible to revert surgically.
|
|
86
|
+
- **No architecture-quick.** Without Picard's plan, the LOC trajectory drifts and entities get extracted in dependency-violating order.
|
|
87
|
+
- **No IDOR matrix.** Refactoring multi-tenant code without cross-tenant denial tests is just rearranging the leak surface.
|
|
88
|
+
- **Mixing entity extractions in one commit.** Each commit must remain shippable independently with green tests. One commit per entity, no exceptions.
|
|
89
|
+
- **Skipping the final cleanup commit.** Duplicated helpers that emerged across entities don't lift themselves; pruning matters.
|
|
90
|
+
- **Running full pytest as the agent's last step.** See SUB_AGENTS.md "Build-Agent Pytest Sequencing" — agent response window truncates mid-suite, orchestrator has to reconstruct.
|
|
91
|
+
|
|
92
|
+
## When NOT to Use
|
|
93
|
+
|
|
94
|
+
- File is under ~600 LOC. Just split it in one commit; the overhead isn't worth it.
|
|
95
|
+
- The file is genuinely cohesive (state machine, single algorithm, generated code). Extraction would fragment what should stay together.
|
|
96
|
+
- The exit gate isn't binding — if there's no LOC limit and no clear quality reason to split, the refactor is yak-shaving.
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
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 };
|
|
@@ -0,0 +1,122 @@
|
|
|
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 };
|
|
@@ -0,0 +1,23 @@
|
|
|
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[];
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
}>;
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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>;
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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>;
|