thevoidforge 21.0.10 → 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/dist/scripts/voidforge.js +1 -1
- package/package.json +1 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: Funding Plan (Core Data Structure + Pure Logic)
|
|
3
|
+
*
|
|
4
|
+
* Key principles:
|
|
5
|
+
* - The FundingPlan is the central data structure connecting:
|
|
6
|
+
* stablecoin source → off-ramp → bank settlement → platform billing
|
|
7
|
+
* - Plans are IMMUTABLE once approved — state transitions only, no field edits
|
|
8
|
+
* - Hash-chained for tamper-evident audit trail (per financial-transaction.ts)
|
|
9
|
+
* - All monetary values use branded integer cents (Cents type)
|
|
10
|
+
* - Pure logic functions: no I/O, no side effects, fully testable
|
|
11
|
+
* - Single-writer: only Heartbeat daemon creates and transitions FundingPlans
|
|
12
|
+
*
|
|
13
|
+
* Agents: Dockson (treasury), Heartbeat daemon
|
|
14
|
+
*
|
|
15
|
+
* PRD Reference: §12.1, §12.2, §12.4, §12.5, §12.6, §13, §15
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
|
|
20
|
+
// ── Branded Financial Types (from financial-transaction.ts) ──
|
|
21
|
+
|
|
22
|
+
type Cents = number & { readonly __brand: 'Cents' };
|
|
23
|
+
|
|
24
|
+
function toCents(dollars: number): Cents {
|
|
25
|
+
return Math.round(dollars * 100) as Cents;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toDollars(cents: Cents): number {
|
|
29
|
+
return cents / 100;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Stablecoin Funding Source (PRD §12.1) ────────────
|
|
33
|
+
|
|
34
|
+
type StablecoinProviderType = 'circle' | 'bridge' | 'manual';
|
|
35
|
+
|
|
36
|
+
interface StablecoinFundingSource {
|
|
37
|
+
id: string; // UUID v4
|
|
38
|
+
provider: StablecoinProviderType;
|
|
39
|
+
asset: string; // 'USDC' in V1
|
|
40
|
+
network: string; // 'ETH', 'SOL', 'MATIC', etc.
|
|
41
|
+
sourceAccountId: string; // provider-specific wallet/account ID
|
|
42
|
+
whitelistedDestinationBankId: string; // only this bank can receive off-ramps
|
|
43
|
+
status: 'active' | 'suspended' | 'unconfigured';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Operating Bank Account (PRD §12.2) ───────────────
|
|
47
|
+
|
|
48
|
+
type BankProvider = 'mercury' | 'external';
|
|
49
|
+
|
|
50
|
+
interface OperatingBankAccount {
|
|
51
|
+
id: string; // UUID v4
|
|
52
|
+
provider: BankProvider;
|
|
53
|
+
accountId: string; // bank-specific account identifier
|
|
54
|
+
currency: 'USD'; // USD-only in V1
|
|
55
|
+
availableBalanceCents: Cents;
|
|
56
|
+
reservedBalanceCents: Cents; // earmarked for pending settlements
|
|
57
|
+
minimumBufferCents: Cents; // floor below which off-ramp triggers
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Platform Billing Profile (PRD §12.3) ────────────
|
|
61
|
+
// Re-exported from ad-billing-adapter.ts for convenience.
|
|
62
|
+
|
|
63
|
+
type CapabilityState = 'FULLY_FUNDABLE' | 'MONITORED_ONLY' | 'UNSUPPORTED';
|
|
64
|
+
type BillingMode =
|
|
65
|
+
| 'monthly_invoicing'
|
|
66
|
+
| 'direct_debit'
|
|
67
|
+
| 'extended_credit'
|
|
68
|
+
| 'manual_bank_transfer'
|
|
69
|
+
| 'card_only'
|
|
70
|
+
| 'unknown';
|
|
71
|
+
type AdPlatform = 'google' | 'meta';
|
|
72
|
+
|
|
73
|
+
interface PlatformBillingProfile {
|
|
74
|
+
platform: AdPlatform;
|
|
75
|
+
capabilityState: CapabilityState;
|
|
76
|
+
billingMode: BillingMode;
|
|
77
|
+
externalAccountId: string;
|
|
78
|
+
billingSetupId?: string;
|
|
79
|
+
invoiceGroupId?: string;
|
|
80
|
+
paymentProfileId?: string;
|
|
81
|
+
fundingSourceId?: string;
|
|
82
|
+
currency: 'USD';
|
|
83
|
+
nextDueDate?: string; // ISO 8601
|
|
84
|
+
status: 'active' | 'degraded' | 'suspended' | 'unconfigured';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Funding Plan (PRD §12.4) ────────────────────────
|
|
88
|
+
|
|
89
|
+
type FundingPlanStatus =
|
|
90
|
+
| 'DRAFT' // created, not yet approved
|
|
91
|
+
| 'APPROVED' // approved by policy or human, awaiting execution
|
|
92
|
+
| 'PENDING_SETTLEMENT' // off-ramp initiated, waiting for bank settlement
|
|
93
|
+
| 'SETTLED' // fiat arrived at bank, plan complete
|
|
94
|
+
| 'FAILED' // off-ramp or settlement failed
|
|
95
|
+
| 'FROZEN'; // frozen by circuit breaker or manual freeze
|
|
96
|
+
|
|
97
|
+
type FundingPlanReason =
|
|
98
|
+
| 'LOW_BUFFER' // bank balance below minimum buffer
|
|
99
|
+
| 'INVOICE_DUE' // Google invoice approaching due date
|
|
100
|
+
| 'RUNWAY_SHORTFALL' // projected spend exceeds available fiat runway
|
|
101
|
+
| 'MANUAL_REQUEST'; // user-initiated funding request
|
|
102
|
+
|
|
103
|
+
type ApprovalMode =
|
|
104
|
+
| 'policy_auto' // rules engine approved automatically
|
|
105
|
+
| 'vault_manual' // user approved via vault password
|
|
106
|
+
| 'totp_required'; // user approved via vault + TOTP (high-risk actions)
|
|
107
|
+
|
|
108
|
+
type TargetPlatform = AdPlatform | 'shared_buffer';
|
|
109
|
+
|
|
110
|
+
interface FundingPlan {
|
|
111
|
+
id: string; // UUID v4
|
|
112
|
+
createdAt: string; // ISO 8601
|
|
113
|
+
updatedAt: string; // ISO 8601
|
|
114
|
+
reason: FundingPlanReason;
|
|
115
|
+
sourceFundingId: string; // → StablecoinFundingSource.id
|
|
116
|
+
destinationBankId: string; // → OperatingBankAccount.id
|
|
117
|
+
targetPlatform: TargetPlatform; // which platform this funding supports
|
|
118
|
+
requiredCents: Cents; // how much fiat is needed
|
|
119
|
+
reservedCents: Cents; // how much has been earmarked at the bank
|
|
120
|
+
status: FundingPlanStatus;
|
|
121
|
+
approvalMode: ApprovalMode;
|
|
122
|
+
approvedAt?: string; // ISO 8601
|
|
123
|
+
approvedBy?: string; // 'policy_engine' | 'user:{hashedId}'
|
|
124
|
+
settledAt?: string; // ISO 8601
|
|
125
|
+
failureReason?: string;
|
|
126
|
+
idempotencyKey: string; // UUID, prevents duplicate off-ramps
|
|
127
|
+
// Hash chain fields
|
|
128
|
+
previousHash: string;
|
|
129
|
+
hash: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Transfer Record (PRD §12.5) ─────────────────────
|
|
133
|
+
|
|
134
|
+
type TransferDirection = 'crypto_to_fiat' | 'bank_to_platform' | 'platform_debit';
|
|
135
|
+
type TransferStatus = 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled';
|
|
136
|
+
|
|
137
|
+
interface TransferRecord {
|
|
138
|
+
id: string; // UUID v4
|
|
139
|
+
fundingPlanId: string; // → FundingPlan.id
|
|
140
|
+
providerTransferId: string; // stablecoin provider's transfer ID
|
|
141
|
+
bankTransactionId?: string; // bank-side reference once detected
|
|
142
|
+
direction: TransferDirection;
|
|
143
|
+
amountCents: Cents;
|
|
144
|
+
feesCents: Cents;
|
|
145
|
+
netAmountCents: Cents; // amountCents - feesCents
|
|
146
|
+
currency: 'USD';
|
|
147
|
+
reference: string; // human-readable reference for matching
|
|
148
|
+
status: TransferStatus;
|
|
149
|
+
initiatedAt: string; // ISO 8601
|
|
150
|
+
completedAt?: string; // ISO 8601
|
|
151
|
+
// Hash chain fields (per financial-transaction.ts pattern)
|
|
152
|
+
previousHash: string;
|
|
153
|
+
hash: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Reconciliation Record (PRD §12.6) ───────────────
|
|
157
|
+
|
|
158
|
+
type ReconciliationResult = 'MATCHED' | 'WITHIN_THRESHOLD' | 'MISMATCH';
|
|
159
|
+
|
|
160
|
+
interface ReconciliationRecord {
|
|
161
|
+
id: string; // UUID v4
|
|
162
|
+
platform: AdPlatform;
|
|
163
|
+
date: string; // YYYY-MM-DD
|
|
164
|
+
spendCents: Cents; // platform-reported ad spend
|
|
165
|
+
bankSettledCents: Cents; // bank transactions matched to this platform
|
|
166
|
+
invoiceCents: Cents; // invoiced amount (Google) or debit amount (Meta)
|
|
167
|
+
varianceCents: Cents; // absolute difference: |invoiceCents - bankSettledCents|
|
|
168
|
+
result: ReconciliationResult;
|
|
169
|
+
notes: string;
|
|
170
|
+
createdAt: string; // ISO 8601
|
|
171
|
+
// Hash chain fields
|
|
172
|
+
previousHash: string;
|
|
173
|
+
hash: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Hash Chain Helpers ──────────────────────────────
|
|
177
|
+
|
|
178
|
+
function computePlanHash(plan: Omit<FundingPlan, 'hash'>, previousHash: string): string {
|
|
179
|
+
const payload = JSON.stringify({
|
|
180
|
+
id: plan.id,
|
|
181
|
+
reason: plan.reason,
|
|
182
|
+
sourceFundingId: plan.sourceFundingId,
|
|
183
|
+
destinationBankId: plan.destinationBankId,
|
|
184
|
+
requiredCents: plan.requiredCents,
|
|
185
|
+
status: plan.status,
|
|
186
|
+
createdAt: plan.createdAt,
|
|
187
|
+
idempotencyKey: plan.idempotencyKey,
|
|
188
|
+
}) + previousHash;
|
|
189
|
+
return createHash('sha256').update(payload).digest('hex');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function computeTransferHash(record: Omit<TransferRecord, 'hash'>, previousHash: string): string {
|
|
193
|
+
const payload = JSON.stringify({
|
|
194
|
+
id: record.id,
|
|
195
|
+
fundingPlanId: record.fundingPlanId,
|
|
196
|
+
providerTransferId: record.providerTransferId,
|
|
197
|
+
amountCents: record.amountCents,
|
|
198
|
+
feesCents: record.feesCents,
|
|
199
|
+
status: record.status,
|
|
200
|
+
initiatedAt: record.initiatedAt,
|
|
201
|
+
}) + previousHash;
|
|
202
|
+
return createHash('sha256').update(payload).digest('hex');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function computeReconciliationHash(
|
|
206
|
+
record: Omit<ReconciliationRecord, 'hash'>,
|
|
207
|
+
previousHash: string
|
|
208
|
+
): string {
|
|
209
|
+
const payload = JSON.stringify({
|
|
210
|
+
id: record.id,
|
|
211
|
+
platform: record.platform,
|
|
212
|
+
date: record.date,
|
|
213
|
+
spendCents: record.spendCents,
|
|
214
|
+
bankSettledCents: record.bankSettledCents,
|
|
215
|
+
invoiceCents: record.invoiceCents,
|
|
216
|
+
varianceCents: record.varianceCents,
|
|
217
|
+
result: record.result,
|
|
218
|
+
}) + previousHash;
|
|
219
|
+
return createHash('sha256').update(payload).digest('hex');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Pure Logic: Funding Plan Creation ───────────────
|
|
223
|
+
|
|
224
|
+
function createFundingPlan(
|
|
225
|
+
reason: FundingPlanReason,
|
|
226
|
+
sourceFundingId: string,
|
|
227
|
+
destinationBankId: string,
|
|
228
|
+
targetPlatform: TargetPlatform,
|
|
229
|
+
requiredCents: Cents,
|
|
230
|
+
previousHash: string
|
|
231
|
+
): FundingPlan {
|
|
232
|
+
const now = new Date().toISOString();
|
|
233
|
+
const id = crypto.randomUUID();
|
|
234
|
+
const idempotencyKey = crypto.randomUUID();
|
|
235
|
+
|
|
236
|
+
const draft: Omit<FundingPlan, 'hash'> = {
|
|
237
|
+
id,
|
|
238
|
+
createdAt: now,
|
|
239
|
+
updatedAt: now,
|
|
240
|
+
reason,
|
|
241
|
+
sourceFundingId,
|
|
242
|
+
destinationBankId,
|
|
243
|
+
targetPlatform,
|
|
244
|
+
requiredCents,
|
|
245
|
+
reservedCents: 0 as Cents,
|
|
246
|
+
status: 'DRAFT',
|
|
247
|
+
approvalMode: 'policy_auto', // default; upgraded by approval logic
|
|
248
|
+
idempotencyKey,
|
|
249
|
+
previousHash,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const hash = computePlanHash(draft, previousHash);
|
|
253
|
+
return { ...draft, hash };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── Pure Logic: Plan Approval ───────────────────────
|
|
257
|
+
|
|
258
|
+
function approvePlan(
|
|
259
|
+
plan: FundingPlan,
|
|
260
|
+
approvalMode: ApprovalMode,
|
|
261
|
+
approvedBy: string
|
|
262
|
+
): FundingPlan {
|
|
263
|
+
if (plan.status !== 'DRAFT') {
|
|
264
|
+
throw new Error(`Cannot approve plan in status ${plan.status} — must be DRAFT`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const now = new Date().toISOString();
|
|
268
|
+
const approved: Omit<FundingPlan, 'hash'> = {
|
|
269
|
+
...plan,
|
|
270
|
+
status: 'APPROVED',
|
|
271
|
+
approvalMode,
|
|
272
|
+
approvedBy,
|
|
273
|
+
approvedAt: now,
|
|
274
|
+
updatedAt: now,
|
|
275
|
+
previousHash: plan.hash,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const hash = computePlanHash(approved, plan.hash);
|
|
279
|
+
return { ...approved, hash };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── Pure Logic: Plan State Transitions ──────────────
|
|
283
|
+
|
|
284
|
+
function transitionPlan(
|
|
285
|
+
plan: FundingPlan,
|
|
286
|
+
newStatus: FundingPlanStatus,
|
|
287
|
+
reason?: string
|
|
288
|
+
): FundingPlan {
|
|
289
|
+
// Validate allowed transitions
|
|
290
|
+
const allowedTransitions: Record<FundingPlanStatus, FundingPlanStatus[]> = {
|
|
291
|
+
'DRAFT': ['APPROVED', 'FROZEN'],
|
|
292
|
+
'APPROVED': ['PENDING_SETTLEMENT', 'FROZEN', 'FAILED'],
|
|
293
|
+
'PENDING_SETTLEMENT': ['SETTLED', 'FAILED', 'FROZEN'],
|
|
294
|
+
'SETTLED': [], // terminal state
|
|
295
|
+
'FAILED': ['DRAFT'], // can retry by creating new draft
|
|
296
|
+
'FROZEN': ['DRAFT'], // can unfreeze to draft for re-approval
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const allowed = allowedTransitions[plan.status];
|
|
300
|
+
if (!allowed.includes(newStatus)) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`Invalid transition: ${plan.status} → ${newStatus}. Allowed: ${allowed.join(', ') || 'none (terminal)'}`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const now = new Date().toISOString();
|
|
307
|
+
const transitioned: Omit<FundingPlan, 'hash'> = {
|
|
308
|
+
...plan,
|
|
309
|
+
status: newStatus,
|
|
310
|
+
updatedAt: now,
|
|
311
|
+
settledAt: newStatus === 'SETTLED' ? now : plan.settledAt,
|
|
312
|
+
failureReason: newStatus === 'FAILED' ? reason : plan.failureReason,
|
|
313
|
+
previousHash: plan.hash,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const hash = computePlanHash(transitioned, plan.hash);
|
|
317
|
+
return { ...transitioned, hash };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── Pure Logic: Runway Calculation ──────────────────
|
|
321
|
+
|
|
322
|
+
function calculateRunway(bankBalanceCents: Cents, dailySpendRateCents: Cents): number {
|
|
323
|
+
if (dailySpendRateCents <= 0) return Infinity;
|
|
324
|
+
return Math.floor(bankBalanceCents / dailySpendRateCents);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── Pure Logic: Off-ramp Trigger Decision ───────────
|
|
328
|
+
|
|
329
|
+
function shouldTriggerOfframp(
|
|
330
|
+
bankBalanceCents: Cents,
|
|
331
|
+
bufferThresholdCents: Cents,
|
|
332
|
+
pendingSpendCents: Cents,
|
|
333
|
+
pendingOfframpCents: Cents
|
|
334
|
+
): boolean {
|
|
335
|
+
// Available balance after accounting for pending obligations
|
|
336
|
+
const effectiveBalance = (bankBalanceCents - pendingSpendCents + pendingOfframpCents) as Cents;
|
|
337
|
+
return effectiveBalance < bufferThresholdCents;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Pure Logic: Required Off-ramp Amount ────────────
|
|
341
|
+
|
|
342
|
+
function calculateRequiredOfframp(
|
|
343
|
+
bankBalanceCents: Cents,
|
|
344
|
+
bufferThresholdCents: Cents,
|
|
345
|
+
pendingSpendCents: Cents,
|
|
346
|
+
pendingOfframpCents: Cents,
|
|
347
|
+
minimumOfframpCents: Cents
|
|
348
|
+
): Cents {
|
|
349
|
+
const deficit = (bufferThresholdCents + pendingSpendCents - bankBalanceCents - pendingOfframpCents);
|
|
350
|
+
if (deficit <= 0) return 0 as Cents;
|
|
351
|
+
// Round up to minimum off-ramp amount if below provider minimum
|
|
352
|
+
return Math.max(deficit, minimumOfframpCents) as Cents;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ── Pure Logic: Reconciliation ──────────────────────
|
|
356
|
+
|
|
357
|
+
function reconcileTransfer(
|
|
358
|
+
transferAmountCents: Cents,
|
|
359
|
+
bankTransactionCents: Cents,
|
|
360
|
+
platformSpendCents: Cents,
|
|
361
|
+
thresholdBps: number, // basis points tolerance (e.g., 50 = 0.5%)
|
|
362
|
+
platform: AdPlatform,
|
|
363
|
+
date: string,
|
|
364
|
+
previousHash: string
|
|
365
|
+
): ReconciliationRecord {
|
|
366
|
+
const invoiceCents = transferAmountCents; // what was transferred
|
|
367
|
+
const varianceCents = Math.abs(invoiceCents - bankTransactionCents) as Cents;
|
|
368
|
+
|
|
369
|
+
// Determine result based on variance threshold
|
|
370
|
+
let result: ReconciliationResult;
|
|
371
|
+
if (varianceCents === 0) {
|
|
372
|
+
result = 'MATCHED';
|
|
373
|
+
} else {
|
|
374
|
+
const varianceBps = invoiceCents > 0
|
|
375
|
+
? (varianceCents / invoiceCents) * 10_000
|
|
376
|
+
: 0;
|
|
377
|
+
result = varianceBps <= thresholdBps ? 'WITHIN_THRESHOLD' : 'MISMATCH';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const id = crypto.randomUUID();
|
|
381
|
+
const now = new Date().toISOString();
|
|
382
|
+
|
|
383
|
+
const record: Omit<ReconciliationRecord, 'hash'> = {
|
|
384
|
+
id,
|
|
385
|
+
platform,
|
|
386
|
+
date,
|
|
387
|
+
spendCents: platformSpendCents,
|
|
388
|
+
bankSettledCents: bankTransactionCents,
|
|
389
|
+
invoiceCents,
|
|
390
|
+
varianceCents,
|
|
391
|
+
result,
|
|
392
|
+
notes: result === 'MISMATCH'
|
|
393
|
+
? `Variance ${varianceCents} cents exceeds ${thresholdBps}bps threshold`
|
|
394
|
+
: result === 'WITHIN_THRESHOLD'
|
|
395
|
+
? `Variance ${varianceCents} cents within ${thresholdBps}bps threshold`
|
|
396
|
+
: 'Exact match',
|
|
397
|
+
createdAt: now,
|
|
398
|
+
previousHash,
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const hash = computeReconciliationHash(record, previousHash);
|
|
402
|
+
return { ...record, hash };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ── Pure Logic: Invoice Priority ────────────────────
|
|
406
|
+
// When multiple invoices are pending, prioritize by due date and overdue status.
|
|
407
|
+
|
|
408
|
+
interface PendingObligation {
|
|
409
|
+
id: string;
|
|
410
|
+
platform: AdPlatform;
|
|
411
|
+
amountCents: Cents;
|
|
412
|
+
dueDate: string; // ISO 8601
|
|
413
|
+
overdue: boolean;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function prioritizeObligations(obligations: PendingObligation[]): PendingObligation[] {
|
|
417
|
+
return [...obligations].sort((a, b) => {
|
|
418
|
+
// Overdue items first
|
|
419
|
+
if (a.overdue && !b.overdue) return -1;
|
|
420
|
+
if (!a.overdue && b.overdue) return 1;
|
|
421
|
+
// Then by due date (earliest first)
|
|
422
|
+
return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime();
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── Framework Adaptation Notes ──────────────────────
|
|
427
|
+
//
|
|
428
|
+
// Express/Node.js:
|
|
429
|
+
// - FundingPlan stored as JSONL append log (funding-plans.jsonl)
|
|
430
|
+
// - TransferRecord stored as JSONL append log (transfers.jsonl)
|
|
431
|
+
// - ReconciliationRecord stored as JSONL append log (reconciliation.jsonl)
|
|
432
|
+
// - All writes go through atomicWrite() from financial-transaction.ts
|
|
433
|
+
// - Only Heartbeat daemon writes — API reads from logs
|
|
434
|
+
//
|
|
435
|
+
// Django/FastAPI:
|
|
436
|
+
// - FundingPlan as Django model with status FSM (django-fsm)
|
|
437
|
+
// - TransferRecord as separate model with FK to FundingPlan
|
|
438
|
+
// - ReconciliationRecord as daily snapshot model
|
|
439
|
+
// - Single Celery worker for all write operations
|
|
440
|
+
//
|
|
441
|
+
// Next.js:
|
|
442
|
+
// - Read API routes serve from JSONL logs
|
|
443
|
+
// - Write operations proxied to Heartbeat daemon via Unix socket
|
|
444
|
+
// - Dashboard components consume normalizeFundingState() output
|
|
445
|
+
|
|
446
|
+
export type {
|
|
447
|
+
Cents,
|
|
448
|
+
StablecoinProviderType, StablecoinFundingSource,
|
|
449
|
+
BankProvider, OperatingBankAccount,
|
|
450
|
+
CapabilityState, BillingMode, AdPlatform, PlatformBillingProfile,
|
|
451
|
+
FundingPlanStatus, FundingPlanReason, ApprovalMode, TargetPlatform, FundingPlan,
|
|
452
|
+
TransferDirection, TransferStatus, TransferRecord,
|
|
453
|
+
ReconciliationResult, ReconciliationRecord,
|
|
454
|
+
PendingObligation,
|
|
455
|
+
};
|
|
456
|
+
export {
|
|
457
|
+
toCents, toDollars,
|
|
458
|
+
computePlanHash, computeTransferHash, computeReconciliationHash,
|
|
459
|
+
createFundingPlan, approvePlan, transitionPlan,
|
|
460
|
+
calculateRunway, shouldTriggerOfframp, calculateRequiredOfframp,
|
|
461
|
+
reconcileTransfer, prioritizeObligations,
|
|
462
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: Entity Component System (ECS)
|
|
3
|
+
*
|
|
4
|
+
* Key principles:
|
|
5
|
+
* - Entities are IDs, not objects. No inheritance hierarchy.
|
|
6
|
+
* - Components are pure data. No methods.
|
|
7
|
+
* - Systems operate on components. All logic lives in systems.
|
|
8
|
+
* - Composition over inheritance: an entity with Position + Velocity + Sprite is a moving sprite.
|
|
9
|
+
* - Object pooling: reuse entity IDs and component storage instead of GC pressure.
|
|
10
|
+
*
|
|
11
|
+
* Agents: Spike-GameDev (architecture), L-Profiler (memory/GC), Gimli (performance)
|
|
12
|
+
*
|
|
13
|
+
* Engine adaptations:
|
|
14
|
+
* Phaser: Arcade Physics bodies are pseudo-ECS. For full ECS, use bitecs or miniplex.
|
|
15
|
+
* Godot: Scene tree is composition-based (nodes = components). Not pure ECS but same spirit.
|
|
16
|
+
* Unity: Unity DOTS/ECS for performance-critical systems, MonoBehaviour for the rest.
|
|
17
|
+
* Three.js: This file (manual ECS, or use bitecs/miniplex library)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// --- Entity ---
|
|
21
|
+
type EntityId = number
|
|
22
|
+
let nextEntityId: EntityId = 0
|
|
23
|
+
|
|
24
|
+
function createEntity(): EntityId {
|
|
25
|
+
return nextEntityId++
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function destroyEntity(id: EntityId): void {
|
|
29
|
+
// Remove all components for this entity
|
|
30
|
+
for (const store of componentStores.values()) {
|
|
31
|
+
store.delete(id)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- Components (pure data, no methods) ---
|
|
36
|
+
interface Position { x: number; y: number }
|
|
37
|
+
interface Velocity { vx: number; vy: number }
|
|
38
|
+
interface Sprite { texture: string; width: number; height: number }
|
|
39
|
+
interface Health { current: number; max: number }
|
|
40
|
+
interface Collider { radius: number }
|
|
41
|
+
interface PlayerControlled { speed: number }
|
|
42
|
+
interface Enemy { aiType: 'chase' | 'patrol' | 'idle'; target?: EntityId }
|
|
43
|
+
|
|
44
|
+
// --- Component Storage ---
|
|
45
|
+
const componentStores = new Map<string, Map<EntityId, unknown>>()
|
|
46
|
+
|
|
47
|
+
function registerComponent<T>(name: string): {
|
|
48
|
+
set: (id: EntityId, data: T) => void
|
|
49
|
+
get: (id: EntityId) => T | undefined
|
|
50
|
+
has: (id: EntityId) => boolean
|
|
51
|
+
delete: (id: EntityId) => void
|
|
52
|
+
all: () => [EntityId, T][]
|
|
53
|
+
} {
|
|
54
|
+
const store = new Map<EntityId, T>()
|
|
55
|
+
componentStores.set(name, store as Map<EntityId, unknown>)
|
|
56
|
+
return {
|
|
57
|
+
set: (id, data) => store.set(id, data),
|
|
58
|
+
get: (id) => store.get(id),
|
|
59
|
+
has: (id) => store.has(id),
|
|
60
|
+
delete: (id) => store.delete(id),
|
|
61
|
+
all: () => [...store.entries()],
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Register component stores
|
|
66
|
+
const positions = registerComponent<Position>('position')
|
|
67
|
+
const velocities = registerComponent<Velocity>('velocity')
|
|
68
|
+
const sprites = registerComponent<Sprite>('sprite')
|
|
69
|
+
const healths = registerComponent<Health>('health')
|
|
70
|
+
const colliders = registerComponent<Collider>('collider')
|
|
71
|
+
const playerControlled = registerComponent<PlayerControlled>('playerControlled')
|
|
72
|
+
const enemies = registerComponent<Enemy>('enemy')
|
|
73
|
+
|
|
74
|
+
// --- Systems (all logic here, not in components) ---
|
|
75
|
+
|
|
76
|
+
function movementSystem(dt: number): void {
|
|
77
|
+
for (const [id, vel] of velocities.all()) {
|
|
78
|
+
const pos = positions.get(id)
|
|
79
|
+
if (pos) {
|
|
80
|
+
pos.x += vel.vx * dt
|
|
81
|
+
pos.y += vel.vy * dt
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function collisionSystem(): [EntityId, EntityId][] {
|
|
87
|
+
const collisions: [EntityId, EntityId][] = []
|
|
88
|
+
const entities = colliders.all()
|
|
89
|
+
for (let i = 0; i < entities.length; i++) {
|
|
90
|
+
for (let j = i + 1; j < entities.length; j++) {
|
|
91
|
+
const [idA, colA] = entities[i]
|
|
92
|
+
const [idB, colB] = entities[j]
|
|
93
|
+
const posA = positions.get(idA)
|
|
94
|
+
const posB = positions.get(idB)
|
|
95
|
+
if (posA && posB) {
|
|
96
|
+
const dx = posA.x - posB.x
|
|
97
|
+
const dy = posA.y - posB.y
|
|
98
|
+
const dist = Math.sqrt(dx * dx + dy * dy)
|
|
99
|
+
if (dist < colA.radius + colB.radius) {
|
|
100
|
+
collisions.push([idA, idB])
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return collisions
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Entity Factory (composition) ---
|
|
109
|
+
|
|
110
|
+
function createPlayer(x: number, y: number): EntityId {
|
|
111
|
+
const id = createEntity()
|
|
112
|
+
positions.set(id, { x, y })
|
|
113
|
+
velocities.set(id, { vx: 0, vy: 0 })
|
|
114
|
+
sprites.set(id, { texture: 'player.png', width: 32, height: 32 })
|
|
115
|
+
healths.set(id, { current: 100, max: 100 })
|
|
116
|
+
colliders.set(id, { radius: 16 })
|
|
117
|
+
playerControlled.set(id, { speed: 200 })
|
|
118
|
+
return id
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function createEnemy(x: number, y: number, aiType: 'chase' | 'patrol'): EntityId {
|
|
122
|
+
const id = createEntity()
|
|
123
|
+
positions.set(id, { x, y })
|
|
124
|
+
velocities.set(id, { vx: 0, vy: 0 })
|
|
125
|
+
sprites.set(id, { texture: 'enemy.png', width: 32, height: 32 })
|
|
126
|
+
healths.set(id, { current: 50, max: 50 })
|
|
127
|
+
colliders.set(id, { radius: 16 })
|
|
128
|
+
enemies.set(id, { aiType })
|
|
129
|
+
return id
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
createEntity, destroyEntity, createPlayer, createEnemy,
|
|
134
|
+
movementSystem, collisionSystem,
|
|
135
|
+
positions, velocities, sprites, healths, colliders, playerControlled, enemies
|
|
136
|
+
}
|
|
137
|
+
export type { EntityId, Position, Velocity, Sprite, Health }
|