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
7 changes: 4 additions & 3 deletions .agent-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,10 @@ Documentation + CI:

| Item | Status | Rationale |
|---|---|---|
| M12: CLI `--json` flag | Deferred | No consumer needs it yet; add post-v4 |
| M12: CLI `--strict` flag | Deferred | Per-check control is better than global flag |
| M12: CLI help text polish | Deferred | Low priority vs dataset |
| M12: CLI `--json` flag | **Done** | `leadforge inspect --json`; `validate --json` deferred separately |
| M12: CLI `--strict` flag | Deferred | Per-check control is better than global flag; design call needed |
| M12: CLI `validate --json` | Deferred | Separate follow-up to inspect's --json |
| M12: CLI help text polish | **Done** | inspect surfaces v4 manifest fields; generate exposes `--snapshot-day`, `--primary-task`, `--label-window-days`; help strings tightened |
| M14: Sample bundle commit | Absorbed into v4-M2 | v4 dataset IS the sample |
| M14: Notebook 1 (inspecting world) | **Done** | `leadforge/examples/notebooks/01_inspect_world.ipynb` |
| M14: Notebook 2 (lead scoring baseline) | Deferred | v4 validation script covers this |
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ Format inspired by [Keep a Changelog](https://keepachangelog.com/).

## Unreleased

### CLI surfaces v4 fields

- `leadforge inspect` now prints `Primary task`, `Label window`,
`Snapshot day`, and `Redactions` for v3+ bundles, immediately after
`Schema ver`. Lines are omitted entirely on older v2 bundles —
no `?` placeholders. Snapshot day prints `(full horizon, no
windowing)` only when the manifest stores `null`; numeric values
(including `snapshot_day == horizon_days`) are printed verbatim.
- `leadforge inspect --json` / `-j` emits the parsed `manifest.json`
to stdout — the output is byte-equivalent JSON to the on-disk
manifest, suitable for `jq` pipelines.
- `leadforge generate` adds `--snapshot-day`, `--primary-task`, and
`--label-window-days` flags, threading directly to existing
`Generator.from_recipe()` kwargs. Recipe defaults still apply when
the flags are omitted.

### Bundle schema v4

`bundle_schema_version` bumped from `"3"` to `"4"`. Closes the final
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ leadforge generate \
# Inspect bundle metadata
leadforge inspect ./out/demo_bundle

# Or pipe the manifest into jq
leadforge inspect ./out/demo_bundle --json | jq .snapshot_day

# Validate bundle integrity
leadforge validate ./out/demo_bundle
```
Expand Down
32 changes: 28 additions & 4 deletions leadforge/cli/commands/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,32 @@ def generate(
"--difficulty",
help="Difficulty profile: intro, intermediate, or advanced.",
),
n_accounts: int | None = typer.Option(None, "--n-accounts", help="Number of accounts."),
n_contacts: int | None = typer.Option(None, "--n-contacts", help="Number of contacts."),
n_leads: int | None = typer.Option(None, "--n-leads", help="Number of leads."),
n_accounts: int | None = typer.Option(
None, "--n-accounts", help="Override recipe default account count."
),
n_contacts: int | None = typer.Option(
None, "--n-contacts", help="Override recipe default contact count."
),
n_leads: int | None = typer.Option(
None, "--n-leads", help="Override recipe default lead count."
),
horizon_days: int | None = typer.Option(
None, "--horizon-days", help="Simulation horizon in days."
None, "--horizon-days", help="Override recipe default simulation horizon in days."
),
primary_task: str | None = typer.Option(
None,
"--primary-task",
help="Override recipe default task identifier (e.g. converted_within_60_days).",
),
label_window_days: int | None = typer.Option(
None,
"--label-window-days",
help="Override recipe default label observation window in days.",
),
snapshot_day: int | None = typer.Option(
None,
"--snapshot-day",
help="Override recipe default snapshot day for windowed feature aggregation.",
),
override: str | None = typer.Option(
None, "--override", help="Path to a YAML config override file."
Expand Down Expand Up @@ -66,6 +87,9 @@ def generate(
n_contacts=n_contacts,
n_leads=n_leads,
horizon_days=horizon_days,
primary_task=primary_task,
label_window_days=label_window_days,
snapshot_day=snapshot_day,
override=override_dict,
)
except (LeadforgeError, ValueError) as exc:
Expand Down
34 changes: 34 additions & 0 deletions leadforge/cli/commands/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

Expand All @@ -13,6 +14,12 @@

def inspect(
bundle_path: str = typer.Argument(..., help="Path to a generated bundle directory."),
json_output: bool = typer.Option( # noqa: FBT001
False,
"--json",
"-j",
help="Emit the parsed manifest as JSON to stdout (pipe-friendly).",
),
) -> None:
"""Inspect a generated dataset bundle and print a summary."""
root = Path(bundle_path)
Expand All @@ -39,6 +46,10 @@ def inspect(
typer.echo("Error: manifest.json is not a JSON object", err=True)
raise typer.Exit(1)

if json_output:
typer.echo(json.dumps(manifest, indent=2))
return

typer.echo(f"Bundle: {root}")
typer.echo(f" Recipe: {manifest.get('recipe_id', '?')}")
typer.echo(f" Seed: {manifest.get('seed', '?')}")
Expand All @@ -48,6 +59,29 @@ def inspect(
typer.echo(f" Generated at: {manifest.get('generation_timestamp', '?')}")
typer.echo(f" Package: leadforge {manifest.get('package_version', '?')}")
typer.echo(f" Schema ver: {manifest.get('bundle_schema_version', '?')}")

# v3+ fields — only print rows for keys actually present in the manifest,
# so older (v2) bundles render cleanly without "?" placeholders.
if "primary_task" in manifest:
typer.echo(f" Primary task: {manifest['primary_task']}")
if "label_window_days" in manifest:
typer.echo(f" Label window: {manifest['label_window_days']} days")
if "snapshot_day" in manifest:
snapshot_day = manifest["snapshot_day"]
if snapshot_day is None:
typer.echo(" Snapshot day: (full horizon, no windowing)")
else:
typer.echo(f" Snapshot day: {snapshot_day} days")
if "redacted_columns" in manifest:
cols = manifest["redacted_columns"] or []
if cols:
noun = "column" if len(cols) == 1 else "columns"
if len(cols) <= 4:
names = ", ".join(cols)
else:
names = ", ".join(cols[:3]) + ", ..."
typer.echo(f" Redactions: {len(cols)} {noun} [{names}]")

typer.echo(f" Motif family: {manifest.get('motif_family', '?')}")

typer.echo("")
Expand Down
Loading
Loading