Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 34 additions & 18 deletions .agent-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,58 @@

## Current System State

**v0.1.0 shipped.** Installable package, full CLI skeleton (`list-recipes` implemented, others stubbed), core enums/exceptions/models, recipe registry, CI workflow, pre-commit config. All 20 tests pass.
**v0.2.0 in progress.** Typed `Recipe` model, `GenerationConfig` with full validation, config
precedence system, `RNGRoot` with deterministic substreams, `Generator.from_recipe()` fully
implemented, `core/hashing.py`, `core/serialization.py`, and recipe narrative/difficulty-profile
assets for `b2b_saas_procurement_v1`. 59 tests passing.

---

## Active Task Breakdown — Milestone 1: Canonical Config, Recipe & Model Objects (v0.2.0)
## Active Task Breakdown — Milestone 2: Narrative Layer (v0.2.0 cont.)

Goal: Establish the typed configuration and recipe system that all simulation work will depend on.
Goal: Build the concrete company/product/market story objects that anchor all later simulation.

- [ ] **1. Core models + RNG utilities**
- Implement `leadforge/core/rng.py`: seeded RNG root + deterministic named substreams
- Flesh out `GenerationConfig` with full validation (config precedence rules)
- Implement `leadforge/core/hashing.py`: deterministic config hashing for manifest identity
- [ ] **1. Narrative models**
- Implement typed dataclasses in `narrative/`: `CompanySpec`, `ProductSpec`, `MarketSpec`,
`PersonaSpec`, `FunnelSpec`
- Loader: parse `narrative.yaml` into these models with validation

- [ ] **2. Recipe registry and loading**
- Implement full `Recipe` typed model (dataclass) in `leadforge/api/recipes.py`
- Implement config precedence: CLI flags > override file > recipe defaults > package defaults
- Implement `leadforge/core/serialization.py`: JSON/YAML read-write helpers
- [ ] **2. WorldSpec population**
- Flesh out `WorldSpec` to hold a resolved `NarrativeSpec`
- Wire into `Generator.from_recipe()` so `gen.world_spec` is populated after construction

- [ ] **3. Recipe assets + validation tests**
- Add `narrative.yaml`, `difficulty_profiles.yaml` to `b2b_saas_procurement_v1/`
- Implement `Generator.from_recipe(...)` skeleton (no simulation yet)
- Tests: recipe validation, config precedence, RNG determinism, `from_recipe` round-trip
- [ ] **3. Dataset card generation**
- Implement `narrative/dataset_card.py`: render a Markdown dataset card from `WorldSpec`
- Tests: round-trip model → YAML → model, dataset-card text contains expected fields

---

## Context Pointers

- Milestone 1 scope: `docs/leadforge_implementation_plan.md` §5 "Milestone 1"
- Milestone 2 scope: `docs/leadforge_implementation_plan.md` §5 "Milestone 2"
- Full milestone dependency graph: `docs/leadforge_implementation_plan.md` §6
- Public API contract: `docs/leadforge_architecture_spec.md` §6
- Config precedence rules: `docs/leadforge_architecture_spec.md` §24
- Narrative spec: `docs/leadforge_architecture_spec.md` §7
- Recipe assets: `leadforge/recipes/b2b_saas_procurement_v1/narrative.yaml`

---

## Completed Phases

### Milestone 1 — Canonical Config, Recipe & Model Objects ✓ (v0.2.0 in PR)
- `leadforge/core/rng.py`: `RNGRoot` with SHA-256-derived named substreams
- `leadforge/core/hashing.py`: `hash_config()` — stable SHA-256 digest of `GenerationConfig`
- `leadforge/core/serialization.py`: `load_yaml`, `load_json`, `dump_json`
- `leadforge/core/models.py`: `GenerationConfig` with `__post_init__` validation + `package_version`
- `leadforge/api/recipes.py`: typed `Recipe` dataclass, `from_dict`, `resolve_config` (full
precedence: explicit kwargs > override dict > recipe defaults > package defaults)
- `leadforge/api/generator.py`: `Generator.from_recipe()` fully implemented (skeleton — no
simulation); `generate()` stubs to v0.3.0
- `leadforge/recipes/b2b_saas_procurement_v1/narrative.yaml`: company, product, market, GTM,
personas, funnel stages
- `leadforge/recipes/b2b_saas_procurement_v1/difficulty_profiles.yaml`: intro / intermediate /
advanced signal-noise profiles
- 39 new tests (rng, hashing, recipes, generator); total 59 passing

### Milestone 0 — Project Foundation ✓ (v0.1.0)
- `pyproject.toml`, `README.md`, `LICENSE`, `.pre-commit-config.yaml`
- Full package skeleton with `__init__.py` stubs for all submodules
Expand Down
4 changes: 3 additions & 1 deletion leadforge/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""leadforge public Python API."""

from leadforge.api.generator import Generator
from leadforge.api.recipes import Recipe
from leadforge.recipes.registry import list_recipes

__all__ = ["Generator"]
__all__ = ["Generator", "Recipe", "list_recipes"]
80 changes: 63 additions & 17 deletions leadforge/api/generator.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
"""Public Generator API — stub for Milestone 1.

The Generator class is the primary entry point for programmatic dataset
generation. It is fully specified in the architecture doc (§6) and will
be implemented across Milestones 1–9.
"""
"""Public Generator API."""

from __future__ import annotations

from typing import Any

from leadforge.core.enums import DifficultyProfile, ExposureMode
from leadforge.core.models import GenerationConfig, WorldBundle
from leadforge.core.rng import RNGRoot
Comment thread
shaypal5 marked this conversation as resolved.
from leadforge.core.sentinels import _MISSING


class Generator:
"""High-level entry point for generating a synthetic CRM dataset bundle.

Usage (once implemented)::
Usage::

gen = Generator.from_recipe(
"b2b_saas_procurement_v1",
Expand All @@ -26,28 +23,77 @@ class Generator:
bundle = gen.generate(n_leads=5000, difficulty="intermediate")
bundle.save("./out/demo_bundle")

Implemented in Milestone 1 (config/recipe) through Milestone 9 (rendering).
``from_recipe`` is implemented in Milestone 1. Full generation
(``generate``) is implemented across Milestones 2–9.
"""

def __init__(self, config: GenerationConfig) -> None:
self._config = config
self._rng = RNGRoot(config.seed)

@property
def config(self) -> GenerationConfig:
return self._config

@classmethod
def from_recipe(
cls,
recipe_id: str,
*,
seed: int = 42,
exposure_mode: str | ExposureMode = ExposureMode.student_public,
**kwargs: Any,
seed: int = _MISSING, # type: ignore[assignment]
exposure_mode: str | ExposureMode = _MISSING, # type: ignore[assignment]
difficulty: str | DifficultyProfile = _MISSING, # type: ignore[assignment]
n_accounts: int | None = None,
n_contacts: int | None = None,
n_leads: int | None = None,
horizon_days: int | None = None,
output_path: str = _MISSING, # type: ignore[assignment]
override: dict[str, Any] | None = None,
Comment thread
shaypal5 marked this conversation as resolved.
) -> Generator:
"""Create a Generator from a recipe ID.
"""Create a :class:`Generator` from a recipe ID, applying config precedence.

Args:
recipe_id: Identifier of a registered recipe (e.g.
``"b2b_saas_procurement_v1"``).
seed: Master RNG seed. Defaults to the package default (42).
exposure_mode: ``"student_public"`` or ``"research_instructor"``.
Defaults to the package default (``student_public``).
difficulty: ``"intro"``, ``"intermediate"``, or ``"advanced"``.
Defaults to the package default (``intermediate``).
n_accounts: Override recipe default account count.
n_contacts: Override recipe default contact count.
n_leads: Override recipe default lead count.
horizon_days: Override recipe default simulation horizon.
output_path: Directory where the bundle will be saved.
override: Optional dict of overrides (mirrors a ``--override`` file).
Applied after recipe defaults but before explicit kwargs.

Not yet implemented — available in v0.2.0.
Returns:
A configured :class:`Generator` instance ready to call
:meth:`generate` on.

Raises:
:class:`~leadforge.core.exceptions.InvalidRecipeError`: if the
recipe does not exist, is malformed, or the requested
exposure mode / difficulty is not supported.
"""
raise NotImplementedError(
"Generator.from_recipe() is not yet implemented. Coming in v0.2.0."
from leadforge.api.recipes import Recipe
from leadforge.recipes.registry import load_recipe

raw = load_recipe(recipe_id)
recipe = Recipe.from_dict(raw)
config = recipe.resolve_config(
seed=seed,
exposure_mode=exposure_mode,
difficulty=difficulty,
n_accounts=n_accounts,
n_contacts=n_contacts,
n_leads=n_leads,
horizon_days=horizon_days,
output_path=output_path,
override=override,
)
return cls(config)

def generate(
self,
Expand All @@ -60,6 +106,6 @@ def generate(
) -> WorldBundle:
"""Run the world simulation and return a bundle.

Not yet implemented — available in v0.2.0.
Not yet implemented — available in v0.3.0+.
"""
raise NotImplementedError("Generator.generate() is not yet implemented. Coming in v0.2.0.")
raise NotImplementedError("Generator.generate() is not yet implemented. Coming in v0.3.0.")
Loading
Loading