Skip to content

feat: Milestone 1 — canonical config, recipe, and model objects#4

Merged
shaypal5 merged 5 commits intomainfrom
feat/milestone-1-config-recipe
Apr 20, 2026
Merged

feat: Milestone 1 — canonical config, recipe, and model objects#4
shaypal5 merged 5 commits intomainfrom
feat/milestone-1-config-recipe

Conversation

@shaypal5
Copy link
Copy Markdown
Contributor

Summary

Implements the typed configuration and recipe system that all simulation work will depend on (Milestone 1 / v0.2.0).

  • core/rng.pyRNGRoot: single seeded root with SHA-256-derived deterministic named substreams; every stochastic component must obtain its random.Random via root.child(name)
  • core/hashing.pyhash_config(): stable SHA-256 digest of a GenerationConfig for manifest identity
  • core/serialization.pyload_yaml, load_json, dump_json helpers used across the package
  • core/models.pyGenerationConfig gains __post_init__ validation (positive counts, non-negative seed, enum coercion) and a package_version field
  • api/recipes.py — typed Recipe dataclass (frozen) with from_dict() validation and resolve_config() implementing the full 4-layer config precedence: explicit kwargs > override dict > recipe defaults > package defaults
  • api/generator.pyGenerator.from_recipe() fully implemented; generate() stubs to v0.3.0
  • Recipe assetsnarrative.yaml (company, product, market, GTM, personas, funnel stages) and difficulty_profiles.yaml (intro / intermediate / advanced signal-noise profiles) for b2b_saas_procurement_v1

Test plan

  • 39 new tests across tests/core/test_rng.py, tests/core/test_hashing.py, tests/api/test_recipes.py, tests/api/test_generator.py — total 59 passing
  • ruff clean, mypy clean (31 source files)
  • Config precedence all 4 layers covered by property-style tests
  • RNG determinism: same seed → same sequence; different names → different sequences
  • Hash stability: same config → same hash; field change → different hash
  • Real recipe round-trip: load → Recipe.from_dictresolve_configGenerator

🤖 Generated with Claude Code

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>
@shaypal5 shaypal5 added type: feature New capability layer: core core/ primitives (RNG, IDs, models, exceptions) layer: api api/ public Python surface layer: recipes recipes/ recipe assets and registry labels Apr 18, 2026
Copilot AI review requested due to automatic review settings April 18, 2026 15:48
@shaypal5 shaypal5 self-assigned this Apr 18, 2026
@github-actions

This comment has been minimized.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 RNGRoot for deterministic named RNG substreams and hash_config() for stable GenerationConfig digests.
  • Introduce typed Recipe with validation + 4-layer config precedence resolution; wire Generator.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.

Comment thread leadforge/api/recipes.py Outdated
Comment thread leadforge/api/recipes.py Outdated
Comment thread leadforge/api/recipes.py Outdated
Comment thread leadforge/core/models.py Outdated
Comment thread tests/api/test_recipes.py
Comment thread tests/api/test_generator.py
@github-actions

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>
@github-actions

This comment has been minimized.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread leadforge/api/recipes.py Outdated
Comment thread leadforge/api/recipes.py Outdated
Comment thread leadforge/api/recipes.py Outdated
Comment thread leadforge/core/models.py Outdated
Comment thread leadforge/core/serialization.py
Comment thread leadforge/core/serialization.py Outdated
Comment thread leadforge/api/generator.py
@github-actions

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>
@github-actions

This comment has been minimized.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread leadforge/core/models.py Outdated
Comment thread leadforge/core/rng.py Outdated
Comment thread leadforge/core/serialization.py Outdated
Comment thread leadforge/api/generator.py
Comment thread leadforge/api/recipes.py Outdated
Comment thread leadforge/api/recipes.py Outdated
@github-actions

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>
@github-actions

This comment has been minimized.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread leadforge/api/recipes.py
@github-actions

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>
@github-actions
Copy link
Copy Markdown

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:

Tool ref: v4
Tool version: 4.0.18
Trigger: commit pushed
Workflow run: 24639305551 attempt 1
Comment timestamp: 2026-04-19T21:15:34.877212+00:00
PR head commit: fce7ef90c5dd5ed1a730a1f0accb9f100e8ab11d

@shaypal5 shaypal5 merged commit f7dff3f into main Apr 20, 2026
5 checks passed
@shaypal5 shaypal5 deleted the feat/milestone-1-config-recipe branch April 20, 2026 05:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

layer: api api/ public Python surface layer: core core/ primitives (RNG, IDs, models, exceptions) 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