feat(lifecycle): build_world + relational tables [LTV-Pn.4a]#125
Merged
Conversation
First sub-PR of the split LTV-Pn.4. Implements the lifecycle scheme's build_world half (the write half is Pn.4b-c). build_world (layer: api): - Deterministically samples a retention motif family from config.seed (a named RNG substream), honouring the "world structure varies via named motif families" hard constraint — different seeds yield structurally different worlds (verified: all 5 families appear across seeds). - Builds the customer population, runs the weekly simulation, and wraps the result in a new LifecycleArtifacts container on the WorldBundle (the scheme-owned payload behind the opaque WorldBundle.artifacts from Pn.2). - Consumes the Pn.3 lifecycle config fields: n_customers, observation_date, early_tenure_weeks, and forward_windows_days (the engine simulates through max(windows) = 730d so every pLTV target is fully covered). This makes the config authoritative, as the Pn.3 doc note promised. - narrative is accepted for protocol parity but unused — the lifecycle population builder generates its own firmographics. Lifecycle relational export (layer: render): - New schemes/lifecycle/render/relational.py to_dataframes: six tables (accounts + customers + subscriptions + subscription_events + health_signals + invoices), mirroring the lead-scoring table-source registry pattern with dtypes from each entity's DTYPE_MAP. write_bundle / write_metadata remain stubbed (Pn.4b-c). Tests (10 new): build_world returns a LifecycleArtifacts bundle; consumes config fields (n_customers, early_tenure, full-window coverage); determinism; cross-seed motif variability; relational table set/shape/typing; FK integrity across LIFECYCLE_CONSTRAINTS; relational determinism; tiny-world empties. Registry stub test updated (build_world live; write path still raises). Full suite 1848 passed / 51 skipped; ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Implements the lifecycle scheme’s build_world path and introduces a relational export that flattens lifecycle population + simulation output into typed pandas DataFrames, along with tests and roadmap/plan documentation updates for the Pn.4a split.
Changes:
- Implement
LifecycleScheme.build_worldto sample a motif family deterministically from the seed, build a customer population, run the simulation, and return aWorldBundlecarryingLifecycleArtifacts. - Add lifecycle relational export
to_dataframesproducing six typed tables (accounts,customers,subscriptions,subscription_events,health_signals,invoices). - Add/adjust tests to cover determinism, motif variability across seeds, FK integrity, relational typing, and stubbing of write paths.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/schemes/test_registry.py | Updates lifecycle registry test to reflect build_world implementation while keeping write paths stubbed. |
| tests/schemes/lifecycle/test_build_world.py | Adds coverage for lifecycle build_world behavior and relational export correctness/determinism. |
| leadforge/schemes/lifecycle/render/relational.py | New lifecycle relational export to typed DataFrames via a table-source registry. |
| leadforge/schemes/lifecycle/render/init.py | Adds lifecycle render package init/docstring. |
| leadforge/schemes/lifecycle/artifacts.py | Introduces LifecycleArtifacts container carried in WorldBundle.artifacts. |
| leadforge/schemes/lifecycle/init.py | Implements LifecycleScheme.build_world and updates module docs/stub messaging. |
| docs/ltv/roadmap.md | Documents the Pn.4 split and records Pn.4a progress. |
| .agent-plan.md | Updates agent plan to reflect the Pn.4a–d split sequencing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
47
to
52
| def build_world( | ||
| self, | ||
| config: GenerationConfig, | ||
| narrative: NarrativeSpec, | ||
| **options: Any, | ||
| ) -> WorldBundle: |
Self-review of build_world: it silently dropped several config inputs. The deferrals are legitimate, but silence is the footgun — surface them at the call site and pin them to concrete PRs. 1. Difficulty (the real one). build_world does not consume config.difficulty / difficulty_params — so every difficulty tier currently yields the SAME lifecycle world, and when Pn.4b builds snapshots it would pass difficulty_params=None (no distortions ever fire). Documented in the build_world docstring; roadmap Pn.4b now explicitly owns resolving difficulty_params and threading it into build_customer_snapshot; simulation-level difficulty scaling is recorded as a flagged deferral near Po/Pp. New test_difficulty_not_yet_differentiating pins the gap as a tested fact (intro == advanced today) — it must be flipped when Pn.4b closes it. 2. Narrative ignored. The lifecycle population hardcodes firmographics and build_world ignores `narrative`, so the recipe's narrative.yaml will not drive them. Documented; roadmap Po now carries the decide-narrative- consumption task. 3. to_dataframes near-duplicates the lead-scoring conversion loop. Noted the deliberate parallel + earmarked unification behind a shared render helper for Pn.4d (the orchestrator PR, where the byte-identity surface is tested). Docs/tests only — build_world behavior unchanged. Full suite 1849 passed / 51 skipped; ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
pr-agent-context report: This run includes an unresolved review comment on PR #125 in repository https://github.com/leadforge-dev/leadforge
For each unresolved review comment, recommend one of: resolve as irrelevant, accept and implement
the recommended solution, open a separate issue and resolve as out-of-scope for this PR, accept and
implement a different solution, or resolve as already treated by the code.
After I reply with my decision per item, implement the accepted actions, resolve the corresponding
PR comments, and push all of these changes in a single commit.
# Copilot Comments
## COPILOT-1
Location: leadforge/schemes/lifecycle/__init__.py:52
URL: https://github.com/leadforge-dev/leadforge/pull/125#discussion_r3409257575
Status: outdated
Root author: copilot-pull-request-reviewer
Comment:
`build_world` is documented and exercised as accepting `narrative=None`, and `WorldSpec.narrative` is `NarrativeSpec | None`, but the method signature currently requires a non-optional `NarrativeSpec`. Making the parameter optional improves API clarity and avoids downstream type friction when lifecycle is invoked without a narrative (as intended here).Run metadata: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First sub-PR of the split
LTV-Pn.4(the write half follows in Pn.4b–c).LTV-Pn.4was too large for one PR — split along the twoGenerationSchememethods, then public-safety, then the carried orchestrator cleanup. This is Pn.4a:build_world.build_world(layer: api)config.seed(a named RNG substream) — honours the "world structure varies via named motif families" hard constraint, so different seeds yield structurally different worlds. (Verified: all 5 retention families appear across seeds.)LifecycleArtifactscontainer on the bundle — the scheme-owned payload behind the opaqueWorldBundle.artifactsintroduced in Pn.2.n_customers,observation_date,early_tenure_weeks,forward_windows_days— engine simulates throughmax(windows)=730dso every pLTV target is fully covered), making the config authoritative as the Pn.3 doc note promised.narrativeis accepted for protocol parity but unused — the lifecycle population builder generates its own firmographics.Lifecycle relational export (
layer: render)New
schemes/lifecycle/render/relational.pyto_dataframes: six tables (accounts+customers+subscriptions+subscription_events+health_signals+invoices), mirroring the lead-scoring table-source registry pattern with dtypes from each entity'sDTYPE_MAP.write_bundle/write_metadataremain stubbed (Pn.4b–c).Tests (10 new; full suite 1848 passed / 51 skipped)
build_world returns a
LifecycleArtifactsbundle; consumes config fields incl. full-window coverage; determinism; cross-seed motif variability (≥3 distinct, ⊆ the 5 families); relational table set/shape/typing; FK integrity acrossLIFECYCLE_CONSTRAINTS; relational determinism; tiny-world empties. The registry stub test is updated (build_world live; write path still raises).Next: Pn.4b — instructor-mode
write_bundle(relational + both-regime snapshots → 8 task dirs + card + manifest +write_metadata).🤖 Generated with Claude Code