atmn 2.0.131 → 2.0.133

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.
@@ -83,6 +83,7 @@ export type License = {
83
83
  flatAmount?: number;
84
84
  }>;
85
85
  }>;
86
+ /** How tiers price usage. 'graduated' (default) charges each unit at the rate of the tier it falls in. 'volume' picks one tier from total usage. On usage-based items it bills only usage past the included amount, at the rate of the tier total usage reaches, plus its flat_amount. On prepaid items it charges every unit, the included ones too, at that tier's rate plus its flat_amount. */
86
87
  tierBehavior?: "graduated" | "volume";
87
88
  /** Billing interval. For consumable features, should match reset.interval. */
88
89
  interval:
@@ -394,6 +394,26 @@ export const LINT_RULES: LintRules = {
394
394
  because:
395
395
  "An archived feature should not gain new customers through a live plan.",
396
396
  },
397
+ {
398
+ kind: "targetForbids",
399
+ field: "featureId",
400
+ in: "features",
401
+ matching: "featureId",
402
+ target: "consumable",
403
+ equals: false,
404
+ whenEquals: {
405
+ "price.tierBehavior": "volume",
406
+ "price.billingMethod": "usage_based",
407
+ },
408
+ whenStated: [],
409
+ whenAnyStated: [
410
+ "price.tiers.0.flatAmount",
411
+ "price.tiers.0.additionalCurrencies.*.flatAmount",
412
+ ],
413
+ whenUnstated: [],
414
+ because:
415
+ "A volume-tiered allocated item can't have a first-tier flatAmount: Stripe would charge it even with no seats over included. Move the fee to a later tier.",
416
+ },
397
417
  ],
398
418
  },
399
419
  "plans.items.expiry": {
@@ -570,12 +590,15 @@ export const LINT_RULES: LintRules = {
570
590
  },
571
591
  rules: [
572
592
  {
573
- kind: "valueWhen",
574
- when: "tierBehavior",
575
- equals: "volume",
576
- field: "billingMethod",
577
- mustBe: "prepaid",
578
- because: "Volume tiers are prepaid-only.",
593
+ kind: "refusedWhen",
594
+ whenEquals: {
595
+ tierBehavior: "volume",
596
+ billingMethod: "usage_based",
597
+ },
598
+ whenStated: [],
599
+ whenUnstated: ["tiers.1"],
600
+ because:
601
+ "Volume-based pricing on a usage-based item needs at least two tiers. Add a tier, or use graduated pricing for a single rate.",
579
602
  },
580
603
  ],
581
604
  },
@@ -180,6 +180,32 @@ export type LintRule =
180
180
  readonly equals: string;
181
181
  readonly because: string;
182
182
  }
183
+ | {
184
+ /** The entry is refused when it matches every `whenEquals`,
185
+ * `whenStated` and `whenUnstated` path. */
186
+ readonly kind: "refusedWhen";
187
+ readonly whenEquals: Readonly<Record<string, string>>;
188
+ readonly whenStated: readonly string[];
189
+ readonly whenUnstated: readonly string[];
190
+ readonly because: string;
191
+ }
192
+ | {
193
+ /** `field` names a row of top-level collection `in` by `matching`. The
194
+ * entry is refused when that row's `target` equals `equals` and the entry
195
+ * matches every `whenEquals`, `whenStated` and `whenUnstated` path. */
196
+ readonly kind: "targetForbids";
197
+ readonly field: string;
198
+ readonly in: string;
199
+ readonly matching: string;
200
+ readonly target: string;
201
+ readonly equals: string | boolean;
202
+ readonly whenEquals: Readonly<Record<string, string>>;
203
+ readonly whenStated: readonly string[];
204
+ /** At least one of these is stated; a `*` segment matches any list element. */
205
+ readonly whenAnyStated?: readonly string[];
206
+ readonly whenUnstated: readonly string[];
207
+ readonly because: string;
208
+ }
183
209
  | {
184
210
  /** `field` names a row of top-level collection `in` by `matching`,
185
211
  * shown in the message as `label`; that row's `target` must not be
@@ -250,7 +276,8 @@ type Walk = {
250
276
  const isEntry = (value: unknown): value is Entry =>
251
277
  value !== null && typeof value === "object" && !Array.isArray(value);
252
278
 
253
- /** Dotted lookup, so a rule can name a field inside a union branch. */
279
+ /** Dotted lookup, so a rule can name a field inside a union branch; a numeric
280
+ * segment indexes an array. */
254
281
  const valueAtPath = ({
255
282
  entry,
256
283
  path,
@@ -260,6 +287,10 @@ const valueAtPath = ({
260
287
  }): unknown => {
261
288
  let current: unknown = entry;
262
289
  for (const segment of path.split(".")) {
290
+ if (Array.isArray(current)) {
291
+ current = current[Number(segment)];
292
+ continue;
293
+ }
263
294
  if (!isEntry(current)) return undefined;
264
295
  current = current[segment];
265
296
  }
@@ -463,6 +494,39 @@ const COMPARE = {
463
494
  ">=": { holds: (a: number, b: number) => a >= b, words: "at least" },
464
495
  } as const;
465
496
 
497
+ /** Whether an entry matches a rule's `when*` paths; zero, false and null count as unstated. */
498
+ const entryMatchesWhen = ({
499
+ entry,
500
+ rule,
501
+ }: {
502
+ entry: Entry;
503
+ rule: Extract<LintRule, { kind: "refusedWhen" | "targetForbids" }>;
504
+ }): boolean => {
505
+ const isStatedValue = (value: unknown) =>
506
+ value !== undefined && value !== null && value !== 0 && value !== false;
507
+ const isStated = (path: string): boolean => {
508
+ const [head, tail] = path.split(".*.");
509
+ if (tail === undefined) return isStatedValue(valueAtPath({ entry, path }));
510
+ const list = valueAtPath({ entry, path: head as string });
511
+ return (
512
+ Array.isArray(list) &&
513
+ list.some((element) => isEntry(element) && isStatedValue(element[tail]))
514
+ );
515
+ };
516
+ const anyStated =
517
+ "whenAnyStated" in rule && rule.whenAnyStated
518
+ ? rule.whenAnyStated.some(isStated)
519
+ : true;
520
+ return (
521
+ Object.entries(rule.whenEquals).every(
522
+ ([path, value]) => valueAtPath({ entry, path }) === value,
523
+ ) &&
524
+ rule.whenStated.every(isStated) &&
525
+ anyStated &&
526
+ !rule.whenUnstated.some(isStated)
527
+ );
528
+ };
529
+
466
530
  const entryRuleFailures = ({
467
531
  entry,
468
532
  rule,
@@ -619,6 +683,21 @@ const entryRuleFailures = ({
619
683
  `${rule.label} ${show(entry[rule.field])} is ${rule.target}. Unarchive it, or archive ${rule.parentLabel} ${show(parent?.[rule.parentIdField])}. ${rule.because}`,
620
684
  ];
621
685
  }
686
+ case "refusedWhen":
687
+ return entryMatchesWhen({ entry, rule }) ? [rule.because] : [];
688
+ case "targetForbids": {
689
+ if (!entryMatchesWhen({ entry, rule })) return [];
690
+ const target = document[rule.in];
691
+ if (!Array.isArray(target)) return [];
692
+ const row = target.find(
693
+ (candidate) =>
694
+ isEntry(candidate) && candidate[rule.matching] === entry[rule.field],
695
+ );
696
+ if (!row || row[rule.target] !== rule.equals) return [];
697
+ return [
698
+ `${rule.in} ${show(entry[rule.field])} has ${rule.target} ${show(rule.equals)}, so this is refused. ${rule.because}`,
699
+ ];
700
+ }
622
701
  case "unique":
623
702
  case "linkedOnce":
624
703
  case "exactlyOneActive":
@@ -120,6 +120,7 @@ export type Plan = {
120
120
  flatAmount?: number;
121
121
  }>;
122
122
  }>;
123
+ /** How tiers price usage. 'graduated' (default) charges each unit at the rate of the tier it falls in. 'volume' picks one tier from total usage. On usage-based items it bills only usage past the included amount, at the rate of the tier total usage reaches, plus its flat_amount. On prepaid items it charges every unit, the included ones too, at that tier's rate plus its flat_amount. */
123
124
  tierBehavior?: "graduated" | "volume";
124
125
  /** Billing interval. For consumable features, should match reset.interval. */
125
126
  interval:
@@ -532,6 +533,7 @@ export type Plan = {
532
533
  flatAmount?: number;
533
534
  }>;
534
535
  }>;
536
+ /** How tiers price usage. 'graduated' (default) charges each unit at the rate of the tier it falls in. 'volume' picks one tier from total usage. On usage-based items it bills only usage past the included amount, at the rate of the tier total usage reaches, plus its flat_amount. On prepaid items it charges every unit, the included ones too, at that tier's rate plus its flat_amount. */
535
537
  tierBehavior?: "graduated" | "volume";
536
538
  /** Billing interval. For consumable features, should match reset.interval. */
537
539
  interval:
@@ -763,6 +765,7 @@ export type Plan = {
763
765
  flatAmount?: number;
764
766
  }>;
765
767
  }>;
768
+ /** How tiers price usage. 'graduated' (default) charges each unit at the rate of the tier it falls in. 'volume' picks one tier from total usage. On usage-based items it bills only usage past the included amount, at the rate of the tier total usage reaches, plus its flat_amount. On prepaid items it charges every unit, the included ones too, at that tier's rate plus its flat_amount. */
766
769
  tierBehavior?: "graduated" | "volume";
767
770
  /** Billing interval. For consumable features, should match reset.interval. */
768
771
  interval:
@@ -1147,6 +1150,7 @@ export type Plan = {
1147
1150
  flatAmount?: number;
1148
1151
  }>;
1149
1152
  }>;
1153
+ /** How tiers price usage. 'graduated' (default) charges each unit at the rate of the tier it falls in. 'volume' picks one tier from total usage. On usage-based items it bills only usage past the included amount, at the rate of the tier total usage reaches, plus its flat_amount. On prepaid items it charges every unit, the included ones too, at that tier's rate plus its flat_amount. */
1150
1154
  tierBehavior?: "graduated" | "volume";
1151
1155
  /** Billing interval. For consumable features, should match reset.interval. */
1152
1156
  interval:
@@ -1441,6 +1445,7 @@ export type Plan = {
1441
1445
  flatAmount?: number;
1442
1446
  }>;
1443
1447
  }>;
1448
+ /** How tiers price usage. 'graduated' (default) charges each unit at the rate of the tier it falls in. 'volume' picks one tier from total usage. On usage-based items it bills only usage past the included amount, at the rate of the tier total usage reaches, plus its flat_amount. On prepaid items it charges every unit, the included ones too, at that tier's rate plus its flat_amount. */
1444
1449
  tierBehavior?: "graduated" | "volume";
1445
1450
  /** Billing interval. For consumable features, should match reset.interval. */
1446
1451
  interval:
@@ -38,7 +38,7 @@ export const SKILLS: readonly BundledSkill[] = [
38
38
  "Modeling a user's pricing into an Autumn catalog — deciding the structure (plans, variants, add-ons, licenses, credit systems, pooled balances) before writing config, then filling in the numbers. Use when the user describes their pricing or asks to model, change, or push a catalog.",
39
39
  version: "2.0.120",
40
40
  markdown:
41
- '---\nname: autumn-catalog\ndescription: Modeling a user\'s pricing into an Autumn catalog — deciding the structure (plans, variants, add-ons, licenses, credit systems, pooled balances) before writing config, then filling in the numbers. Use when the user describes their pricing or asks to model, change, or push a catalog.\nversion: 2.0.120\n---\n\n# Catalog\n\nBefore using this skill, first load the `autumn-concepts` skill — it defines Autumn\'s data model — features, plans, plan items, balances — which every modeling decision builds on.\n\n## STRICT RULES — re-read before every config write\n\n1. **Amounts are in major units (e.g. dollars), never minor units (cents).** $180/month is `amount: 180`. $0.01 per credit is `amount: 0.01`. If any amount you wrote is 100× the user\'s number, it is wrong.\n2. **Never invent a price, limit, or plan name** — a missing number is a question.\n3. **One definition per real thing.** One feature per resource, one child plan per license pattern (parents customize their license, never get their own copy), one add-on per offer (sizes are tiers, not plans).\n\n**Building, or iterating on a live catalog?** What matters is whether customers are on these plans — not whether a config file exists. Still setting up (even across sessions, with a half-built `autumn.config.ts`) → the workflow below; edit the draft freely. Already running Autumn with customers, now changing prices/plans → that\'s an update with real stakes (versioning, migrations, grandfathering) — read `references/catalog-update.md` first. Unsure → check for customers (`atmn pull` / the org) or ask.\n\nTurning pricing into a catalog is two jobs:\n\n- **Shape** — decide the structure: which plans exist, what\'s a variant, what\'s an add-on, where balances live. Decided by relationships in their pricing, not by amounts.\n- **Fill** — put in the numbers and per-item details, then write and validate the config.\n\nDo Shape fully before Fill. Take numbers whenever the user mentions them, but never chase numbers during Shape — the one exception is "is this the same on every plan?", which is a structure question.\n\n## Progress (what the user sees)\n\nCopy this checklist into your first message and keep it up to date. It is the **only** structure the user ever sees — never say "step", "pass", "decide", or "fork" to them. If you skip an item, say why in one line.\n\n- [ ] 1 Your plans and prices\n- [ ] 2 What\'s included in each plan\n- [ ] 3 How billing behaves (signup, trials, limits)\n- [ ] 4 Licenses — paid seats, workspaces, projects (if any)\n- [ ] 5 Structure agreed\n- [ ] 6 Config written and checked\n\n## How to ask\n\n- One topic per message. Two questions at most, and only if both belong to that topic. Never mix topics in one message.\n- If one ambiguity changes which other questions apply, resolve it first before asking those.\n- Attach your guess to each question — "does the trial need a card? I\'d guess no" — a wrong guess gets corrected faster than a blank gets answered.\n- Assert the obvious instead of asking. "500 messages a month" resets monthly — state it as an assumption, don\'t ask. Questions are only for facts that change the structure and can\'t be guessed.\n- Skip anything they already told you. A good message to answer is a nod, not homework.\n- Never announce what you\'ll do next ("once I get those three, I\'ll restate…") — just work the current topic.\n- The restate (step 5) is the safety net: wrong assumptions get caught there cheaply, which is what makes fewer questions safe.\n\n## Shape: collect, then decide\n\nWork the four steps below in order, one at a time — each says when it\'s done. While collecting, make only the small calls each step allows; leave the big ones (marked ↦ Decide) for after.\n\n### Step 1 — Plans\n\n- What are the plans? ("Free, Pro $20/mo, Growth $50/mo")\n- Free tier? → a plan with no price that every new customer starts on automatically.\n- Anything bought *alongside* a plan rather than instead of it (packs, extra storage)? → note it ↦ Decide (add-on).\n- Enterprise tier? Ask. If it exists, model the base enterprise plan in the catalog, and tell the user: custom terms per customer (special prices, custom limits) are applied later when attaching, not modeled here.\n\nDone when you can list every plan they sell, including free, add-ons, and enterprise.\n\n### Step 2 — What each plan includes\n\nHave them describe what they charge for or limit, in their own words. Pick a type per feature:\n\n| They say | Feature type |\n|---|---|\n| "Pro has SSO" | boolean (on/off) |\n| "500 messages a month" | metered, resets |\n| "10 team members" | metered, no reset (held, not used up) |\n| "credits" / "tokens" / "wallet" | credit system — always, even if only one action uses it today. They will add more actions; a single mapped action is fine. Their app tracks the actions (chat, image), never the credit balance itself. |\n\nNote top-ups ("buy more when you run out", "auto-recharge") ↦ Decide (top-up placement).\n\n### Step 3 — How billing behaves\n\nThis step usually means explaining Autumn to the user in plain words. Do.\n\n- **Signup**: every new customer automatically gets the default plan — usually the free one. One default per group. A default can carry no prices at all — a "$0" plan with paid items (per-seat charges, prepaid packs) is a paid plan, not a default. If every plan bills something, there is no default; customers subscribe.\n- **Trials**: ask "does the trial need a card?" (guess from their motion — PLG usually no). Card → `free_trial` on the paid plan. No card → a **separate free trial plan** (`pro_trial`: no price, the paid plan\'s items, auto-enabled when everyone starts on it); the paid plan stays untouched, since a default plan can never be paid. For trial modeling details — trial-behavior on plans, read `references/plan.md` in the `autumn-concepts` skill.\n- **What is a plan attached to?** The customer, or each thing they own (workspace, project, site)? "Pro is $200 per workspace" → attached per entity. Pin this down — it changes everything downstream.\n- **Who uses each metered feature?** The customer as a whole, or each entity? If entities: one shared balance or separate ones? "Shared across…" → shared ↦ Decide (balances).\n\nDone when you know what attaches where, who consumes what, and how trials and signup work.\n\n### Step 4 — Licenses\n\nTriggered whenever the customer pays per unit of some entity — seats, workspaces, projects, sites, members: "each X is $10/month", "comes with 3 X". When you see one, always ask: **what does one X come with?** Never skip this because the user didn\'t say "seat". For modeling a license — the concept: child plan, license link, customize, read `references/licenses.md` in the `autumn-concepts` skill.\n\n- Nothing of its own ("$10 per seat", just a count) → a per-unit priced item on the plan. No entities. The common case.\n- The unit gets something of its own ("each seat gets 100 credits", "every workspace has its own allowance") → a **license**: a small plan of its own that the parent plan hands out per unit.\n- Units must be assigned, reassigned, or sit empty → also licenses. Rare — confirm they need it.\n\nFor deciding between a per-unit item, licenses, and entity-attached plans for a countable paid unit, read `references/fork-licenses.md`.\n\n### Before deciding: restate\n\nOnly after your questions are answered — never announce it in advance, and never restate facts nobody has confirmed yet. One short message: the plans, what\'s metered, what attaches where, who shares what. Let the user correct it. A wrong fact here is much cheaper than a wrong structure later.\n\nIf the user gave you everything up front and you asked nothing, skip the separate restate — the structure message (checklist item 5) does that job.\n\n## Decide\n\nResolve these with all facts in hand. Each has a default — when the facts genuinely don\'t settle it, show both options in one line each and ask.\n\n**Variant or separate plan?**\nA variant can change the price, swap items in or out, and change the trial — nothing else.\n\n- "Pro monthly / Pro annual, same features" → variant. (Annual usually still resets allowances monthly — billing and reset intervals are independent. Confirm.)\n- Volume buckets/tiers — one question decides: **is each bucket the subscription itself (→ a variant per bucket) or a purchase on top of one (→ a prepaid item)?**\n- Different features per tier → separate plans. One plan per tier is normal, not a smell.\n\nFor deciding whether volume buckets/tiers are variants of the plan or one prepaid volume-tiered item — the tells, the trap, when variants are forced, read `references/fork-variants.md`.\n\n**Add-on, or part of the plan?** Two independent questions: is the purchase priced/bundled per plan (→ item on each plan) or one offer across plans (→ one add-on plan, sizes as tiers on its prepaid item)? And does a plan exist at the level its balance is shared at (→ item there) or are all plans entity-attached (→ a customer-level add-on is forced)? Auto-recharge needs the prepaid item to exist — add it in Shape, it\'s structural.\n\nFor modeling packs, top-ups, or any purchase bought on top of a plan — add-on vs plan item, and what level it sits at, read `references/fork-addon.md`.\n\n**Where do balances and purchases live?**\nThe rule: **purchases and balance at the customer; usage tracking and caps at the entity.** Grants "shared across…" entities → pooled (`pooled: true` on the item); separate per-entity balances → no pooling; overage stays on the entity\'s plan either way.\n\nFor deciding where balances and purchases live — pooled grants, per-entity balances, customer-level packs, overage placement, read `references/fork-pooled.md`.\n\n**Groups?**\nCan one customer hold two plans at once from different lines (a support plan AND a sales plan)? → one group per line. Within a group, attaching a plan replaces the current one; that\'s what makes upgrades work.\n\n**One-off?**\n"Lifetime deal", "one-time pack" → a plan with a one-off price: single invoice, no subscription, balances never reset.\n\n## Check\n\nCompare the structure against the shapes in `references/cases.md`. If their pricing matches a known shape but your structure differs, either say why or fix it.\n\nShortcuts that are usually wrong — catch yourself before Show:\n\n| You\'re thinking | Check first |\n|---|---|\n| "seats + credits → licenses" | Are the credits per seat, or one shared pot? Shared → per-unit seats + pooled balance, no licenses. |\n| "annual pricing → separate plan" | Same features? → variant. |\n| "tiers → one plan with tiers" | Does overage or anything else differ per tier? → plan per tier. |\n| "they said credits but it\'s one action → plain meter" | Credits are always a credit system. |\n| "packs belong on the plan" | Are they shared across entities or plans? → add-on. |\n| "everyone starts on a Pro trial → trial + auto-enable on Pro" | A default plan can never be paid. → separate free `pro_trial` plan (Pro\'s items, no price, auto-enabled); Pro untouched. |\n| "several top-up sizes → one add-on plan per size" | Do the sizes differ only in quantity and price? → volume tiers on one prepaid item, one add-on plan. |\n| "the pack is priced per plan → an add-on per plan" | Per-plan pricing IS plan differentiation → a prepaid item on each base plan, no add-ons. Add-ons are for one offer shared across plans, or when no plan exists at the shared level. |\n| "the seat differs per plan → one seat plan per parent" | ONE child plan carrying the mainline take; each differing parent\'s license carries its own diff via `customize`. Never a `<parent>_seat` plan per parent. |\n\nFor checking the derived structure against known-good shapes, read `references/cases.md`.\n\n## Show\n\nPresent the structure in the "Showing the catalog" format below — the same one used for every catalog display, catalog inside a fenced code block. Numbers you don\'t have yet stay open, never invented: write the line without them ("AI messages per month — amount TBD"). Structure notes go in parentheses on the line they describe. The whole message:\n\n````\nHere\'s the structure I\'d build:\n\n```\nFeatures: AI credits (credit system — chat and image messages draw from it) · SSO (on/off)\n\nFree — no price, everyone starts here\n - 100 AI credits per month\nPro — $20/month, or annual (same features)\n - AI credits per month — amount TBD\n - SSO\nCredit pack (add-on) — price TBD, shared across all workspaces\n```\n\nI assumed: credits reset monthly, no rollover. Anything wrong?\n````\n\nList every assumption. Get a clear yes — "sounds good" without reading is not a yes. But if the user already told you to go ahead without review ("no need to ask", "just build it"), show the structure and keep going — don\'t stop to wait. If a late fact changes the structure, redo the affected decision, update the structure, and show what changed in one line.\n\n## Fill\n\nStructure agreed — now finish it. Four moves, in order.\n\n**1 — Fill in what\'s known.** Everything the user already said goes straight into the draft. Never re-ask a confirmed fact.\n\n**2 — Ask for missing essentials.** Values with no sane default: base prices, included amounts, per-unit prices, tier boundaries, trial length. Never invent one — a missing number is a question. Batch per the how-to-ask rules.\n\n**3 — Sweep the options.** Ask each of these **once for the whole catalog**, in plain words — never item by item. The answer distributes to every item it touches ("carry over on both plans, or just Growth?" only if they hint at a difference). Raise a bucket only when the structure makes it relevant; skip anything already answered.\n\n- **Carry-over** — any allowance that resets: "Should unused messages carry over month to month, or reset clean?"\n- **Running out** — any metered feature: "When they run out — hard stop, or keep going and bill the extra?" (often settled in Shape; skip if so)\n- **Top-ups** — credits present or buying-more mentioned: "Can they buy more before the reset?" → a one-off prepaid item\n- **Guardrails** — any plan with overage or heavy usage: "Any caps on how fast or how far usage can run — like a daily limit, or a ceiling on overage? Want customers warned as they approach limits?" → plan-level billing controls\n- **Anything else on/off** — always, it\'s cheap: "Any other on/off differences between plans — SSO, priority support, API access?"\n\nThe sweep exists so the user hears what\'s configurable without being marched through every item. Behind it, check every knob yourself — this list is internal, never show it:\n\n- per plan: base price · trial (length, unit, card — all explicit) · default plan · group · billing controls (guardrails answered → `billingControls` on the plan)\n- per item: billing method · included · price or tiers (tier behavior explicit) · reset · rollover · pooled (if Shape chose shared balances) · purchase caps · the one-off item auto-recharge needs\n\nLanding a guardrail answer means picking the right control — windowed cap vs overage ceiling vs alert vs allow-past-balance vs auto-recharge are different knobs with different fields. What each one is, the three overage knobs, and the plan→customer inheritance are the `autumn-concepts` skill\'s billing-controls reference — read it before writing `billingControls`. The flow rules here:\n\n- Plan-level controls are defaults every subscriber inherits — the right home for anything true of the whole plan ("free users max 20 emails/day"). Per-customer exceptions are a billing/customer operation, not catalog work.\n- A cap stated alongside an allowance ("1,000 a month but never more than 20 a day") is a usage limit on the plan, not a second item or a smaller allowance.\n- "Track it but don\'t bill it" / "let them run over, we\'ll invoice manually" → overage knobs on the plan, not a $0 price.\n- Auto-recharge needs its one-off prepaid item (already on the per-item list) AND the `autoTopups` control.\n\n**4 — Propose, then finalize.** One message: the full catalog in the format below, then "I assumed:" listing every knob you defaulted. Fold corrections in. Then write the config — and before saving, re-read every amount in it: dollars, never cents ($600 is `600`, not `60000`). Validate with `atmn push` (a preview; nothing is applied until `--yes`), fix what it flags, and show the final catalog — same format, no assumptions list. **Done means the config is written and valid — a summary is not done.**\n\n### Showing the catalog\n\nUse **exactly this format** every time you show the catalog — the Show structure message, the Fill proposal, and the done message alike. Never a markdown table, never a different layout per message. Send it as a fenced code block (```) so the indentation survives markdown rendering — plan names on bare lines otherwise get folded into the previous plan\'s list. It\'s the same grammar the dashboard renders. Features first with their kind, then plans, one line per item:\n\n```\nFeatures: AI messages (usage, resets) · Seats (held, not used up) · Credits (credit system — 1 message = 1 credit) · SSO (on/off)\n\nPro — $20/month\n - 500 AI messages per month\n · unused carry over, up to 500, for 1 month\n - 500 AI messages per month, then $0.01 per message\n - $10 per 1,000 credits\n - 5,000 credits for $50 per month\n - 3 seats included, then $10 per seat\n - $18 - $15 per seat\n - Unlimited projects\n - SSO\n - 100 credits per seat per month\nTeam — $500/month\n - 5 seats included, then $40 per seat per month\n each seat gets:\n · 100 summaries per month\n · SSO\nCredit pack (add-on) — $10 for 1,000 credits, buy anytime\n```\n\nThe Features line names every feature and its kind in plain words: `(usage, resets)` for consumable meters, `(held, not used up)` for non-consumable ones like seats, `(credit system — …)` with its action mappings, `(on/off)` for boolean. Item lines, top to bottom: plain allowance · allowance with overage · pure usage price (per billing unit) · prepaid bucket · included + prepaid per-unit · tiered rate shown first-to-last · unlimited · boolean (name only, never "enabled") · per-entity grant · one-off add-on. Numbers get commas; "then …" says what happens after the included runs out; annual variants go inline ("or $200/year — messages still reset monthly").\n\nConfigured item properties — carry-over, purchase caps, top-up behavior — are never their own `-` lines: indent them under the item they belong to as `·` lines, one behavior each, so the hierarchy is visible. Only show what\'s configured; defaults (like usage simply stopping when no overage price is set) get no line.\n\nBilling controls follow the same rule: `·` lines under the item they guard, in plain words — `· max 200 per day` · `· overage capped at 20% over` · `· overage tracked, not billed` · `· warned at 80%` · `· auto-buys 1,000 credits when below 100`. Never the schema names (usage_limits, spend_limits) in user-facing text.\n\n### Config gotchas\n\n- Amounts are plain dollars everywhere — base prices, tier `flatAmount`s, unit prices: $20 is `20`, never `2000`, and a $100 tier is `flatAmount: 100`, never `10000`. Re-check every number before writing; cents is the most common wrong config.\n- A default/auto-enabled plan can\'t be paid: no base price, no paid items.\n- A prepaid quantity **includes** the included amount, and so does each tier\'s `to` — the first tier\'s `to` must exceed `included`.\n- `billingUnits` rounds usage **up** when billing.\n- Set explicitly, never lean on defaults: `billingMethod` and `interval` on every item price, `tierBehavior` on tiered prices, and every trial field (`durationLength`, `durationType`, `cardRequired`, `onEnd`). Explicit defaults cause no spurious diffs.\n- Volume tiers charge the flat amount of the reached tier and are prepaid-only; graduated (the default) sums across brackets.\n- Rollover needs a resetting allowance; `max` and `maxPercentage` are mutually exclusive; `expiryDurationType` is required.\n- `billingControls` is a plain object on the plan with camelCase fields like the rest of the config (`featureId`, `overageLimit`); each control list replaces wholesale on update.\n- Plan `config` (`ignorePastDue`, `anchorToMonthStart`) is unmanaged when omitted. Once stated it is the whole set: a flag left out is off, so `config: {}` turns every flag off.\n- Pooled balances are config: `pooled: true` on the entity plan\'s item. Concluding "shared across workspaces" in Shape and then omitting the flag is the classic miss.\n- Pooled grant + overage = two items on the plan: the pooled grant carries no price; a separate usage-priced item (`included: 0`) carries the overage. A pooled item can\'t itself be usage-priced.\n- Don\'t write `proration` — leave it out and take server defaults.\n- Trial end behavior is `freeTrial.onEnd`: `"bill"` (default) charges when the trial ends, `"revert"` expires it and restores the previous plan.\n- Every plan row carries `versionSlug` and `active`, and every variant row and license link `versionSlug`. A plan\'s rows are its versions — exactly one `active: true` — and a version row left out of `plans` is deleted. How rows express versions, renames and drafts: `references/atmn.md`.\n\nThe config uses the builders `feature`, `plan`, `variant`, `license` as plain function calls with object arguments; items are plain objects inside a plan, and the file\'s default export is `atmn({...})` naming every collection. Never guess other functions or fields; the full shapes are in `references/atmn.md`.\n\n```ts\nimport { atmn, feature, plan } from "atmn";\n\nexport const credits = feature({\n featureId: "credits",\n name: "Credits",\n type: "credit_system",\n creditSchema: [{ meteredFeatureId: "messages", creditCost: 1 }],\n});\n\nexport const pro = plan({\n planId: "pro",\n versionSlug: "v1",\n active: true,\n name: "Pro",\n price: { amount: 20, interval: "month" },\n items: [\n { featureId: credits.featureId, included: 500, reset: { interval: "month" } },\n ],\n});\n\nexport default atmn({ features: [credits], plans: [pro] });\n```\n\nPattern deep-dives, split one file per pattern under `references/` — read the matching one when filling that pattern\'s details:\n\nFor usage overage pricing, read `references/usage-based-pricing.md`.\nFor prepaid quantities (seats, packs, buckets), read `references/prepaid-pricing.md`.\nFor volume-tiered prices, read `references/volume-based-tiers.md`.\nFor $X per unit, read `references/per-unit-pricing.md`.\nFor billing vs reset intervals, read `references/recurring.md`.\nFor one-off purchases / top-ups, read `references/one-off-purchases.md`.\nFor auto-recharge, read `references/auto-top-ups.md`.\nFor carry-over, read `references/rollovers.md`.\nFor trial details, read `references/trials.md`.\nFor entity plans and license attach flows, read `references/entity-plans.md`.\nFor credit schemas and mappings, read `references/credit-systems.md`.\nFor default plans / auto-enable, read `references/free-plans.md`.\nFor add-on balance stacking, read `references/add-ons.md`.\nFor what variants can change, read `references/plan-variants.md`.\n\n## Conduct\n\n- In an existing config, match its patterns: if sibling plans carry their prepaid purchases as items, the new plan does too — don\'t introduce a different structure for the same kind of thing.\n- Stable lowercase IDs with underscores: `pro_plan`, `chat_messages`.\n- `entityFeatureId` is deprecated. Never mention or use it unless the user\'s existing config already has it.\n- Per-unit pricing pairs a base fee with the per-unit item ("$X/seat" plans still have a base price, even $0).\n- Speak plainly: "plans", "what\'s included", "extra usage". Schema words stay in the config — say "carry over" not "rollover", "shared across workspaces" not "pooled", "paid upfront" / "billed at month end" not "prepaid" / "usage_based".\n- Never volunteer what Autumn can or can\'t do. Don\'t offer options Autumn can\'t model, and don\'t explain limitations unprompted — only address one when the user directly asks to model that specific thing, and even then lead with the closest thing that works.\n- Start simple: the most important features first, confirm before adding more.\n\nBefore finishing: re-check the STRICT RULES at the top against the config you wrote.\n\n## Catalog operations\n\nFor using atmn, autumn.config.ts, or headless push flows, read `references/atmn.md`.\n\nFor changing an existing catalog: previewing, versioning, migrations, variant propagation, read `references/catalog-update.md`.\n\nFor creating or changing coupons, promo codes, feature grants, or referral programs, read `references/rewards.md`.',
41
+ '---\nname: autumn-catalog\ndescription: Modeling a user\'s pricing into an Autumn catalog — deciding the structure (plans, variants, add-ons, licenses, credit systems, pooled balances) before writing config, then filling in the numbers. Use when the user describes their pricing or asks to model, change, or push a catalog.\nversion: 2.0.120\n---\n\n# Catalog\n\nBefore using this skill, first load the `autumn-concepts` skill — it defines Autumn\'s data model — features, plans, plan items, balances — which every modeling decision builds on.\n\n## STRICT RULES — re-read before every config write\n\n1. **Amounts are in major units (e.g. dollars), never minor units (cents).** $180/month is `amount: 180`. $0.01 per credit is `amount: 0.01`. If any amount you wrote is 100× the user\'s number, it is wrong.\n2. **Never invent a price, limit, or plan name** — a missing number is a question.\n3. **One definition per real thing.** One feature per resource, one child plan per license pattern (parents customize their license, never get their own copy), one add-on per offer (sizes are tiers, not plans).\n\n**Building, or iterating on a live catalog?** What matters is whether customers are on these plans — not whether a config file exists. Still setting up (even across sessions, with a half-built `autumn.config.ts`) → the workflow below; edit the draft freely. Already running Autumn with customers, now changing prices/plans → that\'s an update with real stakes (versioning, migrations, grandfathering) — read `references/catalog-update.md` first. Unsure → check for customers (`atmn pull` / the org) or ask.\n\nTurning pricing into a catalog is two jobs:\n\n- **Shape** — decide the structure: which plans exist, what\'s a variant, what\'s an add-on, where balances live. Decided by relationships in their pricing, not by amounts.\n- **Fill** — put in the numbers and per-item details, then write and validate the config.\n\nDo Shape fully before Fill. Take numbers whenever the user mentions them, but never chase numbers during Shape — the one exception is "is this the same on every plan?", which is a structure question.\n\n## Progress (what the user sees)\n\nCopy this checklist into your first message and keep it up to date. It is the **only** structure the user ever sees — never say "step", "pass", "decide", or "fork" to them. If you skip an item, say why in one line.\n\n- [ ] 1 Your plans and prices\n- [ ] 2 What\'s included in each plan\n- [ ] 3 How billing behaves (signup, trials, limits)\n- [ ] 4 Licenses — paid seats, workspaces, projects (if any)\n- [ ] 5 Structure agreed\n- [ ] 6 Config written and checked\n\n## How to ask\n\n- One topic per message. Two questions at most, and only if both belong to that topic. Never mix topics in one message.\n- If one ambiguity changes which other questions apply, resolve it first before asking those.\n- Attach your guess to each question — "does the trial need a card? I\'d guess no" — a wrong guess gets corrected faster than a blank gets answered.\n- Assert the obvious instead of asking. "500 messages a month" resets monthly — state it as an assumption, don\'t ask. Questions are only for facts that change the structure and can\'t be guessed.\n- Skip anything they already told you. A good message to answer is a nod, not homework.\n- Never announce what you\'ll do next ("once I get those three, I\'ll restate…") — just work the current topic.\n- The restate (step 5) is the safety net: wrong assumptions get caught there cheaply, which is what makes fewer questions safe.\n\n## Shape: collect, then decide\n\nWork the four steps below in order, one at a time — each says when it\'s done. While collecting, make only the small calls each step allows; leave the big ones (marked ↦ Decide) for after.\n\n### Step 1 — Plans\n\n- What are the plans? ("Free, Pro $20/mo, Growth $50/mo")\n- Free tier? → a plan with no price that every new customer starts on automatically.\n- Anything bought *alongside* a plan rather than instead of it (packs, extra storage)? → note it ↦ Decide (add-on).\n- Enterprise tier? Ask. If it exists, model the base enterprise plan in the catalog, and tell the user: custom terms per customer (special prices, custom limits) are applied later when attaching, not modeled here.\n\nDone when you can list every plan they sell, including free, add-ons, and enterprise.\n\n### Step 2 — What each plan includes\n\nHave them describe what they charge for or limit, in their own words. Pick a type per feature:\n\n| They say | Feature type |\n|---|---|\n| "Pro has SSO" | boolean (on/off) |\n| "500 messages a month" | metered, resets |\n| "10 team members" | metered, no reset (held, not used up) |\n| "credits" / "tokens" / "wallet" | credit system — always, even if only one action uses it today. They will add more actions; a single mapped action is fine. Their app tracks the actions (chat, image), never the credit balance itself. |\n\nNote top-ups ("buy more when you run out", "auto-recharge") ↦ Decide (top-up placement).\n\n### Step 3 — How billing behaves\n\nThis step usually means explaining Autumn to the user in plain words. Do.\n\n- **Signup**: every new customer automatically gets the default plan — usually the free one. One default per group. A default can carry no prices at all — a "$0" plan with paid items (per-seat charges, prepaid packs) is a paid plan, not a default. If every plan bills something, there is no default; customers subscribe.\n- **Trials**: ask "does the trial need a card?" (guess from their motion — PLG usually no). Card → `free_trial` on the paid plan. No card → a **separate free trial plan** (`pro_trial`: no price, the paid plan\'s items, auto-enabled when everyone starts on it); the paid plan stays untouched, since a default plan can never be paid. For trial modeling details — trial-behavior on plans, read `references/plan.md` in the `autumn-concepts` skill.\n- **What is a plan attached to?** The customer, or each thing they own (workspace, project, site)? "Pro is $200 per workspace" → attached per entity. Pin this down — it changes everything downstream.\n- **Who uses each metered feature?** The customer as a whole, or each entity? If entities: one shared balance or separate ones? "Shared across…" → shared ↦ Decide (balances).\n\nDone when you know what attaches where, who consumes what, and how trials and signup work.\n\n### Step 4 — Licenses\n\nTriggered whenever the customer pays per unit of some entity — seats, workspaces, projects, sites, members: "each X is $10/month", "comes with 3 X". When you see one, always ask: **what does one X come with?** Never skip this because the user didn\'t say "seat". For modeling a license — the concept: child plan, license link, customize, read `references/licenses.md` in the `autumn-concepts` skill.\n\n- Nothing of its own ("$10 per seat", just a count) → a per-unit priced item on the plan. No entities. The common case.\n- The unit gets something of its own ("each seat gets 100 credits", "every workspace has its own allowance") → a **license**: a small plan of its own that the parent plan hands out per unit.\n- Units must be assigned, reassigned, or sit empty → also licenses. Rare — confirm they need it.\n\nFor deciding between a per-unit item, licenses, and entity-attached plans for a countable paid unit, read `references/fork-licenses.md`.\n\n### Before deciding: restate\n\nOnly after your questions are answered — never announce it in advance, and never restate facts nobody has confirmed yet. One short message: the plans, what\'s metered, what attaches where, who shares what. Let the user correct it. A wrong fact here is much cheaper than a wrong structure later.\n\nIf the user gave you everything up front and you asked nothing, skip the separate restate — the structure message (checklist item 5) does that job.\n\n## Decide\n\nResolve these with all facts in hand. Each has a default — when the facts genuinely don\'t settle it, show both options in one line each and ask.\n\n**Variant or separate plan?**\nA variant can change the price, swap items in or out, and change the trial — nothing else.\n\n- "Pro monthly / Pro annual, same features" → variant. (Annual usually still resets allowances monthly — billing and reset intervals are independent. Confirm.)\n- Volume buckets/tiers — one question decides: **is each bucket the subscription itself (→ a variant per bucket) or a purchase on top of one (→ a prepaid item)?**\n- Different features per tier → separate plans. One plan per tier is normal, not a smell.\n\nFor deciding whether volume buckets/tiers are variants of the plan or one prepaid volume-tiered item — the tells, the trap, when variants are forced, read `references/fork-variants.md`.\n\n**Add-on, or part of the plan?** Two independent questions: is the purchase priced/bundled per plan (→ item on each plan) or one offer across plans (→ one add-on plan, sizes as tiers on its prepaid item)? And does a plan exist at the level its balance is shared at (→ item there) or are all plans entity-attached (→ a customer-level add-on is forced)? Auto-recharge needs the prepaid item to exist — add it in Shape, it\'s structural.\n\nFor modeling packs, top-ups, or any purchase bought on top of a plan — add-on vs plan item, and what level it sits at, read `references/fork-addon.md`.\n\n**Where do balances and purchases live?**\nThe rule: **purchases and balance at the customer; usage tracking and caps at the entity.** Grants "shared across…" entities → pooled (`pooled: true` on the item); separate per-entity balances → no pooling; overage stays on the entity\'s plan either way.\n\nFor deciding where balances and purchases live — pooled grants, per-entity balances, customer-level packs, overage placement, read `references/fork-pooled.md`.\n\n**Groups?**\nCan one customer hold two plans at once from different lines (a support plan AND a sales plan)? → one group per line. Within a group, attaching a plan replaces the current one; that\'s what makes upgrades work.\n\n**One-off?**\n"Lifetime deal", "one-time pack" → a plan with a one-off price: single invoice, no subscription, balances never reset.\n\n## Check\n\nCompare the structure against the shapes in `references/cases.md`. If their pricing matches a known shape but your structure differs, either say why or fix it.\n\nShortcuts that are usually wrong — catch yourself before Show:\n\n| You\'re thinking | Check first |\n|---|---|\n| "seats + credits → licenses" | Are the credits per seat, or one shared pot? Shared → per-unit seats + pooled balance, no licenses. |\n| "annual pricing → separate plan" | Same features? → variant. |\n| "tiers → one plan with tiers" | Does overage or anything else differ per tier? → plan per tier. |\n| "they said credits but it\'s one action → plain meter" | Credits are always a credit system. |\n| "packs belong on the plan" | Are they shared across entities or plans? → add-on. |\n| "everyone starts on a Pro trial → trial + auto-enable on Pro" | A default plan can never be paid. → separate free `pro_trial` plan (Pro\'s items, no price, auto-enabled); Pro untouched. |\n| "several top-up sizes → one add-on plan per size" | Do the sizes differ only in quantity and price? → volume tiers on one prepaid item, one add-on plan. |\n| "the pack is priced per plan → an add-on per plan" | Per-plan pricing IS plan differentiation → a prepaid item on each base plan, no add-ons. Add-ons are for one offer shared across plans, or when no plan exists at the shared level. |\n| "the seat differs per plan → one seat plan per parent" | ONE child plan carrying the mainline take; each differing parent\'s license carries its own diff via `customize`. Never a `<parent>_seat` plan per parent. |\n\nFor checking the derived structure against known-good shapes, read `references/cases.md`.\n\n## Show\n\nPresent the structure in the "Showing the catalog" format below — the same one used for every catalog display, catalog inside a fenced code block. Numbers you don\'t have yet stay open, never invented: write the line without them ("AI messages per month — amount TBD"). Structure notes go in parentheses on the line they describe. The whole message:\n\n````\nHere\'s the structure I\'d build:\n\n```\nFeatures: AI credits (credit system — chat and image messages draw from it) · SSO (on/off)\n\nFree — no price, everyone starts here\n - 100 AI credits per month\nPro — $20/month, or annual (same features)\n - AI credits per month — amount TBD\n - SSO\nCredit pack (add-on) — price TBD, shared across all workspaces\n```\n\nI assumed: credits reset monthly, no rollover. Anything wrong?\n````\n\nList every assumption. Get a clear yes — "sounds good" without reading is not a yes. But if the user already told you to go ahead without review ("no need to ask", "just build it"), show the structure and keep going — don\'t stop to wait. If a late fact changes the structure, redo the affected decision, update the structure, and show what changed in one line.\n\n## Fill\n\nStructure agreed — now finish it. Four moves, in order.\n\n**1 — Fill in what\'s known.** Everything the user already said goes straight into the draft. Never re-ask a confirmed fact.\n\n**2 — Ask for missing essentials.** Values with no sane default: base prices, included amounts, per-unit prices, tier boundaries, trial length. Never invent one — a missing number is a question. Batch per the how-to-ask rules.\n\n**3 — Sweep the options.** Ask each of these **once for the whole catalog**, in plain words — never item by item. The answer distributes to every item it touches ("carry over on both plans, or just Growth?" only if they hint at a difference). Raise a bucket only when the structure makes it relevant; skip anything already answered.\n\n- **Carry-over** — any allowance that resets: "Should unused messages carry over month to month, or reset clean?"\n- **Running out** — any metered feature: "When they run out — hard stop, or keep going and bill the extra?" (often settled in Shape; skip if so)\n- **Top-ups** — credits present or buying-more mentioned: "Can they buy more before the reset?" → a one-off prepaid item\n- **Guardrails** — any plan with overage or heavy usage: "Any caps on how fast or how far usage can run — like a daily limit, or a ceiling on overage? Want customers warned as they approach limits?" → plan-level billing controls\n- **Anything else on/off** — always, it\'s cheap: "Any other on/off differences between plans — SSO, priority support, API access?"\n\nThe sweep exists so the user hears what\'s configurable without being marched through every item. Behind it, check every knob yourself — this list is internal, never show it:\n\n- per plan: base price · trial (length, unit, card — all explicit) · default plan · group · billing controls (guardrails answered → `billingControls` on the plan)\n- per item: billing method · included · price or tiers (tier behavior explicit) · reset · rollover · pooled (if Shape chose shared balances) · purchase caps · the one-off item auto-recharge needs\n\nLanding a guardrail answer means picking the right control — windowed cap vs overage ceiling vs alert vs allow-past-balance vs auto-recharge are different knobs with different fields. What each one is, the three overage knobs, and the plan→customer inheritance are the `autumn-concepts` skill\'s billing-controls reference — read it before writing `billingControls`. The flow rules here:\n\n- Plan-level controls are defaults every subscriber inherits — the right home for anything true of the whole plan ("free users max 20 emails/day"). Per-customer exceptions are a billing/customer operation, not catalog work.\n- A cap stated alongside an allowance ("1,000 a month but never more than 20 a day") is a usage limit on the plan, not a second item or a smaller allowance.\n- "Track it but don\'t bill it" / "let them run over, we\'ll invoice manually" → overage knobs on the plan, not a $0 price.\n- Auto-recharge needs its one-off prepaid item (already on the per-item list) AND the `autoTopups` control.\n\n**4 — Propose, then finalize.** One message: the full catalog in the format below, then "I assumed:" listing every knob you defaulted. Fold corrections in. Then write the config — and before saving, re-read every amount in it: dollars, never cents ($600 is `600`, not `60000`). Validate with `atmn push` (a preview; nothing is applied until `--yes`), fix what it flags, and show the final catalog — same format, no assumptions list. **Done means the config is written and valid — a summary is not done.**\n\n### Showing the catalog\n\nUse **exactly this format** every time you show the catalog — the Show structure message, the Fill proposal, and the done message alike. Never a markdown table, never a different layout per message. Send it as a fenced code block (```) so the indentation survives markdown rendering — plan names on bare lines otherwise get folded into the previous plan\'s list. It\'s the same grammar the dashboard renders. Features first with their kind, then plans, one line per item:\n\n```\nFeatures: AI messages (usage, resets) · Seats (held, not used up) · Credits (credit system — 1 message = 1 credit) · SSO (on/off)\n\nPro — $20/month\n - 500 AI messages per month\n · unused carry over, up to 500, for 1 month\n - 500 AI messages per month, then $0.01 per message\n - $10 per 1,000 credits\n - 5,000 credits for $50 per month\n - 3 seats included, then $10 per seat\n - $18 - $15 per seat\n - Unlimited projects\n - SSO\n - 100 credits per seat per month\nTeam — $500/month\n - 5 seats included, then $40 per seat per month\n each seat gets:\n · 100 summaries per month\n · SSO\nCredit pack (add-on) — $10 for 1,000 credits, buy anytime\n```\n\nThe Features line names every feature and its kind in plain words: `(usage, resets)` for consumable meters, `(held, not used up)` for non-consumable ones like seats, `(credit system — …)` with its action mappings, `(on/off)` for boolean. Item lines, top to bottom: plain allowance · allowance with overage · pure usage price (per billing unit) · prepaid bucket · included + prepaid per-unit · tiered rate shown first-to-last · unlimited · boolean (name only, never "enabled") · per-entity grant · one-off add-on. Numbers get commas; "then …" says what happens after the included runs out; annual variants go inline ("or $200/year — messages still reset monthly").\n\nConfigured item properties — carry-over, purchase caps, top-up behavior — are never their own `-` lines: indent them under the item they belong to as `·` lines, one behavior each, so the hierarchy is visible. Only show what\'s configured; defaults (like usage simply stopping when no overage price is set) get no line.\n\nBilling controls follow the same rule: `·` lines under the item they guard, in plain words — `· max 200 per day` · `· overage capped at 20% over` · `· overage tracked, not billed` · `· warned at 80%` · `· auto-buys 1,000 credits when below 100`. Never the schema names (usage_limits, spend_limits) in user-facing text.\n\n### Config gotchas\n\n- Amounts are plain dollars everywhere — base prices, tier `flatAmount`s, unit prices: $20 is `20`, never `2000`, and a $100 tier is `flatAmount: 100`, never `10000`. Re-check every number before writing; cents is the most common wrong config.\n- A default/auto-enabled plan can\'t be paid: no base price, no paid items.\n- A prepaid quantity **includes** the included amount, and so does each tier\'s `to` — the first tier\'s `to` must exceed `included`.\n- `billingUnits` rounds usage **up** when billing.\n- Set explicitly, never lean on defaults: `billingMethod` and `interval` on every item price, `tierBehavior` on tiered prices, and every trial field (`durationLength`, `durationType`, `cardRequired`, `onEnd`). Explicit defaults cause no spurious diffs.\n- Volume tiers on usage-based items bill only usage past the included amount, at the rate of the tier total usage reaches, plus its flat fee. On prepaid items they charge every unit (included ones too) at the reached tier\'s rate and flat amount. Graduated (the default) sums across brackets. Volume can\'t be combined with threshold billing, and allocated volume can\'t have a tier-1 flat amount.\n- Rollover needs a resetting allowance; `max` and `maxPercentage` are mutually exclusive; `expiryDurationType` is required.\n- `billingControls` is a plain object on the plan with camelCase fields like the rest of the config (`featureId`, `overageLimit`); each control list replaces wholesale on update.\n- Plan `config` (`ignorePastDue`, `anchorToMonthStart`) is unmanaged when omitted. Once stated it is the whole set: a flag left out is off, so `config: {}` turns every flag off.\n- Pooled balances are config: `pooled: true` on the entity plan\'s item. Concluding "shared across workspaces" in Shape and then omitting the flag is the classic miss.\n- Pooled grant + overage = two items on the plan: the pooled grant carries no price; a separate usage-priced item (`included: 0`) carries the overage. A pooled item can\'t itself be usage-priced.\n- Don\'t write `proration` — leave it out and take server defaults.\n- Trial end behavior is `freeTrial.onEnd`: `"bill"` (default) charges when the trial ends, `"revert"` expires it and restores the previous plan.\n- Every plan row carries `versionSlug` and `active`, and every variant row and license link `versionSlug`. A plan\'s rows are its versions — exactly one `active: true` — and a version row left out of `plans` is deleted. How rows express versions, renames and drafts: `references/atmn.md`.\n\nThe config uses the builders `feature`, `plan`, `variant`, `license` as plain function calls with object arguments; items are plain objects inside a plan, and the file\'s default export is `atmn({...})` naming every collection. Never guess other functions or fields; the full shapes are in `references/atmn.md`.\n\n```ts\nimport { atmn, feature, plan } from "atmn";\n\nexport const credits = feature({\n featureId: "credits",\n name: "Credits",\n type: "credit_system",\n creditSchema: [{ meteredFeatureId: "messages", creditCost: 1 }],\n});\n\nexport const pro = plan({\n planId: "pro",\n versionSlug: "v1",\n active: true,\n name: "Pro",\n price: { amount: 20, interval: "month" },\n items: [\n { featureId: credits.featureId, included: 500, reset: { interval: "month" } },\n ],\n});\n\nexport default atmn({ features: [credits], plans: [pro] });\n```\n\nPattern deep-dives, split one file per pattern under `references/` — read the matching one when filling that pattern\'s details:\n\nFor usage overage pricing, read `references/usage-based-pricing.md`.\nFor prepaid quantities (seats, packs, buckets), read `references/prepaid-pricing.md`.\nFor volume-tiered prices, read `references/volume-based-tiers.md`.\nFor $X per unit, read `references/per-unit-pricing.md`.\nFor billing vs reset intervals, read `references/recurring.md`.\nFor one-off purchases / top-ups, read `references/one-off-purchases.md`.\nFor auto-recharge, read `references/auto-top-ups.md`.\nFor carry-over, read `references/rollovers.md`.\nFor trial details, read `references/trials.md`.\nFor entity plans and license attach flows, read `references/entity-plans.md`.\nFor credit schemas and mappings, read `references/credit-systems.md`.\nFor default plans / auto-enable, read `references/free-plans.md`.\nFor add-on balance stacking, read `references/add-ons.md`.\nFor what variants can change, read `references/plan-variants.md`.\n\n## Conduct\n\n- In an existing config, match its patterns: if sibling plans carry their prepaid purchases as items, the new plan does too — don\'t introduce a different structure for the same kind of thing.\n- Stable lowercase IDs with underscores: `pro_plan`, `chat_messages`.\n- `entityFeatureId` is deprecated. Never mention or use it unless the user\'s existing config already has it.\n- Per-unit pricing pairs a base fee with the per-unit item ("$X/seat" plans still have a base price, even $0).\n- Speak plainly: "plans", "what\'s included", "extra usage". Schema words stay in the config — say "carry over" not "rollover", "shared across workspaces" not "pooled", "paid upfront" / "billed at month end" not "prepaid" / "usage_based".\n- Never volunteer what Autumn can or can\'t do. Don\'t offer options Autumn can\'t model, and don\'t explain limitations unprompted — only address one when the user directly asks to model that specific thing, and even then lead with the closest thing that works.\n- Start simple: the most important features first, confirm before adding more.\n\nBefore finishing: re-check the STRICT RULES at the top against the config you wrote.\n\n## Catalog operations\n\nFor using atmn, autumn.config.ts, or headless push flows, read `references/atmn.md`.\n\nFor changing an existing catalog: previewing, versioning, migrations, variant propagation, read `references/catalog-update.md`.\n\nFor creating or changing coupons, promo codes, feature grants, or referral programs, read `references/rewards.md`.',
42
42
  references: [
43
43
  {
44
44
  path: "references/usage-based-pricing.md",
@@ -53,7 +53,7 @@ export const SKILLS: readonly BundledSkill[] = [
53
53
  {
54
54
  path: "references/volume-based-tiers.md",
55
55
  contents:
56
- '## Volume-Based Tiers\n\nVolume-based pricing uses tiers to determine a single flat charge based on the total usage volume. Unlike [graduated pricing](/documentation/modelling-pricing/graduated-pricing), where each tier has its own rate, volume-based pricing charges a single flat amount based on which tier the total usage falls into.\n\n> **Example** <br />\n> A data platform charges:\n> - 0–1,000 records: $100 flat\n> - 1,001–10,000 records: $500 flat\n> - 10,001+: $1,000 flat\n>\n> A customer who processes 15,000 records falls into the 10,001+ tier and pays a flat **$1,000**\n>\n> Compare this to graduated pricing, where each tier is charged separately and summed together\n\n## Setting up\n\nUse the `tiers` array with `tierBehavior: \'volume\'` on a plan item price:\n\n```ts autumn.config.ts\nimport { atmn, feature, plan } from "atmn";\n\nexport const records = feature({\n featureId: "records",\n name: "Records Processed",\n type: "metered",\n consumable: true,\n});\n\nexport const pro = plan({\n planId: "pro",\n versionSlug: "v1",\n active: true,\n name: "Pro",\n price: { amount: 50, interval: "month" },\n items: [\n {\n featureId: records.featureId,\n reset: { interval: "month" },\n price: {\n tiers: [\n { to: 1000, flatAmount: 100 },\n { to: 10000, flatAmount: 500 },\n { to: "inf", flatAmount: 1000 },\n ],\n tierBehavior: "volume",\n billingMethod: "prepaid",\n interval: "month",\n },\n },\n ],\n});\n\nexport default atmn({ features: [records], plans: [pro] });\n```\n\nPreview with `atmn push`, then apply with `atmn push --yes`.\n\n## How volume-based pricing works\n\nAutumn:\n\n1. Looks at the total volume for the feature\n2. Finds the tier the total falls into\n3. Charges the flat amount for that tier\n\nVolume tiers are prepaid-only.\n\n| Total volume | Matching tier | Charge |\n|-------------|---------------|--------|\n| 500 | 0–1,000 | **$100** |\n| 5,000 | 1,001–10,000 | **$500** |\n| 15,000 | 10,001+ | **$1,000** |\n\n## Tier configuration\n\nEach tier has the following fields:\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `to` | number or `"inf"` | The upper boundary of this tier |\n| `flatAmount` | number | Flat fee charged when the total volume falls in this tier (`flat_amount` over the API) |\n| `amount` | number | Optional per-unit price applied to the total volume when this tier is the matching tier |\n\nTiers must be in ascending order by `to`. The final tier should use `"inf"`.\n\n## Combining flat and per-unit amounts\n\nEach tier can include both `flatAmount` and `amount`: a fixed fee plus a per-unit charge when that tier is the matching tier. This is useful for combining a base fee with per-unit volume pricing.\n\n```ts\nprice: {\n tiers: [\n { to: 1000, amount: 0.10, flatAmount: 0 },\n { to: 10000, amount: 0.08, flatAmount: 50 },\n { to: "inf", amount: 0.05, flatAmount: 100 },\n ],\n tierBehavior: "volume",\n billingMethod: "prepaid",\n interval: "month",\n}\n```\n\nA customer with 5,000 records would pay: (5,000 × $0.08) + $50 = **$450**\n\n## Graduated vs volume-based\n\n| | Graduated | Volume-based |\n|---|-----------|--------------|\n| **Rate applied** | Each tier at its own rate | Single flat amount for the matching tier |\n| **Total charge** | Sum of each tier\'s charge | Flat amount of the matching tier |\n| **Best for** | Rewarding growth with lower marginal rates | Simpler pricing with volume discounts |\n\nSee [Graduated Pricing](/documentation/modelling-pricing/graduated-pricing) for the alternative model.',
56
+ '## Volume-Based Tiers\n\nVolume-based pricing uses tiers to determine a single flat charge based on the total usage volume. Unlike [graduated pricing](/documentation/modelling-pricing/graduated-pricing), where each tier has its own rate, volume-based pricing charges a single flat amount based on which tier the total usage falls into.\n\n> **Example** <br />\n> A data platform charges:\n> - 0–1,000 records: $100 flat\n> - 1,001–10,000 records: $500 flat\n> - 10,001+: $1,000 flat\n>\n> A customer who processes 15,000 records falls into the 10,001+ tier and pays a flat **$1,000**\n>\n> Compare this to graduated pricing, where each tier is charged separately and summed together\n\n## Setting up\n\nUse the `tiers` array with `tierBehavior: \'volume\'` on a plan item price:\n\n```ts autumn.config.ts\nimport { atmn, feature, plan } from "atmn";\n\nexport const records = feature({\n featureId: "records",\n name: "Records Processed",\n type: "metered",\n consumable: true,\n});\n\nexport const pro = plan({\n planId: "pro",\n versionSlug: "v1",\n active: true,\n name: "Pro",\n price: { amount: 50, interval: "month" },\n items: [\n {\n featureId: records.featureId,\n reset: { interval: "month" },\n price: {\n tiers: [\n { to: 1000, flatAmount: 100 },\n { to: 10000, flatAmount: 500 },\n { to: "inf", flatAmount: 1000 },\n ],\n tierBehavior: "volume",\n billingMethod: "prepaid",\n interval: "month",\n },\n },\n ],\n});\n\nexport default atmn({ features: [records], plans: [pro] });\n```\n\nPreview with `atmn push`, then apply with `atmn push --yes`.\n\n## How volume-based pricing works\n\nAutumn:\n\n1. Looks at the total volume for the feature\n2. Finds the tier the total falls into\n3. Charges the flat amount for that tier\n\nVolume tiers work on prepaid and usage-based (pay-per-use or allocated) items. On usage-based items, volume bills only usage past the included amount, at the rate of the tier total usage reaches, plus its flat fee; at or below the included amount nothing is charged. On prepaid items, every unit (the included ones too) is charged at the rate of the tier the total lands in, plus its flat amount. Allocated items can\'t have a flat amount on the first tier. A plan with volume-priced allocated seats can\'t yet be attached to more than one entity on the same customer, because their seats would share one Stripe price.\n\nVolume tiers can\'t be combined with threshold billing.\n\n| Total volume | Matching tier | Charge |\n|-------------|---------------|--------|\n| 500 | 0–1,000 | **$100** |\n| 5,000 | 1,001–10,000 | **$500** |\n| 15,000 | 10,001+ | **$1,000** |\n\n## Tier configuration\n\nEach tier has the following fields:\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `to` | number or `"inf"` | The upper boundary of this tier |\n| `flatAmount` | number | Flat fee charged when the total volume falls in this tier (`flat_amount` over the API) |\n| `amount` | number | Optional per-unit price applied to the total volume when this tier is the matching tier |\n\nTiers must be in ascending order by `to`. The final tier should use `"inf"`.\n\n## Combining flat and per-unit amounts\n\nEach tier can include both `flatAmount` and `amount`: a fixed fee plus a per-unit charge when that tier is the matching tier. This is useful for combining a base fee with per-unit volume pricing.\n\n```ts\nprice: {\n tiers: [\n { to: 1000, amount: 0.10, flatAmount: 0 },\n { to: 10000, amount: 0.08, flatAmount: 50 },\n { to: "inf", amount: 0.05, flatAmount: 100 },\n ],\n tierBehavior: "volume",\n billingMethod: "prepaid",\n interval: "month",\n}\n```\n\nA customer with 5,000 records would pay: (5,000 × $0.08) + $50 = **$450**\n\n## Graduated vs volume-based\n\n| | Graduated | Volume-based |\n|---|-----------|--------------|\n| **Rate applied** | Each tier at its own rate | Single flat amount for the matching tier |\n| **Total charge** | Sum of each tier\'s charge | Flat amount of the matching tier |\n| **Best for** | Rewarding growth with lower marginal rates | Simpler pricing with volume discounts |\n\nSee [Graduated Pricing](/documentation/modelling-pricing/graduated-pricing) for the alternative model.',
57
57
  },
58
58
  {
59
59
  path: "references/per-unit-pricing.md",
@@ -117,6 +117,7 @@ export type Variant = {
117
117
  flatAmount?: number;
118
118
  }>;
119
119
  }>;
120
+ /** How tiers price usage. 'graduated' (default) charges each unit at the rate of the tier it falls in. 'volume' picks one tier from total usage. On usage-based items it bills only usage past the included amount, at the rate of the tier total usage reaches, plus its flat_amount. On prepaid items it charges every unit, the included ones too, at that tier's rate plus its flat_amount. */
120
121
  tierBehavior?: "graduated" | "volume";
121
122
  /** Billing interval. For consumable features, should match reset.interval. */
122
123
  interval:
@@ -348,6 +349,7 @@ export type Variant = {
348
349
  flatAmount?: number;
349
350
  }>;
350
351
  }>;
352
+ /** How tiers price usage. 'graduated' (default) charges each unit at the rate of the tier it falls in. 'volume' picks one tier from total usage. On usage-based items it bills only usage past the included amount, at the rate of the tier total usage reaches, plus its flat_amount. On prepaid items it charges every unit, the included ones too, at that tier's rate plus its flat_amount. */
351
353
  tierBehavior?: "graduated" | "volume";
352
354
  /** Billing interval. For consumable features, should match reset.interval. */
353
355
  interval:
@@ -732,6 +734,7 @@ export type Variant = {
732
734
  flatAmount?: number;
733
735
  }>;
734
736
  }>;
737
+ /** How tiers price usage. 'graduated' (default) charges each unit at the rate of the tier it falls in. 'volume' picks one tier from total usage. On usage-based items it bills only usage past the included amount, at the rate of the tier total usage reaches, plus its flat_amount. On prepaid items it charges every unit, the included ones too, at that tier's rate plus its flat_amount. */
735
738
  tierBehavior?: "graduated" | "volume";
736
739
  /** Billing interval. For consumable features, should match reset.interval. */
737
740
  interval:
@@ -24,6 +24,7 @@ type PriceLite = {
24
24
  interval?: string;
25
25
  intervalCount?: number;
26
26
  tiers?: unknown[];
27
+ tierBehavior?: string;
27
28
  } | null;
28
29
 
29
30
  type TrialLite = { durationLength?: number; durationType?: string } | null;
@@ -264,8 +265,10 @@ const formatInterval = (interval?: string, count?: number): string => {
264
265
 
265
266
  const formatPrice = (price: PriceLite | undefined): string => {
266
267
  if (price === null || price === undefined) return "Free";
267
- if (price.tiers !== undefined && price.tiers.length > 0)
268
- return `${price.tiers.length} tiers ${formatInterval(price.interval, price.intervalCount)}`;
268
+ if (price.tiers !== undefined && price.tiers.length > 0) {
269
+ const behavior = price.tierBehavior ? `${price.tierBehavior} ` : "";
270
+ return `${price.tiers.length} ${behavior}tiers ${formatInterval(price.interval, price.intervalCount)}`;
271
+ }
269
272
  return `${formatMoney(price.amount ?? 0)} ${formatInterval(price.interval, price.intervalCount)}`;
270
273
  };
271
274