Skip to content

feat: Milestone 2 — narrative layer (NarrativeSpec, WorldSpec, dataset card)#5

Merged
shaypal5 merged 2 commits intomainfrom
feat/milestone-2-narrative
Apr 21, 2026
Merged

feat: Milestone 2 — narrative layer (NarrativeSpec, WorldSpec, dataset card)#5
shaypal5 merged 2 commits intomainfrom
feat/milestone-2-narrative

Conversation

@shaypal5
Copy link
Copy Markdown
Contributor

Summary

  • leadforge/narrative/spec.py — frozen dataclasses for the full NarrativeSpec hierarchy: CompanySpec, ProductSpec, MarketSpec, GtmMotionSpec, PersonaSpec, FunnelStageSpec. Each has a validated from_dict() classmethod that rejects bools masquerading as ints, enforces list-pair shapes, and raises InvalidRecipeError on bad input.
  • leadforge/narrative/dataset_card.pyrender_dataset_card(world_spec: WorldSpec) -> str producing a Markdown card with: metadata header table, narrative summary (vendor/product/market/GTM/personas), primary task + label definition, stub sections for table inventory and feature categories (v0.3.0+), suggested use cases, and caveats.
  • leadforge/core/models.pyWorldSpec gains a narrative: NarrativeSpec | None = None field.
  • leadforge/api/generator.pyGenerator.from_recipe() loads and parses narrative.yaml into a NarrativeSpec, wraps it in a WorldSpec, and exposes it via the world_spec property.
  • 51 new tests across tests/narrative/test_spec.py and tests/narrative/test_dataset_card.py — covering sub-model validation, bool rejection, frozen-dataclass immutability, real YAML round-trip, card content assertions, and Generator integration. Total: 110 tests passing.

Test plan

  • pytest — 110 tests pass
  • ruff check . && ruff format --check . — clean
  • mypy leadforge/ — no errors
  • test_real_narrative_yaml_parses — verifies b2b_saas_procurement_v1/narrative.yaml round-trips
  • test_card_with_narrative_contains_company_name — verifies card contains "Veridian Technologies"
  • test_narrative_spec_frozen — confirms FrozenInstanceError on mutation attempt

🤖 Generated with Claude Code

…t card)

- narrative/spec.py: frozen dataclasses for NarrativeSpec hierarchy
  (CompanySpec, ProductSpec, MarketSpec, GtmMotionSpec, PersonaSpec,
  FunnelStageSpec) with validated from_dict() classmethods
- narrative/dataset_card.py: render_dataset_card() produces Markdown
  dataset card from WorldSpec (header, narrative summary, task, stubs
  for table inventory and feature categories, use cases, caveats)
- core/models.py: WorldSpec.narrative field (NarrativeSpec | None)
- api/generator.py: world_spec property; from_recipe() resolves the
  recipe's narrative.yaml into a NarrativeSpec and populates WorldSpec
- 51 new tests covering spec validation, card rendering, and Generator
  integration (110 total); ruff + mypy clean

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: narrative narrative/ vertical story layer layer: api api/ public Python surface labels Apr 20, 2026
Copilot AI review requested due to automatic review settings April 20, 2026 08:13
@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

Introduces Milestone 2’s “narrative layer” by adding typed, validated narrative spec models, wiring them into WorldSpec/Generator, and providing a Markdown dataset-card renderer with accompanying tests.

Changes:

  • Add frozen dataclass hierarchy for NarrativeSpec and sub-specs with from_dict() parsing/validation.
  • Add render_dataset_card(WorldSpec) -> str to produce a Markdown dataset card (with stubs for future milestones).
  • Extend WorldSpec with an optional narrative field and have Generator.from_recipe() populate/expose a world_spec.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
leadforge/narrative/spec.py New narrative datamodels + validation helpers.
leadforge/narrative/dataset_card.py Markdown dataset card renderer based on WorldSpec.
leadforge/core/models.py Add WorldSpec.narrative (optional) + docstring update.
leadforge/api/generator.py Populate/expose world_spec (incl. narrative) from recipes; update constructor.
tests/narrative/test_spec.py Tests for parsing/validation, immutability, and real YAML parsing.
tests/narrative/test_dataset_card.py Tests for dataset card rendering and generator integration.
tests/narrative/__init__.py New test package marker.
.agent-plan.md Project plan updated to reflect Milestone 2 completion and Milestone 3 next steps.

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

Comment thread leadforge/narrative/spec.py
Comment thread leadforge/narrative/spec.py Outdated
Comment thread leadforge/narrative/spec.py
Comment thread leadforge/api/generator.py Outdated
Comment thread leadforge/narrative/dataset_card.py Outdated
Comment thread leadforge/narrative/spec.py Outdated
Comment thread leadforge/narrative/spec.py Outdated
@github-actions

This comment has been minimized.

@shaypal5 shaypal5 self-assigned this Apr 20, 2026
- spec.py: _require_keys now guards against non-dict input (COPILOT-3)
- spec.py: NarrativeSpec.from_dict validates each personas/funnel_stages
  element is a dict before passing to sub-from_dict (COPILOT-3)
- spec.py: GtmMotionSpec.from_dict validates channels is a list of
  strings, rejects bools for share floats, and enforces [0, 1] range
  (COPILOT-1)
- spec.py: PersonaSpec.from_dict validates title_variants is a list of
  strings instead of silently splitting a bare string (COPILOT-2)
- spec.py: ProductSpec.from_dict requires free_trial_available /
  demo_available to be actual bools; rejects int/str coercion (COPILOT-6)
- spec.py: MarketSpec.from_dict validates icp_industries and geographies
  are lists of strings (COPILOT-7)
- generator.py: Generator.__init__ takes only world_spec; config
  property derives from world_spec.config (single source of truth)
  (COPILOT-4)
- dataset_card.py: stub text changed to "Narrative unavailable for this
  dataset." (COPILOT-5); test updated to match

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 7 out of 8 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/narrative/dataset_card.py
@github-actions
Copy link
Copy Markdown

pr-agent-context report:

This is a refreshed snapshot of the current PR state.

This run includes an unresolved review comment on PR #5.

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/narrative/dataset_card.py:103
URL: https://github.com/leadforge-dev/leadforge/pull/5#discussion_r3115717615
Root author: copilot-pull-request-reviewer

Comment:
    The "Primary task" section is hard-coded to `converted_within_90_days` and a 90-day label definition. That matches the current shipped recipe, but it will drift once additional recipes/tasks exist (or if task parameters become configurable). Consider carrying `primary_task` (and any label-window parameter) into `WorldSpec` during `Generator.from_recipe()` so the dataset card renders from resolved spec rather than literals.
    ~~~suggestion
        primary_task = getattr(
            world_spec,
            "primary_task",
            getattr(cfg, "primary_task", "converted_within_90_days"),
        )
        label_window_days = getattr(
            world_spec,
            "label_window_days",
            getattr(cfg, "label_window_days", 90),
        )

        lines += [
            "## Primary task",
            "",
            f"**Task:** `{primary_task}`",
            "",
            "**Label definition:** A lead is considered converted if a `closed_won` event "
            f"is recorded within {label_window_days} days of the lead's snapshot anchor date. "
    ~~~

Run metadata:

Tool ref: v4
Tool version: 4.0.18
Trigger: review posted
Workflow run: 24709536777 attempt 2
Comment timestamp: 2026-04-21T10:20:00.848803+00:00
PR head commit: 8c933bd13c1849adaac375625a605016fd2b0c9a

@shaypal5 shaypal5 merged commit d668146 into main Apr 21, 2026
10 checks passed
@shaypal5 shaypal5 deleted the feat/milestone-2-narrative branch April 21, 2026 11:09
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: narrative narrative/ vertical story layer type: feature New capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants