Skip to main content

max / makenotwork

docengine: derive annual prices from monthly + discount; fix fixture path Closes the last hand-typed-number gap in public docs. Annual prices ($86, $130, $194, $324 founder; $173, $259, $389, $648 standard) now compute from monthly × 12 × annual_discount.multiplier via insert_derived and substitute into tiers.md and pricing.md as ${{ derived.annual_*_* }}. Changes: - shared/docengine/src/assumptions.rs - Add TAnnualDiscount struct + annual_discount field on Typed. - insert_derived: compute 8 new keys (derived.annual_{founding,standard}_{basic,small_files,big_files,everything}), rounded to nearest dollar. - Fix include_str! FIXTURE path: canonical toml moved to ~/Code/_private/ on 2026-05-20; bump path 3 -> 4 dots up. cargo test --features assumptions now compiles (was broken). - Update existing test expectations to canonical $16/$24/$36/$60 prices: marginal_stripe_standard 0.938 -> 1.0772, marginal_stripe_founding 0.619 -> 0.6886, break_even ceil 28 -> 23, break_even round(1) 22.5 -> 22.6. - Update validation_catches_founding_above_standard fixture-mutation string from stale "basic = 5" comment to canonical "basic = 8". - Add derived_annual_prices_match_published_values test pinning all 8 annual values to the published $86/$130/$194/$324/$173/$259/$389/$648. - 126/126 tests pass. - shared/docengine/Cargo.toml: 0.3.4 -> 0.3.5 (additive change to derived registry; semver-patch since no public API break). - server/tests/assumptions.rs: fix ASSUMPTIONS_PATH to point at _private/ (../../_private/...) so cargo test --test assumptions can load the canonical file. Both integration tests pass, including the every_marker_in_site_docs_resolves end-to-end check that confirms every substitution in site-docs/public/*.md resolves. - site-docs/public/guide/tiers.md and about/pricing.md: substitute ${{ derived.annual_* }} for the 12 hand-typed $N/yr cells. No server version bump; this ships with the in-flight 0.8.14 deploy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-01 05:32 UTC
Commit: c08307462e198a5d258d572aebf7f5f29eb42f63
Parent: c3375ec
7 files changed, +81 insertions, -33 deletions
@@ -2431,7 +2431,7 @@ dependencies = [
2431 2431
2432 2432 [[package]]
2433 2433 name = "docengine"
2434 - version = "0.3.4"
2434 + version = "0.3.5"
2435 2435 dependencies = [
2436 2436 "ammonia",
2437 2437 "pulldown-cmark",
@@ -24,10 +24,10 @@ Every tier is the complete platform: profile page at `/u/username`, project and
24 24
25 25 | Tier | Monthly | Annual (10% off) | File-size envelope |
26 26 |---|---|---|---|
27 - | **Basic** | ${{ tiers.founding.basic }}/mo | $86/yr | Up to {{ tier_limits.basic_per_file }} per file, {{ tier_limits.basic_total }} total. Fits text, blogs, newsletters, posts. |
28 - | **Small Files** | ${{ tiers.founding.small_files }}/mo | $130/yr | Up to {{ tier_limits.small_files_per_file }} per file, {{ tier_limits.small_files_total }} total. Fits audio, plugins, binaries, sample packs. |
29 - | **Big Files** | ${{ tiers.founding.big_files }}/mo | $194/yr | Up to {{ tier_limits.big_files_per_file }} per file, {{ tier_limits.big_files_total }} total. Fits video, games, large software. |
30 - | **Everything** | ${{ tiers.founding.everything }}/mo | $324/yr | Big Files envelope plus first access to high-cost capabilities as they ship (streaming infrastructure, etc.). |
27 + | **Basic** | ${{ tiers.founding.basic }}/mo | ${{ derived.annual_founding_basic }}/yr | Up to {{ tier_limits.basic_per_file }} per file, {{ tier_limits.basic_total }} total. Fits text, blogs, newsletters, posts. |
28 + | **Small Files** | ${{ tiers.founding.small_files }}/mo | ${{ derived.annual_founding_small_files }}/yr | Up to {{ tier_limits.small_files_per_file }} per file, {{ tier_limits.small_files_total }} total. Fits audio, plugins, binaries, sample packs. |
29 + | **Big Files** | ${{ tiers.founding.big_files }}/mo | ${{ derived.annual_founding_big_files }}/yr | Up to {{ tier_limits.big_files_per_file }} per file, {{ tier_limits.big_files_total }} total. Fits video, games, large software. |
30 + | **Everything** | ${{ tiers.founding.everything }}/mo | ${{ derived.annual_founding_everything }}/yr | Big Files envelope plus first access to high-cost capabilities as they ship (streaming infrastructure, etc.). |
31 31
32 32 Annual is 10% off the monthly total. Most of that is the Stripe per-transaction fee we don't pay when we charge once a year instead of twelve times. We pass it back rather than keep it.
33 33
@@ -41,10 +41,10 @@ The standard rate applies to new signups after the founder window closes. The va
41 41
42 42 | Tier | Monthly | Annual (10% off) |
43 43 |---|---|---|
44 - | **Basic** | ${{ tiers.standard.basic }}/mo | $173/yr |
45 - | **Small Files** | ${{ tiers.standard.small_files }}/mo | $259/yr |
46 - | **Big Files** | ${{ tiers.standard.big_files }}/mo | $389/yr |
47 - | **Everything** | ${{ tiers.standard.everything }}/mo | $648/yr |
44 + | **Basic** | ${{ tiers.standard.basic }}/mo | ${{ derived.annual_standard_basic }}/yr |
45 + | **Small Files** | ${{ tiers.standard.small_files }}/mo | ${{ derived.annual_standard_small_files }}/yr |
46 + | **Big Files** | ${{ tiers.standard.big_files }}/mo | ${{ derived.annual_standard_big_files }}/yr |
47 + | **Everything** | ${{ tiers.standard.everything }}/mo | ${{ derived.annual_standard_everything }}/yr |
48 48
49 49 ---
50 50
@@ -8,10 +8,10 @@ We are running a founder window. **Every creator who joins right now pays half t
8 8
9 9 | Tier | Founder monthly | Founder annual (10% off) | Eventual sticker |
10 10 |------|-----------------|--------------------------|------------------|
11 - | **Basic** | **${{ tiers.founding.basic }}/mo** | $86/yr | ${{ tiers.standard.basic }}/mo |
12 - | **Small Files** | **${{ tiers.founding.small_files }}/mo** | $130/yr | ${{ tiers.standard.small_files }}/mo |
13 - | **Big Files** | **${{ tiers.founding.big_files }}/mo** | $194/yr | ${{ tiers.standard.big_files }}/mo |
14 - | **Everything** | **${{ tiers.founding.everything }}/mo** | $324/yr | ${{ tiers.standard.everything }}/mo |
11 + | **Basic** | **${{ tiers.founding.basic }}/mo** | ${{ derived.annual_founding_basic }}/yr | ${{ tiers.standard.basic }}/mo |
12 + | **Small Files** | **${{ tiers.founding.small_files }}/mo** | ${{ derived.annual_founding_small_files }}/yr | ${{ tiers.standard.small_files }}/mo |
13 + | **Big Files** | **${{ tiers.founding.big_files }}/mo** | ${{ derived.annual_founding_big_files }}/yr | ${{ tiers.standard.big_files }}/mo |
14 + | **Everything** | **${{ tiers.founding.everything }}/mo** | ${{ derived.annual_founding_everything }}/yr | ${{ tiers.standard.everything }}/mo |
15 15
16 16 The window closes when we reach {{ cohort.cap_display }} creators or when we exit beta, whichever comes first. After that, new signups pay the sticker price.
17 17
@@ -9,7 +9,11 @@
9 9
10 10 use docengine::Assumptions;
11 11
12 - const ASSUMPTIONS_PATH: &str = "docs/internal/business/assumptions.toml";
12 + // Canonical assumptions.toml moved to _private/ on 2026-05-20. Test runs from
13 + // the server crate's cwd (MNW/server/); one level up gets to MNW/, two gets
14 + // to ~/Code/. Production uses ASSUMPTIONS_PATH env override (see main.rs).
15 + const ASSUMPTIONS_PATH: &str =
16 + "../../_private/docs/mnw/server-internal/business/assumptions.toml";
13 17 const SITE_DOCS_PATH: &str = "site-docs/public";
14 18
15 19 #[test]
@@ -84,7 +84,7 @@ dependencies = [
84 84
85 85 [[package]]
86 86 name = "docengine"
87 - version = "0.3.4"
87 + version = "0.3.5"
88 88 dependencies = [
89 89 "ammonia",
90 90 "pulldown-cmark",
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "docengine"
3 - version = "0.3.4"
3 + version = "0.3.5"
4 4 edition = "2024"
5 5
6 6 [features]
@@ -334,6 +334,7 @@ struct Typed {
334 334 reserve: TReserve,
335 335 cohort: TCohort,
336 336 creator_marginal: TCreatorMarginal,
337 + annual_discount: TAnnualDiscount,
337 338 }
338 339
339 340 #[derive(Debug, Deserialize)]
@@ -399,6 +400,11 @@ struct TCohort {
399 400 }
400 401
401 402 #[derive(Debug, Deserialize)]
403 + struct TAnnualDiscount {
404 + multiplier: f64,
405 + }
406 +
407 + #[derive(Debug, Deserialize)]
402 408 struct TCreatorMarginal {
403 409 storage_basic_gb: f64,
404 410 storage_small_files_gb: f64,
@@ -465,6 +471,19 @@ fn insert_derived(t: &Typed, out: &mut HashMap<String, LookupValue>) {
465 471 put("stripe_fee_big_std", stripe_fee(t.tiers.standard.big_files));
466 472 put("stripe_fee_ev_std", stripe_fee(t.tiers.standard.everything));
467 473
474 + // Annual prices per tier (monthly × 12 × annual_discount.multiplier, rounded
475 + // to nearest dollar). Substituted into docs as `${{ derived.annual_*_* }}`
476 + // so a price change auto-propagates.
477 + let yr = |monthly: f64| (monthly * 12.0 * t.annual_discount.multiplier).round();
478 + put("annual_founding_basic", yr(t.tiers.founding.basic));
479 + put("annual_founding_small_files", yr(t.tiers.founding.small_files));
480 + put("annual_founding_big_files", yr(t.tiers.founding.big_files));
481 + put("annual_founding_everything", yr(t.tiers.founding.everything));
482 + put("annual_standard_basic", yr(t.tiers.standard.basic));
483 + put("annual_standard_small_files", yr(t.tiers.standard.small_files));
484 + put("annual_standard_big_files", yr(t.tiers.standard.big_files));
485 + put("annual_standard_everything", yr(t.tiers.standard.everything));
486 +
468 487 // ── Marginal cost per creator/month, broken down by component ──
469 488 //
470 489 // What MNW pays per active creator on top of fixed costs F:
@@ -544,7 +563,15 @@ fn insert_derived(t: &Typed, out: &mut HashMap<String, LookupValue>) {
544 563 mod tests {
545 564 use super::*;
546 565
547 - const FIXTURE: &str = include_str!("../../../server/docs/internal/business/assumptions.toml");
566 + // Canonical assumptions.toml lives outside the MNW repo at
567 + // ~/Code/_private/docs/mnw/server-internal/business/assumptions.toml (moved
568 + // 2026-05-20). include_str! resolves relative to this source file. A clone
569 + // of the MNW repo without _private/ alongside will fail to build the
570 + // assumptions feature; production never hits this path because main.rs
571 + // uses runtime Assumptions::load with ASSUMPTIONS_PATH env override.
572 + const FIXTURE: &str = include_str!(
573 + "../../../../_private/docs/mnw/server-internal/business/assumptions.toml"
574 + );
548 575
549 576 fn loaded() -> Assumptions {
550 577 Assumptions::parse(FIXTURE).expect("fixture parses")
@@ -630,9 +657,9 @@ mod tests {
630 657 assert!((storage - 0.03666).abs() < 1e-4, "storage = {storage}");
631 658
632 659 // Stripe fee on creator subs at standard rates (weighted by tier mix):
633 - // 0.4·0.59 + 0.3·0.88 + 0.2·1.17 + 0.1·2.04 = 0.938
660 + // 0.4·0.764 + 0.3·0.996 + 0.2·1.344 + 0.1·2.040 = 1.0772
634 661 let stripe = get_f("derived.marginal_stripe_standard");
635 - assert!((stripe - 0.938).abs() < 1e-4, "stripe = {stripe}");
662 + assert!((stripe - 1.0772).abs() < 1e-4, "stripe = {stripe}");
636 663
637 664 // Chargeback EV: 0.001 × $15 dispute = $0.015
638 665 let chargeback = get_f("derived.marginal_chargeback");
@@ -643,9 +670,30 @@ mod tests {
643 670 assert!((avg - (storage + stripe + chargeback)).abs() < 1e-9, "avg = {avg}");
644 671
645 672 // Founding Stripe fee is lower (lower tier prices, less % component):
646 - // 0.4·0.445 + 0.3·0.59 + 0.2·0.735 + 0.1·1.17 = 0.619
673 + // 0.4·0.532 + 0.3·0.648 + 0.2·0.822 + 0.1·1.170 = 0.6886
647 674 let stripe_f = get_f("derived.marginal_stripe_founding");
648 - assert!((stripe_f - 0.619).abs() < 1e-4, "stripe_f = {stripe_f}");
675 + assert!((stripe_f - 0.6886).abs() < 1e-4, "stripe_f = {stripe_f}");
676 + }
677 +
678 + #[test]
679 + fn derived_annual_prices_match_published_values() {
680 + let a = loaded();
681 + let get_f = |k: &str| match a.get(k).unwrap() {
682 + LookupValue::Float(x) => *x,
683 + v => panic!("expected float at {k}, got {v:?}"),
684 + };
685 +
686 + // Founder: $8/$12/$18/$30 × 12 × 0.9 → rounded.
687 + assert_eq!(get_f("derived.annual_founding_basic"), 86.0);
688 + assert_eq!(get_f("derived.annual_founding_small_files"), 130.0);
689 + assert_eq!(get_f("derived.annual_founding_big_files"), 194.0);
690 + assert_eq!(get_f("derived.annual_founding_everything"), 324.0);
691 +
692 + // Standard: $16/$24/$36/$60 × 12 × 0.9 → rounded.
693 + assert_eq!(get_f("derived.annual_standard_basic"), 173.0);
694 + assert_eq!(get_f("derived.annual_standard_small_files"), 259.0);
695 + assert_eq!(get_f("derived.annual_standard_big_files"), 389.0);
696 + assert_eq!(get_f("derived.annual_standard_everything"), 648.0);
649 697 }
650 698
651 699 #[test]
@@ -654,9 +702,9 @@ mod tests {
654 702 let out = a
655 703 .substitute("Break-even at ~{{ derived.break_even_standard | ceil }} creators.")
656 704 .unwrap();
657 - // break_even_standard ≈ 27.6 → ceil → 28 (with full marginal model:
658 - // storage + weighted Stripe fee on creator subs + chargeback EV).
659 - assert_eq!(out, "Break-even at ~28 creators.");
705 + // break_even_standard ≈ 22.6 → ceil → 23 (with full marginal model at
706 + // standard $16/$24/$36/$60: 580 / (26.80 − 1.128) ≈ 22.6).
707 + assert_eq!(out, "Break-even at ~23 creators.");
660 708 }
661 709
662 710 #[test]
@@ -679,8 +727,8 @@ mod tests {
679 727 let out = a
680 728 .substitute("{{ derived.break_even_standard | round(1) }}")
681 729 .unwrap();
682 - // 580 / (26.80 − ~1.08) ≈ 22.5
683 - assert_eq!(out, "22.5");
730 + // 580 / (26.80 − ~1.128) ≈ 22.6 at canonical $16/$24/$36/$60.
731 + assert_eq!(out, "22.6");
684 732 }
685 733
686 734 #[test]
@@ -782,13 +830,9 @@ mod tests {
782 830
783 831 #[test]
784 832 fn validation_catches_founding_above_standard() {
785 - let t = FIXTURE.replace(
786 - "[tiers.founding]
787 - # OPEN: founding ratio (currently 50% of standard) — pricing.md §7 item 2
788 - basic = 5",
789 - "[tiers.founding]
790 - basic = 999",
791 - );
833 + // `basic = 8` appears only in [tiers.founding] in the canonical fixture
834 + // (standard has `basic = 16`), so this substring is unambiguous.
835 + let t = FIXTURE.replace("basic = 8", "basic = 999");
792 836 let a = Assumptions::parse(&t).unwrap();
793 837 let err = a.validate().unwrap_err();
794 838 match err {