Skip to content

feat(recipes): b2b_saas_ltv_v1 recipe assets + e2e [LTV-Po.2b]#132

Open
shaypal5 wants to merge 1 commit into
mainfrom
feat/lifecycle-recipe-assets
Open

feat(recipes): b2b_saas_ltv_v1 recipe assets + e2e [LTV-Po.2b]#132
shaypal5 wants to merge 1 commit into
mainfrom
feat/lifecycle-recipe-assets

Conversation

@shaypal5

Copy link
Copy Markdown
Contributor

LTV-Po.2b — b2b_saas_ltv_v1 recipe assets + end-to-end round-trip

The finish line for LTV-M6: the first point where
Generator.from_recipe("b2b_saas_ltv_v1").generate() produces a complete
lifecycle / pLTV bundle from the public API, in both exposure modes.

What this adds

Recipe assetsleadforge/recipes/b2b_saas_ltv_v1/:

The recipe registry auto-discovers the directory — no manual registration.

Difficulty resolutionLifecycleScheme._resolve_difficulty mirrors
LeadScoringScheme._resolve_difficulty (minus the lead-scoring-only
category_latent_correlations): it reads the active profile and attaches
DifficultyParams to the config. build_world now calls it, so the resolved
params ride on the returned spec.config and the snapshot builders apply
per-tier distortions. The simulation itself stays tier-independent — every
tier yields the same underlying world; simulation-level scaling remains deferred
(issue #129).

Tests

  • tests/recipes/test_b2b_saas_ltv_v1.py (new) — discovery, asset shape, config
    resolution (n_customers from default_population), build_world round-trip,
    narrative-driven firmographics, determinism, both-mode bundle round-trip +
    full-bundle byte-identity, and public industry/region variance.
  • Flipped the two tracked-gap guards now that difficulty resolves:
    • test_difficulty_not_yet_differentiatingtest_difficulty_resolves_params_but_world_unchanged
      (params differ per tier; the world stays identical — lifecycle: simulation-level difficulty scaling (advanced = genuinely harder world) #129 still open).
    • the explicit-param test_difficulty_params_thread_into_snapshots
      tier-based test_difficulty_tiers_produce_different_task_features (since
      _resolve_difficulty always overwrites difficulty_params from the profile,
      an explicitly-passed one would be clobbered).

Carried limitations / constraints

  • early_tenure_weeks / observation_date stay override-only (carried from
    Po.2a): the Recipe schema has no field for them, so recipe.yaml does not
    declare them; the bundle uses the GenerationConfig defaults (4 weeks;
    observation_date derived by the population builder).
  • Public mode stays calendar-only (Option A, locked): the early-pLTV family
    is omitted from student_public (4 task dirs public / 8 instructor).

Gates

  • pytest: 1899 passed, 51 skipped (full suite).
  • ruff check . clean · ruff format --check clean · mypy leadforge/ clean
    (114 source files).
  • Lead-scoring is untouched (changes are lifecycle-scheme + new recipe dir only);
    its determinism/byte-identity tests pass unchanged.

Closes LTV-M6. Next: LTV-M7 (LTV-Pp — scheme-aware validate_bundle).

🤖 Generated with Claude Code

Add the three recipe assets for the lifecycle / pLTV scheme and resolve
difficulty per tier, completing LTV-M6 (the first end-to-end
b2b_saas_ltv_v1 bundle from the public Generator API).

Recipe assets (leadforge/recipes/b2b_saas_ltv_v1/):
- recipe.yaml: scheme: lifecycle, primary_task pltv_revenue_365d,
  default_population {n_customers: 1500}, both exposure modes, all tiers.
- narrative.yaml: subscription-LTV story; market declares 4 icp_industries
  + 3 geographies so the public industry/region columns keep variance
  (student_public invariant #6).
- difficulty_profiles.yaml: intro/intermediate/advanced distortion knobs.
The registry auto-discovers the dir; no manual registration needed.

Difficulty resolution:
- LifecycleScheme._resolve_difficulty mirrors lead-scoring (minus the
  lead-scoring-only category_latent_correlations): reads the active profile
  and attaches DifficultyParams to config. build_world calls it so the
  resolved params ride on spec.config and the snapshot builders apply
  per-tier distortions. Simulation-level scaling stays deferred (issue #129)
  — the world is still tier-independent.

Tests:
- tests/recipes/test_b2b_saas_ltv_v1.py: discovery, asset shape, config
  resolution (n_customers), build_world round-trip, narrative-driven
  firmographics, determinism, both-mode bundle round-trip + byte-identity,
  public industry/region variance.
- Flipped the two tracked-gap guards: test_difficulty_not_yet_differentiating
  -> test_difficulty_resolves_params_but_world_unchanged; the explicit-param
  test_difficulty_params_thread_into_snapshots -> tier-based
  test_difficulty_tiers_produce_different_task_features.

Roadmap + .agent-plan.md mark Po.2b done and LTV-M6 complete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 20, 2026 05:10
@shaypal5 shaypal5 added this to the dataset: leadforge-ltv-v1 milestone Jun 20, 2026
@shaypal5 shaypal5 added type: feature New capability layer: api api/ public Python surface layer: recipes recipes/ recipe assets and registry dataset: leadforge-ltv-v1 Issue/PR scoped to the b2b_saas_ltv_v1 LTV dataset workstream labels Jun 20, 2026
@github-actions

Copy link
Copy Markdown

pr-agent-context report:

No unresolved review comments, failing checks, or actionable patch coverage gaps were found on PR #132 in repository https://github.com/leadforge-dev/leadforge. Treat this PR as all clear unless new signals appear.

Run metadata:

Tool ref: v4
Tool version: 4.0.21
Trigger: pull request opened
Workflow run: 27861125430 attempt 1
Comment timestamp: 2026-06-20T05:10:46.843352+00:00
PR head commit: 3384a0a95940468e1376126d2920c6c2b11b3818

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the first end-to-end, public-API round-trip for the new lifecycle scheme via the b2b_saas_ltv_v1 recipe, including recipe assets, difficulty-profile resolution into snapshot distortions, and comprehensive e2e tests across exposure modes and tiers.

Changes:

  • Added leadforge/recipes/b2b_saas_ltv_v1/ assets (recipe.yaml, narrative.yaml, difficulty_profiles.yaml) for lifecycle/pLTV generation.
  • Implemented lifecycle difficulty resolution in LifecycleScheme.build_world() to attach DifficultyParams from the recipe profile for downstream snapshot distortion.
  • Added/updated tests to validate recipe discovery, config resolution, determinism, bundle round-trips, and difficulty-tier distortion behavior.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/schemes/lifecycle/test_write_bundle.py Updates difficulty-threading test to assert tier-based distortions (intro vs advanced).
tests/schemes/lifecycle/test_build_world.py Updates tracked-gap test: difficulty params now resolve; simulated world remains tier-independent.
tests/recipes/test_b2b_saas_ltv_v1.py New e2e recipe tests: discovery, config resolution, determinism, both exposure modes, and snapshot invariants.
leadforge/schemes/lifecycle/init.py Adds difficulty-profile resolution into DifficultyParams for lifecycle bundle writing/snapshots.
leadforge/recipes/b2b_saas_ltv_v1/recipe.yaml New lifecycle recipe definition and defaults.
leadforge/recipes/b2b_saas_ltv_v1/narrative.yaml New narrative with multi-valued industry/geo vocab for public variance invariant.
leadforge/recipes/b2b_saas_ltv_v1/difficulty_profiles.yaml New per-tier distortion knobs (noise/missing/outliers) plus carried forward params.
leadforge/recipes/b2b_saas_ltv_v1/init.py Adds package marker for recipe directory.
docs/ltv/roadmap.md Marks LTV-M6 complete and records Po.2b deliverables.
.agent-plan.md Updates agent plan to reflect Po.2b completion and next milestone.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +143 to +150
try:
raw = load_recipe(config.recipe_id)
recipe = Recipe.from_dict(raw)
profiles = recipe.load_difficulty_profiles()
except (FileNotFoundError, KeyError):
return config
if not profiles:
return config
Comment on lines +172 to +181
cr_range = profile["conversion_rate_range"]
difficulty_params = DifficultyParams(
signal_strength=profile["signal_strength"],
noise_scale=profile["noise_scale"],
missing_rate=profile["missing_rate"],
outlier_rate=profile["outlier_rate"],
conversion_rate_lo=cr_range[0],
conversion_rate_hi=cr_range[1],
committee_friction=profile["committee_friction"],
)
Comment on lines +184 to +187
for col in ("industry", "region"):
if col in train.columns:
assert train[col].nunique(dropna=True) >= 2, f"{col} is zero-variance"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dataset: leadforge-ltv-v1 Issue/PR scoped to the b2b_saas_ltv_v1 LTV dataset workstream layer: api api/ public Python surface layer: recipes recipes/ recipe assets and registry type: feature New capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants