Skip to content

feat(brainstormer): .forge/product-vision.md + axes.yaml discovery + schema #122

@hadamrd

Description

@hadamrd

Problem

The Product Brainstormer (epic #121) needs a typed, validated "north star" describing what the product is for and which axes of value count as real progress. Today no such structured input exists — the brainstormer would either invent goals or have to be re-briefed every run. We need a discovery layer that pulls this from .forge/product-vision.md (free-form prose) and .forge/axes.yaml (structured, schema-checked). Without both, the brainstormer must refuse to start, loudly, rather than silently fabricate direction.

Example: an operator runs the brainstormer in a repo missing .forge/axes.yaml. Today: nothing stops it from spinning up a generic "improve the codebase" agent. Required behaviour: MissingVisionError: axes.yaml not found at /repo/.forge/axes.yaml — and the brainstormer halts before any LLM call.

Acceptance criteria

  • New module src/forge_loop/product_vision.py exposes ProductVision, Axis, MissingVisionError, and discover(repo_path: Path) -> ProductVision.
  • Axis is a pydantic v2 BaseModel with required fields: name: str, customer: str, valuable_means: str, acceptable_work: list[str] (min_length=1), rejected_as_cosmetic: list[str] (may be empty).
  • ProductVision is a pydantic v2 BaseModel holding vision_markdown: str (raw markdown text) and axes: list[Axis] (min_length=1).
  • discover(repo_path) reads <repo_path>/.forge/product-vision.md + <repo_path>/.forge/axes.yaml, validates both, and returns a typed ProductVision. Returns the object (never None) on success — missing/invalid inputs raise.
  • MissingVisionError is raised with a specific reason string for each failure mode: missing vision file, missing axes file, axes.yaml not valid YAML, axes.yaml schema-invalid (mention the offending field path).
  • Error messages include the absolute path of the file that failed so operators can fix without grep.
  • Empty product-vision.md (zero non-whitespace chars) is treated as invalid with reason "vision markdown is empty".
  • Public surface is re-exported in src/forge_loop/__init__.py if other modules already follow that pattern (investigate); otherwise leave as submodule import.

Test matrix

Location: tests/test_product_vision.py, fixtures under tests/fixtures/product_vision/.

Unit tests (happy paths):

  • valid_minimal/ — single axis, minimal required fields → discover() returns ProductVision with one Axis, vision_markdown equals the file content verbatim.
  • valid_full/ — multi-axis fixture with rich markdown and non-empty rejected_as_cosmetic → validates and round-trips all fields.

Unit tests (sad paths, all must raise MissingVisionError with the documented reason substring):

  • missing_vision/ — only axes.yaml present → reason mentions "product-vision.md" and the path.
  • missing_axes/ — only product-vision.md present → reason mentions "axes.yaml".
  • empty_vision/ — vision file exists but is whitespace only → reason "vision markdown is empty".
  • invalid_axes_yaml/ — axes.yaml is not parseable YAML (e.g. unbalanced brackets) → reason mentions YAML parse failure.
  • invalid_axes_schema/ — axes.yaml parses but an axis is missing customer → reason mentions the missing field customer.
  • empty_axes_list/ — axes.yaml has axes: [] → reason mentions axes list cannot be empty.
  • acceptable_work_empty/ — axis with acceptable_work: [] → reason mentions acceptable_work min length.

Adversarial:

  • discover() on a path where .forge/ itself does not exist → MissingVisionError (not FileNotFoundError leakage).
  • Symlinked .forge/ directory pointing at a valid fixture → resolves and validates.
  • Extra unknown keys in axes.yaml entries → either ignored or rejected; the test pins the chosen behaviour (recommended: ignore for forward-compat).

No integration/e2e tests required at this stage — the brainstormer caller is out of scope for this ticket.

Out of scope

  • Wiring discover() into the brainstormer CLI / runtime (separate ticket under epic epic: customer-tunable Product Brainstormer / PO Master (vision + axes drive every ticket) #121).
  • Auto-generating .forge/product-vision.md or .forge/axes.yaml (no scaffolder here).
  • Editing the brief templates in src/forge_loop/briefs/.
  • Adding new pydantic versions or migrating others — use whatever pydantic version is already pinned in pyproject.toml.
  • Caching, file-watching, or hot-reload of vision files.
  • Multi-repo / monorepo discovery — single repo_path argument only.

File pointers

  • src/forge_loop/product_vision.py (new)
  • tests/test_product_vision.py (new)
  • tests/fixtures/product_vision/ (new tree — one subdir per fixture above, each containing a .forge/ dir)
  • src/forge_loop/__init__.py (investigate — only touch if existing pattern re-exports public modules)
  • pyproject.toml (investigate — confirm pyyaml and pydantic are already deps; if pyyaml is missing, add it)

Original report

Part of #121 — Product Brainstormer epic.

What

Auto-discover .forge/product-vision.md (free-form markdown) and .forge/axes.yaml (structured pydantic schema) at brainstormer startup. Missing or invalid files = hard refusal with a clear error.

Acceptance

  • New forge_loop/product_vision.py module.
  • ProductVision pydantic model loads .forge/product-vision.md (markdown, free-form) + .forge/axes.yaml (structured).
  • Axis pydantic model with required fields: name, customer, valuable_means, acceptable_work: list[str], rejected_as_cosmetic: list[str].
  • discover(repo_path) -> ProductVision | None reads both, validates, returns the typed object.
  • MissingVisionError raised when either file is missing or invalid.
  • Tests: discovery against fixtures (full / missing-vision / missing-axes / invalid-axes-yaml / valid-minimal).

File pointers

  • src/forge_loop/product_vision.py (new)
  • tests/test_product_vision.py (new)
  • tests/fixtures/product_vision/ (new)

Metadata

Metadata

Assignees

No one assigned

    Labels

    po:expandedPO subagent expanded the issue bodypriority:p1Important, near-term

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions