feat: Milestone 1 — canonical config, recipe, and model objects#4
feat: Milestone 1 — canonical config, recipe, and model objects#4
Conversation
Implements the typed configuration and recipe system that all simulation work will depend on (v0.2.0 milestone 1). Core utilities - core/rng.py: RNGRoot with SHA-256-derived deterministic named substreams - core/hashing.py: hash_config() — stable SHA-256 digest of GenerationConfig - core/serialization.py: load_yaml, load_json, dump_json helpers Config & recipe models - core/models.py: GenerationConfig gains __post_init__ validation and package_version field; WorldSpec/WorldBundle updated docstrings - api/recipes.py: typed Recipe dataclass (frozen) with from_dict() validation and resolve_config() implementing full config precedence: explicit kwargs > override dict > recipe defaults > package defaults - api/generator.py: Generator.from_recipe() fully implemented; exposes .config property and RNGRoot; generate() stubs to v0.3.0 - api/__init__.py: export Recipe and list_recipes at top level Recipe assets (b2b_saas_procurement_v1) - narrative.yaml: company, product, market, GTM motion, personas, funnel stages - difficulty_profiles.yaml: intro / intermediate / advanced signal-noise profiles Tests (39 new → 59 total) - tests/core/test_rng.py: determinism, independence, repr, error handling - tests/core/test_hashing.py: stability, sensitivity, hex format - tests/api/test_recipes.py: from_dict validation, all 4 precedence layers, unsupported mode/difficulty, real recipe loading, narrative/profile assets - tests/api/test_generator.py: from_recipe round-trip, deterministic config, override dict, invalid recipe, generate() stub Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Implements Milestone 1’s canonical config/recipe plumbing that future simulation layers will build on, including deterministic RNG substreams, stable config hashing for manifest identity, and typed recipe/config resolution.
Changes:
- Add
RNGRootfor deterministic named RNG substreams andhash_config()for stableGenerationConfigdigests. - Introduce typed
Recipewith validation + 4-layer config precedence resolution; wireGenerator.from_recipe()to recipe registry loading. - Add serialization helpers and new recipe narrative/difficulty profile YAML assets, plus a test suite covering core behaviors.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
leadforge/core/rng.py |
Adds seeded RNG root with SHA-256-derived deterministic child streams. |
leadforge/core/hashing.py |
Adds canonicalization + SHA-256 hashing for GenerationConfig. |
leadforge/core/serialization.py |
Adds shared YAML/JSON load + JSON dump helpers. |
leadforge/core/models.py |
Adds GenerationConfig validation + package_version field; updates milestone notes. |
leadforge/api/recipes.py |
Introduces frozen Recipe model, validation, narrative/profile loaders, and config precedence resolution. |
leadforge/api/generator.py |
Implements Generator.from_recipe() and adds config property; generate() remains stubbed. |
leadforge/api/__init__.py |
Exposes Recipe and list_recipes in the public API. |
leadforge/recipes/b2b_saas_procurement_v1/narrative.yaml |
Adds narrative defaults asset for the recipe. |
leadforge/recipes/b2b_saas_procurement_v1/difficulty_profiles.yaml |
Adds difficulty profiles asset for the recipe. |
tests/core/test_rng.py |
Adds determinism/independence/validation tests for RNGRoot. |
tests/core/test_hashing.py |
Adds stability/difference tests for hash_config(). |
tests/api/test_recipes.py |
Adds validation + config precedence tests for Recipe. |
tests/api/test_generator.py |
Adds tests for Generator.from_recipe() config wiring + determinism. |
tests/api/__init__.py |
Adds package marker for API tests (empty init). |
.agent-plan.md |
Updates project plan status to reflect Milestone 1 completion and Milestone 2 focus. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This comment has been minimized.
This comment has been minimized.
- api/recipes.py: repurpose _MISSING sentinel to detect whether seed / output_path were explicitly passed, fixing the bug where override-dict values for those fields were silently discarded (COPILOT-1, COPILOT-3). Initialize resolved dict from GenerationConfig field defaults via dataclasses.fields() — eliminates duplicated magic numbers 1500/4200/5000 and keeps package defaults as the single source of truth (COPILOT-2). - api/generator.py: propagate _MISSING through from_recipe() so the sentinel works end-to-end across the public API. - core/models.py: replace TypeError/ValueError in __post_init__ with InvalidConfigError so all config errors are catchable as LeadforgeError (COPILOT-4). - tests/api/test_recipes.py: add two tests covering override-dict seed / output_path precedence and explicit-kwarg-beats-override semantics (COPILOT-5). - tests/api/test_generator.py: same coverage at the Generator level (COPILOT-6). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 15 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This comment has been minimized.
This comment has been minimized.
core/sentinels.py (new, COPILOT-7)
Move _MISSING sentinel to a shared module so recipes.py and generator.py
both import from one place, removing private-API coupling.
api/recipes.py (COPILOT-1, COPILOT-2, COPILOT-3)
- Apply _MISSING sentinel to exposure_mode and difficulty parameters so
override-dict values for those fields are not silently discarded when
the caller omits the kwargs (COPILOT-1).
- Add exposure_mode / difficulty to the resolved dict initialised from
GenerationConfig field defaults, keeping the package defaults as the
single source of truth for all four sentinel-guarded params (COPILOT-2).
- Validate that load_narrative() / load_difficulty_profiles() return a
dict; raise InvalidRecipeError on malformed (non-mapping) YAML (COPILOT-3).
api/generator.py (COPILOT-1, COPILOT-7)
- Switch exposure_mode / difficulty defaults to _MISSING so sentinels
propagate correctly through from_recipe → resolve_config.
- Import _MISSING from core.sentinels instead of api.recipes.
core/models.py (COPILOT-4)
- Add _require_positive_int() helper that rejects bool (int subclass)
and non-int types with InvalidConfigError before numeric comparisons,
preventing silent TypeError from YAML-sourced string values.
- Apply to n_accounts, n_contacts, n_leads, horizon_days; same guard
added inline for seed.
core/serialization.py (COPILOT-5, COPILOT-6)
- Wrap OSError in LeadforgeError for load_yaml, load_json, dump_json
so file I/O errors surface as consistent domain exceptions (COPILOT-5).
- Replace default=str in dump_json with explicit _json_default() that
only handles Path→str and raises TypeError for anything else,
preventing silent coercion of unexpected types (COPILOT-6).
Tests (6 new → 69 total)
- test_recipes.py: override-dict and explicit-kwarg precedence for
exposure_mode / difficulty (COPILOT-1).
- test_generator.py: same coverage at Generator level.
- test_exceptions.py: string and bool count-field values raise
InvalidConfigError (COPILOT-4).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This comment has been minimized.
This comment has been minimized.
core/sentinels.py (COPILOT-4)
Replace bare object() sentinel with a named _MissingType singleton class
that has __repr__ = "<default>" and __bool__ = False. Users who call
help() or read generated docs now see "<default>" instead of an opaque
memory address.
core/rng.py (COPILOT-2)
RNGRoot.__init__ now rejects bool (int subclass) seeds and enforces
non-negative seeds, matching GenerationConfig's validation contract.
core/serialization.py (COPILOT-3)
All Path.open() calls now pass encoding="utf-8" for deterministic
cross-platform behaviour on non-ASCII content.
core/models.py (COPILOT-1)
Enum coercion in __post_init__ (ExposureMode / DifficultyProfile) now
wraps ValueError in InvalidConfigError with the field name and value,
keeping all config validation errors as typed LeadforgeErrors.
api/recipes.py (COPILOT-5, COPILOT-6)
- default_population validation now rejects bool values (isinstance bool
check added alongside the existing int check) (COPILOT-5).
- ExposureMode / DifficultyProfile conversion in resolve_config wraps
ValueError in InvalidRecipeError with a clear message (COPILOT-6).
Tests (9 new → 78 total)
- test_rng.py: bool seed rejection, negative seed rejection.
- test_exceptions.py: bad exposure_mode/difficulty strings raise
InvalidConfigError; _MISSING repr is "<default>"; singleton property.
- test_recipes.py: bool in default_population raises InvalidRecipeError;
invalid override exposure_mode/difficulty raise InvalidRecipeError.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This comment has been minimized.
This comment has been minimized.
int(True) → 1 and int(3.5) → 3 were both accepted silently. Now horizon_days is validated with an explicit bool-rejection check and a positivity guard (matching the pattern used for default_population and GenerationConfig.__post_init__), raising InvalidRecipeError on bad types or non-positive values. Adds three tests: bool, float, and non-positive horizon_days. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
pr-agent-context report: This run includes an unresolved review comment on PR #4.
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/api/recipes.py:106
URL: https://github.com/leadforge-dev/leadforge/pull/4#discussion_r3107487349
Root author: copilot-pull-request-reviewer
Comment:
`horizon_days` is coerced via `int(...)`, which will silently accept `True` (→ 1) and truncate floats. That can let malformed recipe YAML pass validation and produce unintended horizons. Validate `horizon_days` as a positive plain `int` (explicitly rejecting `bool`) and raise `InvalidRecipeError` on bad types/values, similar to `GenerationConfig.__post_init__`.Run metadata: |
Summary
Implements the typed configuration and recipe system that all simulation work will depend on (Milestone 1 / v0.2.0).
core/rng.py—RNGRoot: single seeded root with SHA-256-derived deterministic named substreams; every stochastic component must obtain itsrandom.Randomviaroot.child(name)core/hashing.py—hash_config(): stable SHA-256 digest of aGenerationConfigfor manifest identitycore/serialization.py—load_yaml,load_json,dump_jsonhelpers used across the packagecore/models.py—GenerationConfiggains__post_init__validation (positive counts, non-negative seed, enum coercion) and apackage_versionfieldapi/recipes.py— typedRecipedataclass (frozen) withfrom_dict()validation andresolve_config()implementing the full 4-layer config precedence: explicit kwargs > override dict > recipe defaults > package defaultsapi/generator.py—Generator.from_recipe()fully implemented;generate()stubs to v0.3.0narrative.yaml(company, product, market, GTM, personas, funnel stages) anddifficulty_profiles.yaml(intro / intermediate / advanced signal-noise profiles) forb2b_saas_procurement_v1Test plan
tests/core/test_rng.py,tests/core/test_hashing.py,tests/api/test_recipes.py,tests/api/test_generator.py— total 59 passingRecipe.from_dict→resolve_config→Generator🤖 Generated with Claude Code