This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Product Model is a structured MDX-based grammar for bridging product intent (PRDs) and code. PMs author .product.mdx files using typed blocks; tooling validates, parses, and builds JSON ASTs.
Core philosophy: "Product Manager should write Product Model, not code" (PMPM).
pnpm install # Install all dependencies
pnpm build # Build all packages (tsup, via Turborepo)
pnpm typecheck # TypeScript type check across all packages
pnpm lint # Biome lint + format check
pnpm lint:fix # Auto-fix lint and formatting
pnpm test # Run all Vitest tests
# Run a single test file
cd packages/core && npx vitest run tests/schema.test.ts
# Run tests matching a pattern
cd packages/core && npx vitest run -t "BlockIdSchema"Build must succeed before typecheck and test (configured in turbo.json via dependsOn: ["^build"]).
Monorepo: pnpm workspaces + Turborepo. Two packages:
packages/core(@product-model/core) — schemas, parser, validatorpackages/cli(@product-model/cli) — citty-based CLI, depends on core viaworkspace:*
ESM-only — unified/remark require ESM. No CJS anywhere.
.product.mdx source string
→ unified + remark-parse + remark-mdx (MDX → MDAST)
→ remarkProductModel plugin (MDAST → ExtractedBlock[])
→ mdxToPmast() (ExtractedBlock[] → PMDocument via Zod parse)
→ validate() (PMDocument → ValidationResult)
remarkProductModel(parser/remark-product-model.ts) — custom remark plugin that walks MDAST, findsmdxJsxFlowElementnodes matching block types, extracts attributes and nested children intoExtractedBlock[], attaches tofile.data.extractedBlocksmdxToPmast(parser/mdx-to-pmast.ts) — transforms extracted blocks into a raw object, handles JSON-encodedfieldsattribute for Definition blocks, then runsPMDocumentSchema.parse()for Zod validationparse()(parser/index.ts) — public async API combining steps 1-2. RequiresParseOptionswithversionandtitle(document metadata is not in MDX frontmatter; it's passed via options/CLI args)validate()(validator/index.ts) — runs all rules against a PMDocument in all-errors mode (not fail-fast)
All TypeScript types are derived via z.infer<> in types/ast.ts. Never define standalone interfaces for block types — derive from schemas.
Schema hierarchy: primitives.ts → fields.ts → blocks.ts. Recursive types (SectionBlock, FeatureBlock children) use z.lazy() with explicit interface + double cast — this is the standard Zod workaround for recursive schemas.
GRAMMAR_TABLE in grammar.ts is a Record<BlockType, readonly BlockType[]> defining allowed parent→child relationships. Only Feature is allowed at document root (ROOT_BLOCK_TYPES). Policy may contain nested Logic blocks, while leaf blocks (Definition, Constraint, Link, Logic) have no children arrays.
Four built-in rules in validator/rules/:
grammar-rules— root block types + parent-child conformance to GRAMMAR_TABLEid-uniqueness— no duplicate block IDs across the documentversion-check— SemVer format on document and Definition versionslink-integrity— Linkfrom/totargets must reference existing block IDs
Each rule is (document: PMDocument) => ValidationDiagnostic[]. Add new rules to the RULES array in validator/index.ts.
Feature, Section, Definition, Policy, Constraint, Link, Logic
Enforced by Biome: tabs for indentation, double quotes, semicolons, line width 100.
Use pnpm changeset when changes affect published packages.