Skip to main content

max / makenotwork

server: /docs/economics is now an Askama page with live runway disclosure The retired markdown source is replaced by an Askama template at the same URL (/docs/economics) so inbound links don't break. Static-slug route registers before the catch-all /docs/{slug} so axum prefers the exact match. New disclosure section pulls live data: - count_active_paying: strict status='active' — revenue-bearing seats - count_trialing_or_grace: trialing + canceled-with-grace, shown only when non-zero so a quiet platform doesn't render "0 in trial" [runway] block in assumptions.toml carries the operator-set quarters figure + last_updated_iso stamp. quarters=0 is the "not yet published" sentinel and the template suppresses the bullet rather than rendering "0 quarters at current burn". Refresh quarterly at sprint close. RunwayConfig sibling to TierPrices/CostAllocation in tier_prices.rs; RunwayConfig::is_published gates the cash-runway line. Two new tier_prices tests pin the toml keys (so a rename catches at PR time) and the is_published gating; 1,665 lib tests passing.
Author: Max Johnson <me@maxj.phd> · 2026-06-04 04:33 UTC
Signed with PGP, not checked
Commit: e746b8d58e85996581ec5b3696bc08faa7e87aaa
Parent: 4347b1d
11 files changed, +288 insertions, -56 deletions
@@ -76,6 +76,7 @@
76 76 pub docs: Arc<DocLoader>,
77 77 pub tier_prices: tier_prices::TierPrices,
78 78 pub cost_allocation: tier_prices::CostAllocation,
79 + pub runway_config: tier_prices::RunwayConfig,
79 80 pub scanner: Option<Arc<ScanPipeline>>,
80 81 pub webauthn: Arc<Webauthn>,
81 82 pub syntax: Option<Arc<git::SyntaxHighlighter>>,
@@ -322,6 +322,7 @@
322 322 let tp = makenotwork::tier_prices::TierPrices::from_assumptions(&assumptions);
323 323 makenotwork::tier_prices::CostAllocation::from_assumptions(&assumptions, &tp)
324 324 },
325 + runway_config: makenotwork::tier_prices::RunwayConfig::from_assumptions(&assumptions),
325 326 scanner,
326 327 webauthn,
327 328 syntax,
@@ -255,6 +255,38 @@
255 255 s
256 256 }
257 257
258 + /// Operator-edited runway figures, loaded once at startup. The
259 + /// live paying-creator counts come from the DB at request time and
260 + /// are NOT in this struct — see `db::creator_tiers::count_active_paying`
261 + /// and `count_trialing_or_grace`.
262 + ///
263 + /// `quarters` is the cash-runway bucket in whole quarters (rounded down).
264 + /// A value of `0` means "not yet published" and the template should
265 + /// suppress the line rather than render "0 quarters".
266 + ///
267 + /// `last_updated_iso` is the date the operator last refreshed the figure,
268 + /// in ISO 8601 (`YYYY-MM-DD`). Rendered verbatim into the "Last updated"
269 + /// stamp on the disclosure surface.
270 + #[derive(Clone, Debug, Default)]
271 + pub struct RunwayConfig {
272 + pub quarters: i32,
273 + pub last_updated_iso: String,
274 + }
275 +
276 + impl RunwayConfig {
277 + pub fn from_assumptions(a: &Assumptions) -> Self {
278 + Self {
279 + quarters: int_at(a, "runway.quarters"),
280 + last_updated_iso: str_at(a, "runway.last_updated_iso"),
281 + }
282 + }
283 + /// True iff the operator has published a runway figure. Suppress the
284 + /// "X quarters at current burn" line when this is false.
285 + pub fn is_published(&self) -> bool {
286 + self.quarters > 0
287 + }
288 + }
289 +
258 290 fn float_at(a: &Assumptions, key: &str) -> f64 {
259 291 match a.get(key) {
260 292 Some(LookupValue::Float(x)) => *x,
@@ -331,6 +363,34 @@
331 363 assert_eq!(everything.segments[4].amount, "$7.50"); // reserves 12.5% of $60
332 364 }
333 365
366 + #[test]
367 + fn runway_config_loads_from_canonical_assumptions() {
368 + // The presence of the [runway] block is the only enforced thing —
369 + // the values inside are operator-edited. We pin the keys so a
370 + // future toml edit that renames `quarters` or `last_updated_iso`
371 + // is caught at PR time, not at boot.
372 + let a = Assumptions::load(ASSUMPTIONS_PATH).expect("load canonical toml");
373 + let r = RunwayConfig::from_assumptions(&a);
374 + assert!(r.quarters >= 0, "quarters must be a non-negative integer");
375 + assert!(!r.last_updated_iso.is_empty(), "last_updated_iso must be set");
376 + // ISO 8601 date format: YYYY-MM-DD.
377 + assert_eq!(r.last_updated_iso.len(), 10);
378 + assert!(r.last_updated_iso.chars().nth(4) == Some('-'));
379 + assert!(r.last_updated_iso.chars().nth(7) == Some('-'));
380 + }
381 +
382 + #[test]
383 + fn runway_config_is_published_only_when_quarters_nonzero() {
384 + // The disclosure template hides the cash-runway bullet when this
385 + // returns false, so a freshly-deployed instance with quarters=0
386 + // doesn't display "0 quarters at current burn" — which would be
387 + // both wrong and alarming.
388 + let r = RunwayConfig { quarters: 0, last_updated_iso: "2026-06-03".into() };
389 + assert!(!r.is_published());
390 + let r = RunwayConfig { quarters: 4, last_updated_iso: "2026-06-03".into() };
391 + assert!(r.is_published());
392 + }
393 +
334 394 #[test]
335 395 fn cost_allocation_aria_label_names_every_segment() {
336 396 // Screen readers get the full breakdown via aria-label since the
@@ -293,3 +293,19 @@
293 293 engineering = 6.00
294 294 reserves = 7.50
295 295 earnback = 33.46
296 +
297 +
298 + # ─── Runway disclosure (transparency block on /docs/about/economics) ─────
299 + #
300 + # Operator-set. Refreshed quarterly at sprint close, alongside the
301 + # /changelog recap. The paying-creator count on the page is pulled live
302 + # from the DB; this number is the cash-runway bucket.
303 + #
304 + # Units: quarters at current burn (whole-number bucket; we round down
305 + # rather than mislead in either direction).
306 + #
307 + # A value of 0 means "not yet published" — the page renders without a
308 + # number until the first quarterly refresh sets it.
309 + [runway]
310 + quarters = 0
311 + last_updated_iso = "2026-06-03"
@@ -472,6 +472,47 @@
472 472 Ok(())
473 473 }
474 474
475 + /// Count fully-paying creators — `status = 'active'` only.
476 + ///
477 + /// Excludes trialing (free trial), past_due (payment failed but not yet
478 + /// canceled), canceled-in-grace (winding down), and incomplete states.
479 + /// This is the number that goes on the runway disclosure as "paying
480 + /// creators today": revenue-bearing seats, no fudge.
481 + #[tracing::instrument(skip_all)]
482 + pub async fn count_active_paying(pool: &PgPool) -> Result<i64> {
483 + let count: (i64,) = sqlx::query_as(
484 + "SELECT COUNT(*) FROM creator_subscriptions WHERE status = 'active'",
485 + )
486 + .fetch_one(pool)
487 + .await?;
488 + Ok(count.0)
489 + }
490 +
491 + /// Count creators in a trial or 30-day cancellation grace period.
492 + ///
493 + /// These are not revenue-bearing today but represent the near-term
494 + /// pipeline: trialing seats may convert, grace seats may resubscribe
495 + /// before enforcement. Disclosed as a secondary number on the runway
496 + /// surface so the headline `count_active_paying` stays strict.
497 + #[tracing::instrument(skip_all)]
498 + pub async fn count_trialing_or_grace(pool: &PgPool) -> Result<i64> {
499 + let count: (i64,) = sqlx::query_as(
500 + r#"
501 + SELECT COUNT(*) FROM creator_subscriptions
502 + WHERE status = 'trialing'
503 + OR (
504 + status = 'canceled'
505 + AND canceled_at IS NOT NULL
506 + AND canceled_at > NOW() - INTERVAL '30 days'
507 + AND grace_enforced_at IS NULL
508 + )
509 + "#,
510 + )
511 + .fetch_one(pool)
512 + .await?;
513 + Ok(count.0)
514 + }
515 +
475 516 /// Check whether a user is in the 30-day cancellation grace period.
476 517 ///
477 518 /// Returns `true` if the subscription is canceled but within 30 days of cancellation
@@ -87,6 +87,8 @@
87 87 DocIndexTemplate,
88 88 // Pricing calculator
89 89 PricingTemplate,
90 + // Platform economics + runway disclosure
91 + EconomicsTemplate,
90 92 // Use cases
91 93 UseCasesTemplate,
92 94 // Team