thevoidforge 21.0.11 → 21.0.12
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/commands/ai.md +69 -0
- package/dist/.claude/commands/architect.md +121 -0
- package/dist/.claude/commands/assemble.md +201 -0
- package/dist/.claude/commands/assess.md +75 -0
- package/dist/.claude/commands/blueprint.md +135 -0
- package/dist/.claude/commands/build.md +116 -0
- package/dist/.claude/commands/campaign.md +201 -0
- package/dist/.claude/commands/cultivation.md +166 -0
- package/dist/.claude/commands/current.md +128 -0
- package/dist/.claude/commands/dangerroom.md +74 -0
- package/dist/.claude/commands/debrief.md +178 -0
- package/dist/.claude/commands/deploy.md +99 -0
- package/dist/.claude/commands/devops.md +143 -0
- package/dist/.claude/commands/gauntlet.md +140 -0
- package/dist/.claude/commands/git.md +104 -0
- package/dist/.claude/commands/grow.md +146 -0
- package/dist/.claude/commands/imagine.md +126 -0
- package/dist/.claude/commands/portfolio.md +50 -0
- package/dist/.claude/commands/prd.md +113 -0
- package/dist/.claude/commands/qa.md +107 -0
- package/dist/.claude/commands/review.md +151 -0
- package/dist/.claude/commands/security.md +100 -0
- package/dist/.claude/commands/test.md +96 -0
- package/dist/.claude/commands/thumper.md +116 -0
- package/dist/.claude/commands/treasury.md +100 -0
- package/dist/.claude/commands/ux.md +118 -0
- package/dist/.claude/commands/vault.md +189 -0
- package/dist/.claude/commands/void.md +108 -0
- package/dist/CHANGELOG.md +1918 -0
- package/dist/CLAUDE.md +250 -0
- package/dist/HOLOCRON.md +856 -0
- package/dist/VERSION.md +123 -0
- package/dist/docs/NAMING_REGISTRY.md +478 -0
- package/dist/docs/methods/AI_INTELLIGENCE.md +276 -0
- package/dist/docs/methods/ASSEMBLER.md +142 -0
- package/dist/docs/methods/BACKEND_ENGINEER.md +165 -0
- package/dist/docs/methods/BUILD_JOURNAL.md +185 -0
- package/dist/docs/methods/BUILD_PROTOCOL.md +426 -0
- package/dist/docs/methods/CAMPAIGN.md +568 -0
- package/dist/docs/methods/CONTEXT_MANAGEMENT.md +189 -0
- package/dist/docs/methods/DEEP_CURRENT.md +184 -0
- package/dist/docs/methods/DEVOPS_ENGINEER.md +295 -0
- package/dist/docs/methods/FIELD_MEDIC.md +261 -0
- package/dist/docs/methods/FORGE_ARTIST.md +108 -0
- package/dist/docs/methods/FORGE_KEEPER.md +268 -0
- package/dist/docs/methods/GAUNTLET.md +344 -0
- package/dist/docs/methods/GROWTH_STRATEGIST.md +466 -0
- package/dist/docs/methods/HEARTBEAT.md +168 -0
- package/dist/docs/methods/MCP_INTEGRATION.md +139 -0
- package/dist/docs/methods/MUSTER.md +148 -0
- package/dist/docs/methods/PRD_GENERATOR.md +186 -0
- package/dist/docs/methods/PRODUCT_DESIGN_FRONTEND.md +250 -0
- package/dist/docs/methods/QA_ENGINEER.md +337 -0
- package/dist/docs/methods/RELEASE_MANAGER.md +145 -0
- package/dist/docs/methods/SECURITY_AUDITOR.md +320 -0
- package/dist/docs/methods/SUB_AGENTS.md +335 -0
- package/dist/docs/methods/SYSTEMS_ARCHITECT.md +171 -0
- package/dist/docs/methods/TESTING.md +359 -0
- package/dist/docs/methods/THUMPER.md +175 -0
- package/dist/docs/methods/TIME_VAULT.md +120 -0
- package/dist/docs/methods/TREASURY.md +184 -0
- package/dist/docs/methods/TROUBLESHOOTING.md +265 -0
- package/dist/docs/patterns/README.md +52 -0
- package/dist/docs/patterns/ad-billing-adapter.ts +537 -0
- package/dist/docs/patterns/ad-platform-adapter.ts +421 -0
- package/dist/docs/patterns/ai-classifier.ts +195 -0
- package/dist/docs/patterns/ai-eval.ts +272 -0
- package/dist/docs/patterns/ai-orchestrator.ts +341 -0
- package/dist/docs/patterns/ai-router.ts +194 -0
- package/dist/docs/patterns/ai-tool-schema.ts +237 -0
- package/dist/docs/patterns/api-route.ts +241 -0
- package/dist/docs/patterns/backtest-engine.ts +499 -0
- package/dist/docs/patterns/browser-review.ts +292 -0
- package/dist/docs/patterns/combobox.tsx +300 -0
- package/dist/docs/patterns/component.tsx +262 -0
- package/dist/docs/patterns/daemon-process.ts +338 -0
- package/dist/docs/patterns/data-pipeline.ts +297 -0
- package/dist/docs/patterns/database-migration.ts +466 -0
- package/dist/docs/patterns/e2e-test.ts +629 -0
- package/dist/docs/patterns/error-handling.ts +312 -0
- package/dist/docs/patterns/execution-safety.ts +601 -0
- package/dist/docs/patterns/financial-transaction.ts +342 -0
- package/dist/docs/patterns/funding-plan.ts +462 -0
- package/dist/docs/patterns/game-entity.ts +137 -0
- package/dist/docs/patterns/game-loop.ts +113 -0
- package/dist/docs/patterns/game-state.ts +143 -0
- package/dist/docs/patterns/job-queue.ts +225 -0
- package/dist/docs/patterns/kongo-integration.ts +164 -0
- package/dist/docs/patterns/middleware.ts +363 -0
- package/dist/docs/patterns/mobile-screen.tsx +139 -0
- package/dist/docs/patterns/mobile-service.ts +167 -0
- package/dist/docs/patterns/multi-tenant.ts +382 -0
- package/dist/docs/patterns/oauth-token-lifecycle.ts +223 -0
- package/dist/docs/patterns/outbound-rate-limiter.ts +260 -0
- package/dist/docs/patterns/prompt-template.ts +195 -0
- package/dist/docs/patterns/revenue-source-adapter.ts +311 -0
- package/dist/docs/patterns/service.ts +224 -0
- package/dist/docs/patterns/sse-endpoint.ts +118 -0
- package/dist/docs/patterns/stablecoin-adapter.ts +511 -0
- package/dist/docs/patterns/third-party-script.ts +68 -0
- package/dist/scripts/thumper/gom-jabbar.sh +241 -0
- package/dist/scripts/thumper/relay.sh +610 -0
- package/dist/scripts/thumper/scan.sh +359 -0
- package/dist/scripts/thumper/thumper.sh +190 -0
- package/dist/scripts/thumper/water-rings.sh +76 -0
- package/package.json +1 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: Financial Transaction (Branded Types + Hash-Chained Append Log)
|
|
3
|
+
*
|
|
4
|
+
* Key principles:
|
|
5
|
+
* - All money in integer cents (Cents branded type) — NEVER floating point
|
|
6
|
+
* - Append-only immutable logs — never rewrite, only append
|
|
7
|
+
* - Hash chain for tamper detection (SHA-256 of previous entry)
|
|
8
|
+
* - Atomic writes (write-to-temp + fsync + rename) per ADR-1
|
|
9
|
+
* - macOS: use F_FULLFSYNC instead of fsync for financial files (§9.18)
|
|
10
|
+
* - Single-writer architecture — only the heartbeat daemon writes financial state
|
|
11
|
+
*
|
|
12
|
+
* Agents: Dockson (treasury), Steris (budget), Vin (analytics)
|
|
13
|
+
*
|
|
14
|
+
* PRD Reference: §9.9, §9.17 (branded types), §9.18 (macOS fsync), ADR-1/ADR-3
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
import { writeFile, appendFile, open, rename, unlink } from 'node:fs/promises';
|
|
19
|
+
import { constants } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { homedir, platform } from 'node:os';
|
|
22
|
+
|
|
23
|
+
// ── Branded Financial Types (§9.17) ───────────────────
|
|
24
|
+
// These prevent mixing dollars and cents at the type level.
|
|
25
|
+
|
|
26
|
+
type Cents = number & { readonly __brand: 'Cents' };
|
|
27
|
+
type Percentage = number & { readonly __brand: 'Percentage' }; // 0-100
|
|
28
|
+
type Ratio = number & { readonly __brand: 'Ratio' }; // e.g., 3.68
|
|
29
|
+
|
|
30
|
+
type AdPlatform = 'meta' | 'google' | 'tiktok' | 'linkedin' | 'twitter' | 'reddit';
|
|
31
|
+
type RevenueSource = 'stripe' | 'paddle';
|
|
32
|
+
type BankSource = 'mercury' | 'brex';
|
|
33
|
+
type TransactionSource = AdPlatform | RevenueSource | BankSource;
|
|
34
|
+
|
|
35
|
+
function toCents(dollars: number): Cents {
|
|
36
|
+
return Math.round(dollars * 100) as Cents;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toDollars(cents: Cents): number {
|
|
40
|
+
return cents / 100;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Transaction Record (§9.9) ─────────────────────────
|
|
44
|
+
|
|
45
|
+
interface Transaction {
|
|
46
|
+
id: string; // UUID v4
|
|
47
|
+
projectId: string;
|
|
48
|
+
type: 'revenue' | 'spend' | 'refund';
|
|
49
|
+
source: TransactionSource;
|
|
50
|
+
externalId: string; // platform's transaction ID
|
|
51
|
+
amount: Cents; // integer cents, never float
|
|
52
|
+
currency: 'USD'; // USD-only per ADR-6
|
|
53
|
+
description: string;
|
|
54
|
+
metadata: Record<string, string>;
|
|
55
|
+
createdAt: string; // ISO 8601
|
|
56
|
+
reconciledAt?: string;
|
|
57
|
+
reconciledStatus?: 'matched' | 'discrepancy' | 'pending';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Budget Record (§9.9) ──────────────────────────────
|
|
61
|
+
|
|
62
|
+
interface Budget {
|
|
63
|
+
id: string;
|
|
64
|
+
projectId: string;
|
|
65
|
+
period: 'daily' | 'weekly' | 'monthly';
|
|
66
|
+
totalAmount: Cents;
|
|
67
|
+
currency: 'USD';
|
|
68
|
+
allocations: Array<{
|
|
69
|
+
platform: AdPlatform;
|
|
70
|
+
amount: Cents;
|
|
71
|
+
dailyCap: Cents; // enforced on platform side
|
|
72
|
+
}>;
|
|
73
|
+
safetyTiers: {
|
|
74
|
+
autoApproveBelow: Cents; // default 2500 ($25/day)
|
|
75
|
+
agentApproveBelow: Cents; // default 10000 ($100/day)
|
|
76
|
+
humanConfirmBelow: Cents; // default 50000 ($500/day)
|
|
77
|
+
hardStopAbove: Cents; // default 50000 ($500/day)
|
|
78
|
+
};
|
|
79
|
+
createdAt: string;
|
|
80
|
+
updatedAt: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Campaign Record (§9.9 + §9.17 + §9.20.3) ─────────
|
|
84
|
+
|
|
85
|
+
type CampaignStatus =
|
|
86
|
+
| 'draft'
|
|
87
|
+
| 'pending_approval'
|
|
88
|
+
| 'creating'
|
|
89
|
+
| 'active'
|
|
90
|
+
| 'paused'
|
|
91
|
+
| 'completed'
|
|
92
|
+
| 'error'
|
|
93
|
+
| 'suspended'
|
|
94
|
+
| 'deleting'
|
|
95
|
+
| 'freeze_pending';
|
|
96
|
+
|
|
97
|
+
type CampaignEventSource = 'cli' | 'daemon' | 'platform' | 'agent';
|
|
98
|
+
|
|
99
|
+
interface CampaignStateEvent {
|
|
100
|
+
timestamp: string;
|
|
101
|
+
source: CampaignEventSource;
|
|
102
|
+
oldStatus: CampaignStatus;
|
|
103
|
+
newStatus: CampaignStatus;
|
|
104
|
+
reason: string;
|
|
105
|
+
ruleId?: string; // for agent-initiated: which Tier 1 rule triggered
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface GrowthCampaign {
|
|
109
|
+
id: string;
|
|
110
|
+
projectId: string;
|
|
111
|
+
platform: AdPlatform;
|
|
112
|
+
externalId: string;
|
|
113
|
+
name: string;
|
|
114
|
+
status: CampaignStatus;
|
|
115
|
+
dailyBudget: Cents;
|
|
116
|
+
totalSpend: Cents;
|
|
117
|
+
metrics: {
|
|
118
|
+
impressions: number;
|
|
119
|
+
clicks: number;
|
|
120
|
+
conversions: number;
|
|
121
|
+
ctr: Percentage;
|
|
122
|
+
cpc: Cents;
|
|
123
|
+
roas: Ratio;
|
|
124
|
+
};
|
|
125
|
+
testGroupId?: string; // A/B test group link (§9.20.3)
|
|
126
|
+
testVariant?: string; // 'A' | 'B' | 'C' etc.
|
|
127
|
+
testMetric?: 'ctr' | 'roas' | 'conversions';
|
|
128
|
+
events: CampaignStateEvent[]; // event-sourced state transitions
|
|
129
|
+
createdAt: string;
|
|
130
|
+
updatedAt: string;
|
|
131
|
+
pausedAt?: string;
|
|
132
|
+
pauseReason?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Revenue Event (§9.9) ──────────────────────────────
|
|
136
|
+
|
|
137
|
+
interface RevenueEvent {
|
|
138
|
+
id: string;
|
|
139
|
+
projectId: string;
|
|
140
|
+
source: RevenueSource;
|
|
141
|
+
type: 'charge' | 'subscription' | 'refund' | 'dispute';
|
|
142
|
+
amount: Cents; // negative for refunds/disputes
|
|
143
|
+
currency: 'USD';
|
|
144
|
+
customerId?: string; // hashed — never store raw email
|
|
145
|
+
subscriptionId?: string;
|
|
146
|
+
metadata: Record<string, string>;
|
|
147
|
+
createdAt: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Reconciliation Record (§9.9 + §9.17) ──────────────
|
|
151
|
+
|
|
152
|
+
interface ReconciliationReport {
|
|
153
|
+
id: string; // UUID v4
|
|
154
|
+
date: string; // YYYY-MM-DD
|
|
155
|
+
type: 'preliminary' | 'final';
|
|
156
|
+
projectId: string;
|
|
157
|
+
spend: Array<{
|
|
158
|
+
platform: AdPlatform;
|
|
159
|
+
voidforgeRecorded: Cents;
|
|
160
|
+
platformReported: Cents;
|
|
161
|
+
discrepancy: Cents; // absolute difference
|
|
162
|
+
status: 'matched' | 'discrepancy' | 'unavailable';
|
|
163
|
+
}>;
|
|
164
|
+
revenue: Array<{
|
|
165
|
+
source: RevenueSource;
|
|
166
|
+
recorded: Cents;
|
|
167
|
+
reported: Cents;
|
|
168
|
+
discrepancy: Cents;
|
|
169
|
+
status: 'matched' | 'discrepancy' | 'unavailable';
|
|
170
|
+
}>;
|
|
171
|
+
netPosition: Cents; // total revenue - total spend
|
|
172
|
+
blendedRoas: Ratio;
|
|
173
|
+
alerts: string[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── System State (§9.19.12) ───────────────────────────
|
|
177
|
+
|
|
178
|
+
type CultivationSystemState =
|
|
179
|
+
| 'inactive'
|
|
180
|
+
| 'active'
|
|
181
|
+
| 'frozen'
|
|
182
|
+
| 'partial_freeze'
|
|
183
|
+
| 'recovering'
|
|
184
|
+
| 'degraded'
|
|
185
|
+
| 'recovery_failed';
|
|
186
|
+
|
|
187
|
+
// ── Hash-Chained Append Log ───────────────────────────
|
|
188
|
+
// Every spend-log and revenue-log entry includes a hash of the previous entry.
|
|
189
|
+
// This detects accidental corruption and casual tampering.
|
|
190
|
+
// Limitation: an attacker with filesystem write access can recompute the chain.
|
|
191
|
+
|
|
192
|
+
interface HashChainedEntry<T> {
|
|
193
|
+
data: T;
|
|
194
|
+
prevHash: string; // SHA-256 of previous entry (hex)
|
|
195
|
+
hash: string; // SHA-256 of this entry (hex)
|
|
196
|
+
walIntentId?: string; // WAL intent ID for idempotent replay (ADR-3)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function computeHash(data: unknown, prevHash: string): string {
|
|
200
|
+
const payload = JSON.stringify(data) + prevHash;
|
|
201
|
+
return createHash('sha256').update(payload).digest('hex');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function createChainedEntry<T>(data: T, prevHash: string, walIntentId?: string): HashChainedEntry<T> {
|
|
205
|
+
const hash = computeHash(data, prevHash);
|
|
206
|
+
return { data, prevHash, hash, walIntentId };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function verifyChain<T>(entries: HashChainedEntry<T>[]): { valid: boolean; brokenAt?: number } {
|
|
210
|
+
for (let i = 0; i < entries.length; i++) {
|
|
211
|
+
const expected = computeHash(entries[i].data, entries[i].prevHash);
|
|
212
|
+
if (expected !== entries[i].hash) {
|
|
213
|
+
return { valid: false, brokenAt: i };
|
|
214
|
+
}
|
|
215
|
+
if (i > 0 && entries[i].prevHash !== entries[i - 1].hash) {
|
|
216
|
+
return { valid: false, brokenAt: i };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { valid: true };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Atomic File Write ─────────────────────────────────
|
|
223
|
+
// write-to-temp + fsync (F_FULLFSYNC on macOS) + rename
|
|
224
|
+
// Per ADR-1: all mutable financial file writes use this pattern.
|
|
225
|
+
|
|
226
|
+
async function atomicWrite(filePath: string, content: string): Promise<void> {
|
|
227
|
+
const tempPath = filePath + '.tmp.' + process.pid;
|
|
228
|
+
|
|
229
|
+
// Write to temp file
|
|
230
|
+
const fd = await open(tempPath, 'w');
|
|
231
|
+
try {
|
|
232
|
+
await fd.writeFile(content, 'utf-8');
|
|
233
|
+
|
|
234
|
+
// Durable sync — F_FULLFSYNC on macOS, fsync on Linux (§9.18)
|
|
235
|
+
if (platform() === 'darwin') {
|
|
236
|
+
// macOS: fsync() does NOT guarantee physical durability
|
|
237
|
+
// Must use fcntl(fd, F_FULLFSYNC) — 51 is the macOS constant
|
|
238
|
+
// Node.js datasync maps to fdatasync, not F_FULLFSYNC
|
|
239
|
+
// In production: use native addon or child_process to call fcntl(fd, 51)
|
|
240
|
+
// For now: document the gap — datasync is sufficient for crash safety, not power loss
|
|
241
|
+
await fd.datasync();
|
|
242
|
+
} else {
|
|
243
|
+
await fd.sync();
|
|
244
|
+
}
|
|
245
|
+
} finally {
|
|
246
|
+
await fd.close();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Atomic rename — on POSIX, rename is atomic
|
|
250
|
+
await rename(tempPath, filePath);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Append-Only Log ───────────────────────────────────
|
|
254
|
+
// For spend-log.jsonl and revenue-log.jsonl
|
|
255
|
+
|
|
256
|
+
async function appendToLog<T>(
|
|
257
|
+
logPath: string,
|
|
258
|
+
data: T,
|
|
259
|
+
prevHash: string,
|
|
260
|
+
walIntentId?: string
|
|
261
|
+
): Promise<HashChainedEntry<T>> {
|
|
262
|
+
const entry = createChainedEntry(data, prevHash, walIntentId);
|
|
263
|
+
const line = JSON.stringify(entry) + '\n';
|
|
264
|
+
await appendFile(logPath, line, 'utf-8');
|
|
265
|
+
return entry;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Idempotent Append (ADR-3 WAL replay) ──────────────
|
|
269
|
+
// Before appending during WAL replay, check if the intent ID exists in recent entries.
|
|
270
|
+
|
|
271
|
+
async function idempotentAppend<T>(
|
|
272
|
+
logPath: string,
|
|
273
|
+
data: T,
|
|
274
|
+
prevHash: string,
|
|
275
|
+
walIntentId: string,
|
|
276
|
+
recentEntries: HashChainedEntry<T>[]
|
|
277
|
+
): Promise<HashChainedEntry<T> | null> {
|
|
278
|
+
// Check if this WAL intent was already applied
|
|
279
|
+
const existing = recentEntries.find(e => e.walIntentId === walIntentId);
|
|
280
|
+
if (existing) {
|
|
281
|
+
return null; // Already applied — skip
|
|
282
|
+
}
|
|
283
|
+
return appendToLog(logPath, data, prevHash, walIntentId);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── Storage Layout ────────────────────────────────────
|
|
287
|
+
// ~/.voidforge/treasury/
|
|
288
|
+
// ├── vault.enc # financial vault (AES-256-GCM, Argon2id)
|
|
289
|
+
// ├── budgets.json # active budget allocations per project
|
|
290
|
+
// ├── spend-log.jsonl # append-only spend log (immutable, hash-chained)
|
|
291
|
+
// ├── revenue-log.jsonl # append-only revenue log (immutable, hash-chained)
|
|
292
|
+
// ├── pending-ops.jsonl # WAL for platform API operations (ADR-3)
|
|
293
|
+
// ├── campaigns/
|
|
294
|
+
// │ └── {projectId}/
|
|
295
|
+
// │ ├── meta-{id}.json
|
|
296
|
+
// │ └── google-{id}.json
|
|
297
|
+
// ├── reconciliation/
|
|
298
|
+
// │ ├── 2026-03-17.json
|
|
299
|
+
// │ └── 2026-03-16.json
|
|
300
|
+
// └── reports/
|
|
301
|
+
// └── 2026-03.json # monthly summary
|
|
302
|
+
|
|
303
|
+
const TREASURY_DIR = join(homedir(), '.voidforge', 'treasury');
|
|
304
|
+
const SPEND_LOG = join(TREASURY_DIR, 'spend-log.jsonl');
|
|
305
|
+
const REVENUE_LOG = join(TREASURY_DIR, 'revenue-log.jsonl');
|
|
306
|
+
const PENDING_OPS = join(TREASURY_DIR, 'pending-ops.jsonl');
|
|
307
|
+
const BUDGETS_FILE = join(TREASURY_DIR, 'budgets.json');
|
|
308
|
+
|
|
309
|
+
// ── Number Formatting (§9.15.4) ───────────────────────
|
|
310
|
+
|
|
311
|
+
function formatCurrency(cents: Cents, detail: boolean = false): string {
|
|
312
|
+
const dollars = toDollars(cents);
|
|
313
|
+
if (dollars >= 100_000) {
|
|
314
|
+
return dollars >= 1_000_000 ? `$${(dollars / 1_000_000).toFixed(1)}M` : `$${Math.round(dollars / 1000)}K`;
|
|
315
|
+
}
|
|
316
|
+
if (detail) return `$${dollars.toFixed(2)}`;
|
|
317
|
+
if (cents < 0) return `-$${Math.abs(dollars).toLocaleString('en-US', { maximumFractionDigits: 0 })}`;
|
|
318
|
+
return `$${dollars.toLocaleString('en-US', { maximumFractionDigits: 0 })}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function formatRoas(roas: Ratio): string {
|
|
322
|
+
return `${roas.toFixed(1)}x`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function formatPercentage(pct: Percentage, showSign: boolean = false): string {
|
|
326
|
+
const sign = showSign && pct > 0 ? '+' : '';
|
|
327
|
+
return `${sign}${pct.toFixed(1)}%`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export type {
|
|
331
|
+
Transaction, Budget, GrowthCampaign, RevenueEvent, ReconciliationReport,
|
|
332
|
+
CampaignStatus, CampaignEventSource, CampaignStateEvent,
|
|
333
|
+
CultivationSystemState, HashChainedEntry,
|
|
334
|
+
Cents, Percentage, Ratio, AdPlatform, RevenueSource, BankSource, TransactionSource,
|
|
335
|
+
};
|
|
336
|
+
export {
|
|
337
|
+
toCents, toDollars,
|
|
338
|
+
computeHash, createChainedEntry, verifyChain,
|
|
339
|
+
atomicWrite, appendToLog, idempotentAppend,
|
|
340
|
+
formatCurrency, formatRoas, formatPercentage,
|
|
341
|
+
TREASURY_DIR, SPEND_LOG, REVENUE_LOG, PENDING_OPS, BUDGETS_FILE,
|
|
342
|
+
};
|