voidforge-build 23.9.1 → 23.10.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/kusanagi-devops.md +8 -0
- package/dist/.claude/agents/leia-secrets.md +10 -0
- package/dist/.claude/agents/picard-architecture.md +8 -0
- package/dist/.claude/agents/silver-surfer-herald.md +14 -0
- package/dist/.claude/agents/thufir-protocol-parsing.md +10 -0
- package/dist/.claude/commands/architect.md +18 -0
- package/dist/.claude/commands/campaign.md +24 -1
- package/dist/.claude/commands/deploy.md +31 -0
- package/dist/.claude/commands/prd.md +8 -0
- package/dist/CHANGELOG.md +64 -0
- package/dist/VERSION.md +3 -1
- package/dist/docs/methods/BUILD_PROTOCOL.md +19 -0
- package/dist/docs/methods/CAMPAIGN.md +8 -0
- package/dist/docs/methods/DEVOPS_ENGINEER.md +64 -0
- package/dist/docs/methods/FORGE_KEEPER.md +62 -3
- package/dist/docs/methods/PRD_GENERATOR.md +15 -0
- package/dist/docs/methods/SPEC_HANDOFF.md +53 -0
- package/dist/docs/methods/SYSTEMS_ARCHITECT.md +13 -0
- package/dist/docs/methods/TROUBLESHOOTING.md +27 -0
- package/dist/docs/patterns/deploy-preflight.ts +195 -0
- package/dist/scripts/voidforge.js +0 -0
- package/package.json +1 -1
- package/dist/wizard/lib/anomaly-detection.d.ts +0 -59
- package/dist/wizard/lib/anomaly-detection.js +0 -122
- package/dist/wizard/lib/asset-scanner.d.ts +0 -23
- package/dist/wizard/lib/asset-scanner.js +0 -107
- package/dist/wizard/lib/build-analytics.d.ts +0 -39
- package/dist/wizard/lib/build-analytics.js +0 -91
- package/dist/wizard/lib/codegen/erd-gen.d.ts +0 -16
- package/dist/wizard/lib/codegen/erd-gen.js +0 -98
- package/dist/wizard/lib/codegen/openapi-gen.d.ts +0 -15
- package/dist/wizard/lib/codegen/openapi-gen.js +0 -79
- package/dist/wizard/lib/codegen/prisma-types.d.ts +0 -15
- package/dist/wizard/lib/codegen/prisma-types.js +0 -44
- package/dist/wizard/lib/codegen/seed-gen.d.ts +0 -16
- package/dist/wizard/lib/codegen/seed-gen.js +0 -128
- package/dist/wizard/lib/correlation-engine.d.ts +0 -59
- package/dist/wizard/lib/correlation-engine.js +0 -152
- package/dist/wizard/lib/desktop-notify.d.ts +0 -27
- package/dist/wizard/lib/desktop-notify.js +0 -98
- package/dist/wizard/lib/image-gen.d.ts +0 -56
- package/dist/wizard/lib/image-gen.js +0 -159
- package/dist/wizard/lib/natural-language-deploy.d.ts +0 -30
- package/dist/wizard/lib/natural-language-deploy.js +0 -186
- package/dist/wizard/lib/route-optimizer.d.ts +0 -28
- package/dist/wizard/lib/route-optimizer.js +0 -93
- package/dist/wizard/lib/service-install.d.ts +0 -18
- package/dist/wizard/lib/service-install.js +0 -182
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# SPEC_HANDOFF — Cross-Session Implementation Hand-off
|
|
2
|
+
## System Protocol · Introduced by: field reports #307, #308
|
|
3
|
+
|
|
4
|
+
## When to Use
|
|
5
|
+
|
|
6
|
+
When a session would benefit from offloading mechanical implementation work to a different session (separate context window, separate repo, separate worktree) while preserving the orchestrator's context for synthesis and review.
|
|
7
|
+
|
|
8
|
+
Typical triggers:
|
|
9
|
+
- Multi-repo campaign (e.g., scaffold methodology update + marketing-site content update)
|
|
10
|
+
- Large-but-mechanical work (26-finding spec executed without back-and-forth — v23.9.x demonstrated this)
|
|
11
|
+
- Executor needs fresh context; orchestrator needs to stay on high-level synthesis
|
|
12
|
+
|
|
13
|
+
## The Pattern
|
|
14
|
+
|
|
15
|
+
### Session A (orchestrator) produces the spec
|
|
16
|
+
|
|
17
|
+
Spec doc lives at `docs/SITE_UPDATE_SPEC.md` or similar in the TARGET repo (so the executing session finds it by path).
|
|
18
|
+
|
|
19
|
+
Required structure:
|
|
20
|
+
1. **Title** with date and source.
|
|
21
|
+
2. **Numbered findings** — each finding has ID, severity (Critical / High / Should / Nice-to-have), file:line citation, proposed change, and a `verified-against-commit: <SHA>` field. The verified-against-commit field lets Session B fast-skip any finding whose state hasn't changed since the spec was authored.
|
|
22
|
+
3. **Phases** — group findings by logical phase (data fixes, new pages, content rewrite, test updates, typecheck/build). One commit per phase.
|
|
23
|
+
4. **Nav-order requirements for new pages** — if the spec proposes creating new pages in a linear tutorial/flow, explicitly state `prev=<page>, next=<page>` for each new page. Without this, the executor guesses and often chooses the wrong direction (field report #308 RC-7).
|
|
24
|
+
5. **Success criteria** — typecheck green, tests green, build green, optional per-phase smoke checks.
|
|
25
|
+
|
|
26
|
+
### Session B (executor) receives the hand-off prompt
|
|
27
|
+
|
|
28
|
+
Copy-pasteable prompt template:
|
|
29
|
+
```
|
|
30
|
+
Read docs/SITE_UPDATE_SPEC.md in this repo. Execute phases in order.
|
|
31
|
+
Commit per phase with CHANGELOG entry. Run typecheck + test + build between phases.
|
|
32
|
+
If you hit a blocker, stop and save state — do not improvise.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Session B validates before acting
|
|
36
|
+
|
|
37
|
+
For each finding: `git show <verified-against-commit>:<path>` and compare to local HEAD. If they match, the claimed state is current — execute as planned. If they differ, the state has moved; re-evaluate before applying.
|
|
38
|
+
|
|
39
|
+
## Evidence
|
|
40
|
+
|
|
41
|
+
- Field report #308: 23/26 items executed across 5 phases. 3 Must-Fix items slipped through (all related to spec gaps around nav direction and table captions). Net positive — saved ~20k tokens of orchestrator context.
|
|
42
|
+
- Field report #307 F4: CAMPAIGN.md convention for `verified-against-commit: <SHA>` stamping.
|
|
43
|
+
|
|
44
|
+
## Limitations
|
|
45
|
+
|
|
46
|
+
- Executor may optimize for literal compliance over holistic UX (nav direction example above).
|
|
47
|
+
- Spec must include nav order, table captions, and a11y requirements for new components explicitly.
|
|
48
|
+
- Orchestrator MUST run a review pass (`/engage`) on the executor's output before considering the hand-off complete.
|
|
49
|
+
|
|
50
|
+
## Handoffs
|
|
51
|
+
|
|
52
|
+
- After executor completes, orchestrator runs `/engage` then `/assemble --fast` on the affected files to surface integration issues.
|
|
53
|
+
- If the executor skipped findings whose `verified-against-commit` matched local HEAD, note which in the completion report — helps validate the SHA-skip heuristic.
|
|
@@ -104,6 +104,19 @@ Use the Agent tool to run these in parallel — they are independent analysis ta
|
|
|
104
104
|
- **ToS/API policy compatibility:** For ADRs selecting third-party services, verify the provider's Terms of Service and API usage policies permit the intended usage pattern (automation, bot-initiated transactions, reselling, volume). A service rejected on ToS grounds after building requires a full architecture pivot. (Field report #300)
|
|
105
105
|
- **Riker reviews:** "Number One, does this hold up?" Riker challenges each ADR's trade-offs — are the alternatives truly worse? Are the consequences acceptable? Did we consider the second-order effects? **Riker also verifies the implementation scope is honest** — if an ADR says "fully implemented" but the code throws `'Implement...'`, that's a finding. Riker's review prevents architectural decisions made in a vacuum.
|
|
106
106
|
|
|
107
|
+
### Npm-name availability pre-flight (ADR authoring)
|
|
108
|
+
|
|
109
|
+
When an ADR proposes a published npm package name or scope, the architect MUST verify availability via BOTH:
|
|
110
|
+
|
|
111
|
+
1. **Registry query** — `npm view <name>` returns E404 (or equivalent "not found" signal)
|
|
112
|
+
2. **Org-create form** — if scoped (e.g., `@foo/bar`), visit npmjs.com/org/create and attempt to create the org. npm has no CLI-level `npm org create`; scope availability in the registry does NOT imply org-create availability.
|
|
113
|
+
|
|
114
|
+
Do not canonicalize the name in docs, code, or CHANGELOG entries until BOTH checks pass. Checklist item in the ADR's Decision section:
|
|
115
|
+
|
|
116
|
+
> "Npm-name availability confirmed: registry E404 ✓, scope create-form accepts ✓."
|
|
117
|
+
|
|
118
|
+
Field report evidence: #308 RC-1 documents v23.9.0 → v23.9.1 mid-flight pivot from `@voidforge/cli` to unscoped `voidforge-build` because `voidforge` org creation was rejected after docs had already canonicalized the scoped name. Related: LRN-4, LRN-7 in docs/LEARNINGS.md; ADR-061 §13.
|
|
119
|
+
|
|
107
120
|
### `--adr-only` Lightweight Mode
|
|
108
121
|
|
|
109
122
|
When architecture work is deferred (e.g., designing auth that won't be built for months), skip the full parallel analysis (Steps 1-4) and go straight to Step 5:
|
|
@@ -263,3 +263,30 @@ Before clearing, deleting, or modifying database fields to "fix" missing files o
|
|
|
263
263
|
5. **NEVER clear a DB field to work around a missing file.** Restore the file first, or confirm the regeneration cost is acceptable BEFORE deleting the reference.
|
|
264
264
|
|
|
265
265
|
(Field report #103: 251 avatarUrl fields cleared to "fix" missing files, triggering ~$10 in DALL-E regeneration + 50 minutes downtime. The files existed on the VPS — they were deleted by `rsync --delete`, not lost. Restoring from backup would have been free.)
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Cloudflare / Wrangler Gotchas
|
|
270
|
+
|
|
271
|
+
### `wrangler pages deploy` in Direct Upload mode ignores `.gitignore`
|
|
272
|
+
|
|
273
|
+
`.gitignore` semantics differ between Git-integrated and Direct Upload Pages projects. Direct Upload uploads EVERY file in the target directory, including `.env`, `.pem`, `.key`, and anything else `.gitignore` would normally hide. Always deploy from a dedicated subdirectory (`dist/`, `public/`, `site/`) — never repo root. See `docs/methods/DEVOPS_ENGINEER.md` §Deploy Surface Boundary.
|
|
274
|
+
|
|
275
|
+
Evidence: field report #305 — 32-day live credential leak via `wrangler pages deploy .` from repo root.
|
|
276
|
+
|
|
277
|
+
### `wrangler pages deployment delete --force` doesn't force aliased deployments
|
|
278
|
+
|
|
279
|
+
For aliased deployments (preview URLs attached to branch names), the CLI's `--force` flag does NOT pass `force=true` to the underlying API. Result: error code 8000035 "deployment is aliased and cannot be deleted." Workaround: call the Cloudflare API directly with `force=true` in the request body.
|
|
280
|
+
|
|
281
|
+
### Cloudflare Pages Dev Mode + Purge Everything may not evict all cache
|
|
282
|
+
|
|
283
|
+
Dev Mode and Purge Everything are both best-effort across Cloudflare's PoP network. For time-critical evictions (e.g., post-credential-rotation):
|
|
284
|
+
|
|
285
|
+
1. Purge Everything in the dashboard.
|
|
286
|
+
2. Run Custom Purge by URL for the specific asset.
|
|
287
|
+
3. Enable Dev Mode.
|
|
288
|
+
4. Wait at least TTL + 60 seconds before asserting eviction.
|
|
289
|
+
|
|
290
|
+
If the stale content persists after step 4, a second Custom Purge + TTL wait is often required. Do not assume a single purge is sufficient.
|
|
291
|
+
|
|
292
|
+
Evidence: field report #305 — credential-leak remediation required multiple purge passes before all PoPs served the rotated content.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy Preflight — Pre-deploy secret and sensitive-path scan
|
|
3
|
+
*
|
|
4
|
+
* Reference implementation for .claude/commands/deploy.md Step 2.5.
|
|
5
|
+
* Scans the deploy artifact directory BEFORE upload. Exits non-zero on any hit.
|
|
6
|
+
*
|
|
7
|
+
* Evidence: field reports #305 (32-day credential leak), #303 (methodology exposure).
|
|
8
|
+
*
|
|
9
|
+
* Key principles:
|
|
10
|
+
* - Scan the deploy payload directory, NOT the repo root.
|
|
11
|
+
* - Never auto-filter — a hit means the operator must investigate.
|
|
12
|
+
* - Never print secret content; only paths + pattern IDs.
|
|
13
|
+
* - Allowlist escape hatch via DEPLOY_PREFLIGHT_ALLOW (comma-separated globs).
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* npx tsx docs/patterns/deploy-preflight.ts ./dist
|
|
17
|
+
* DEPLOY_PREFLIGHT_ALLOW='fixtures/*,public/ok.env.example' npx tsx docs/patterns/deploy-preflight.ts ./dist
|
|
18
|
+
*
|
|
19
|
+
* CI step example (before wrangler/vercel/firebase):
|
|
20
|
+
* - run: npx tsx docs/patterns/deploy-preflight.ts ./dist
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
24
|
+
import { extname, join, relative, sep } from 'node:path';
|
|
25
|
+
import { argv, env, exit } from 'node:process';
|
|
26
|
+
|
|
27
|
+
// ---------- forbidden filename patterns ----------
|
|
28
|
+
const FORBIDDEN_NAME_PATTERNS: { id: string; test: (name: string, rel: string) => boolean }[] = [
|
|
29
|
+
{ id: 'env-file', test: (n) => /^\.env(\..+)?$/.test(n) && !/\.(example|template|sample)$/.test(n) },
|
|
30
|
+
{ id: 'pem-file', test: (n) => n.endsWith('.pem') },
|
|
31
|
+
{ id: 'key-file', test: (n) => n.endsWith('.key') },
|
|
32
|
+
{ id: 'ssh-private-key', test: (n) => /^id_(rsa|ed25519|ecdsa|dsa)(\..+)?$/.test(n) && !n.endsWith('.pub') },
|
|
33
|
+
{ id: 'pkcs12', test: (n) => n.endsWith('.p12') || n.endsWith('.pfx') },
|
|
34
|
+
{ id: 'methodology-claude', test: (_, rel) => rel.split(sep)[0] === '.claude' },
|
|
35
|
+
{ id: 'methodology-docs-methods', test: (_, rel) => rel.startsWith(`docs${sep}methods${sep}`) },
|
|
36
|
+
{ id: 'methodology-docs-patterns', test: (_, rel) => rel.startsWith(`docs${sep}patterns${sep}`) },
|
|
37
|
+
{ id: 'methodology-holocron', test: (n) => n === 'HOLOCRON.md' },
|
|
38
|
+
{ id: 'methodology-changelog', test: (n) => n === 'CHANGELOG.md' },
|
|
39
|
+
{ id: 'methodology-version', test: (n) => n === 'VERSION.md' },
|
|
40
|
+
{ id: 'build-logs', test: (_, rel) => rel.split(sep)[0] === 'logs' },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// ---------- forbidden content patterns (scanned in text-ish files only) ----------
|
|
44
|
+
const FORBIDDEN_CONTENT_PATTERNS: { id: string; re: RegExp }[] = [
|
|
45
|
+
{ id: 'aws-access-key', re: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
46
|
+
{ id: 'cloudflare-token', re: /\b[0-9a-f]{40}\b/ },
|
|
47
|
+
{ id: 'github-pat', re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/ },
|
|
48
|
+
{ id: 'private-key-block', re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const TEXT_EXTENSIONS = new Set([
|
|
52
|
+
'.html', '.htm', '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx',
|
|
53
|
+
'.json', '.map', '.txt', '.md', '.xml', '.yml', '.yaml', '.env',
|
|
54
|
+
'.css', '.svg',
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
interface Hit {
|
|
58
|
+
kind: 'name' | 'content';
|
|
59
|
+
path: string;
|
|
60
|
+
patternId: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function globToRegex(glob: string): RegExp {
|
|
64
|
+
const escaped = glob
|
|
65
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
66
|
+
.replace(/\*/g, '.*')
|
|
67
|
+
.replace(/\?/g, '.');
|
|
68
|
+
return new RegExp(`^${escaped}$`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function loadAllowlist(): RegExp[] {
|
|
72
|
+
const raw = env.DEPLOY_PREFLIGHT_ALLOW ?? '';
|
|
73
|
+
return raw
|
|
74
|
+
.split(',')
|
|
75
|
+
.map((s) => s.trim())
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.map(globToRegex);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isAllowed(relPath: string, allowlist: RegExp[]): boolean {
|
|
81
|
+
return allowlist.some((re) => re.test(relPath));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function* walk(root: string, current = root): Generator<string> {
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
88
|
+
} catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
for (const e of entries) {
|
|
92
|
+
const full = join(current, e.name);
|
|
93
|
+
if (e.isSymbolicLink()) continue;
|
|
94
|
+
if (e.isDirectory()) {
|
|
95
|
+
yield* walk(root, full);
|
|
96
|
+
} else if (e.isFile()) {
|
|
97
|
+
yield full;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function scanName(fullPath: string, relPath: string): string | null {
|
|
103
|
+
const base = relPath.split(sep).pop() ?? '';
|
|
104
|
+
for (const p of FORBIDDEN_NAME_PATTERNS) {
|
|
105
|
+
if (p.test(base, relPath)) return p.id;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function scanContent(fullPath: string): string | null {
|
|
111
|
+
const ext = extname(fullPath).toLowerCase();
|
|
112
|
+
if (!TEXT_EXTENSIONS.has(ext)) return null;
|
|
113
|
+
let stats;
|
|
114
|
+
try {
|
|
115
|
+
stats = statSync(fullPath);
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
// skip files >2MB to keep the scan fast; secrets are typically short
|
|
120
|
+
if (stats.size > 2_000_000) return null;
|
|
121
|
+
let buf: string;
|
|
122
|
+
try {
|
|
123
|
+
buf = readFileSync(fullPath, 'utf8');
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
for (const p of FORBIDDEN_CONTENT_PATTERNS) {
|
|
128
|
+
if (p.re.test(buf)) return p.id;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function main(): void {
|
|
134
|
+
const target = argv[2];
|
|
135
|
+
if (!target) {
|
|
136
|
+
console.error('[deploy-preflight] Usage: deploy-preflight <deploy-dir>');
|
|
137
|
+
exit(2);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let rootStat;
|
|
141
|
+
try {
|
|
142
|
+
rootStat = statSync(target);
|
|
143
|
+
} catch {
|
|
144
|
+
console.error(`[deploy-preflight] target does not exist: ${target}`);
|
|
145
|
+
exit(2);
|
|
146
|
+
}
|
|
147
|
+
if (!rootStat.isDirectory()) {
|
|
148
|
+
console.error(`[deploy-preflight] target is not a directory: ${target}`);
|
|
149
|
+
exit(2);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const allowlist = loadAllowlist();
|
|
153
|
+
const hits: Hit[] = [];
|
|
154
|
+
let scanned = 0;
|
|
155
|
+
|
|
156
|
+
for (const fullPath of walk(target)) {
|
|
157
|
+
const relPath = relative(target, fullPath);
|
|
158
|
+
if (isAllowed(relPath, allowlist)) continue;
|
|
159
|
+
scanned += 1;
|
|
160
|
+
|
|
161
|
+
const nameHit = scanName(fullPath, relPath);
|
|
162
|
+
if (nameHit) {
|
|
163
|
+
hits.push({ kind: 'name', path: relPath, patternId: nameHit });
|
|
164
|
+
continue; // skip content scan on already-forbidden names
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const contentHit = scanContent(fullPath);
|
|
168
|
+
if (contentHit) {
|
|
169
|
+
hits.push({ kind: 'content', path: relPath, patternId: contentHit });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const summary = {
|
|
174
|
+
action: 'deploy-preflight',
|
|
175
|
+
target,
|
|
176
|
+
scanned,
|
|
177
|
+
hits: hits.length,
|
|
178
|
+
allowlist: allowlist.length,
|
|
179
|
+
};
|
|
180
|
+
console.log(JSON.stringify(summary));
|
|
181
|
+
|
|
182
|
+
if (hits.length > 0) {
|
|
183
|
+
console.error(`[deploy-preflight] ${hits.length} forbidden path(s) in deploy payload:`);
|
|
184
|
+
for (const h of hits) {
|
|
185
|
+
console.error(` - [${h.kind}:${h.patternId}] ${h.path}`);
|
|
186
|
+
}
|
|
187
|
+
console.error('[deploy-preflight] ABORTED. Remove offending files or fix deploy surface configuration.');
|
|
188
|
+
exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log('[deploy-preflight] clean');
|
|
192
|
+
exit(0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
main();
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -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
|
-
}>;
|