AI-assisted coding with semantic guardrails
Quick Start • How It Works • Examples • Packages • Contributing
Blocks is a development-time validator that lets AI agents write code freely while maintaining consistency through domain semantics and multi-layer validation. Think of it as a type system for your domain logic—except it uses LLMs to check semantic alignment, not just syntax.
You're using Claude Code, Cursor, or another AI coding tool. The agent generates code fast, but:
- No shared understanding - The AI doesn't know your domain concepts (what's a "Resume"? what makes HTML "semantic"?)
- Inconsistent output - Each generation drifts from your design patterns
- No feedback loop - The AI can't learn from mistakes without manual correction
- Runtime surprises - Bugs emerge when generated code violates implicit rules
You need a way to teach the AI your domain and validate before runtime.
Blocks provides:
- Domain specification in
blocks.yml- Define entities, signals, measures, and rules once - Multi-layer validation - Schema (fast) → Shape (fast) → Domain (AI-powered)
- Semantic feedback loop - AI writes code → Blocks validates → AI learns from feedback
- Drift detection - Spots when code diverges from spec, you decide: fix code or update spec
Not a framework. Not a runtime library. A development-time validator that keeps AI-generated code aligned with your domain.
name: "Resume Themes"
# AI Configuration
ai:
provider: "openai"
model: "gpt-4o-mini"
philosophy:
- "Themes must use semantic HTML and be accessible"
- "All layouts must be responsive"
domain:
entities:
resume:
description: "A resume document"
fields: [basics, work, education, skills]
signals:
readability:
description: "How easy is the resume to scan?"
extraction_hint: "Look for clear headings, whitespace, logical flow"
measures:
valid_html:
constraints:
- "Must use semantic tags (header, main, section)"
- "Must include ARIA labels"
blocks:
domain_rules: # Apply to ALL blocks by default
- id: semantic_html
description: "Use semantic HTML5 tags"
- id: accessibility
description: "Include proper ARIA labels"
theme.modern_professional:
type: theme
description: "Modern professional resume theme"
path: "themes/modern-professional"
inputs:
- name: resume
type: entity.resume
description: "Resume data to render"
outputs:
- name: html
type: string
measures: [valid_html]
constraints:
- "Must produce valid semantic HTML"
# Inherits blocks.domain_rules automatically
# Validators (optional - defaults to domain only)
# Uncomment to run all validators:
# validators:
# - schema # Fast config structure checks
# - shape.ts # File structure validation
# - domain # AI-powered semantic validation// themes/modern-professional/block.ts
export function modernProfessionalTheme(resume: Resume) {
if (!resume.basics?.name) {
throw new Error("Resume must include name");
}
return { html: template(resume) };
}$ blocks run theme.modern_professionalOutput (with all validators enabled):
📦 Validating: theme.modern_professional
✓ schema ok (inputs/outputs match spec)
✓ shape.ts ok (files exist, exports present)
✓ domain ok (semantic HTML found in template source)
✅ Block "theme.modern_professional" passed all validations
Note: By default, only the domain validator runs. To run all validators, add:
validators:
- schema
- shape.ts
- domainIf validation fails, Blocks provides actionable feedback:
📦 Validating: theme.modern_professional
✓ schema ok
✓ shape ok
⚠ [domain] Template missing semantic HTML tags
→ Suggestion: Replace <div class="header"> with <header role="banner">
⚠ [domain] No ARIA labels found in work experience section
→ Suggestion: Add aria-label="Work Experience" to section
❌ Block "theme.modern_professional" has warnings
The AI reads this output, updates the code, and re-runs validation until it passes.
- Node.js ≥20.0.0
- pnpm (recommended) or npm
- OpenAI API key (for AI-powered domain validation)
# Install CLI globally
npm install -g @blocksai/cli
# Or use in project
npm install --save-dev @blocksai/cli# Create blocks.yml
blocks initThis generates a starter blocks.yml:
# Blocks Configuration
# Domain: Define your project's semantic domain model
name: "My Blocks Project"
root: "blocks" # Default directory for blocks
# AI Configuration (optional)
# Defaults to OpenAI gpt-4o-mini if not specified
ai:
provider: "openai" # Options: openai, anthropic, google
model: "gpt-4o-mini" # OpenAI: gpt-4o-mini, gpt-4o | Anthropic: claude-3-5-sonnet-20241022
# Philosophy statements guide AI validation
philosophy:
- "Blocks must be small, composable, and deterministic"
- "Express domain intent clearly in code"
- "Validate at development time, trust at runtime"
# Domain semantics
domain:
entities:
user:
description: "A user in the system"
fields:
- id
- name
- email
signals:
user_engagement:
description: "Measures how engaged a user is with the system"
extraction_hint: "Look for login frequency, feature usage, interaction patterns"
measures:
score_0_1:
constraints:
- "Value must be between 0 and 1"
# Block definitions
blocks:
# Default domain rules for all blocks
domain_rules:
- id: clear_intent
description: "Block implementation must clearly express domain intent"
example_block:
type: function
description: "Example block that computes user engagement score"
path: "blocks/example-block"
inputs:
- name: user
type: entity.user
outputs:
- name: result
type: entity.result
measures: [score_0_1]
# Discovery configuration
targets:
kind: "block"
discover:
root: "blocks"Domain validation uses OpenAI by default. Set your API key:
export OPENAI_API_KEY="sk-..."Or create .env:
OPENAI_API_KEY=sk-...
Get a key at platform.openai.com/api-keys
mkdir -p blocks/example-blockblocks/example-block/block.ts:
interface User {
id: string;
name: string;
email: string;
}
interface Result {
value: number;
score: number;
}
export function exampleBlock(user: User): { result: Result } {
// Calculate engagement score (0-1 range)
const score = user.email.includes("@") ? 0.8 : 0.3;
return {
result: {
value: 1,
score
}
};
}blocks/example-block/index.ts:
export { exampleBlock } from "./block.js";blocks run example_blockOutput:
📦 Validating: example_block
✓ schema ok
✓ shape ok
✓ domain ok
✅ Block "example_block" passed all validations
This is where Blocks shines. Here's a simulated session showing how Claude Code + Blocks work together:
User:
Build a weather dashboard block that takes a city and returns HTML with current weather
Claude Code:
I'll help you build that. Let me first read
blocks.ymlto understand your domain.
$ cat blocks.ymlClaude Code:
I see your project defines entities and domain rules. I'll create a new block called
weather_dashboard. Let me add it to the spec first:
# Claude edits blocks.yml
blocks:
weather_dashboard:
description: "Weather dashboard with current conditions"
inputs:
- name: city
type: string
outputs:
- name: html
type: string
measures: [valid_html]Claude Code:
Now I'll implement the block:
// blocks/weather_dashboard/block.ts
export async function weatherDashboard(city: string) {
const weather = await fetchWeather(city);
return {
html: `
<div class="weather">
<h1>${city}</h1>
<p>${weather.temp}°F</p>
<p>${weather.condition}</p>
</div>
`
};
}// blocks/weather_dashboard/index.ts
export { weatherDashboard } from "./block.js";Claude Code:
Let me validate this:
$ blocks run weather_dashboardOutput:
📦 Validating: weather_dashboard
✓ schema ok
✓ shape ok
⚠ [domain] HTML output uses non-semantic tags
→ Suggestion: Replace <div class="weather"> with semantic tags like <article> or <section>
⚠ [domain] Missing ARIA labels for accessibility
→ Suggestion: Add role and aria-label attributes
❌ Block "weather_dashboard" has warnings
Claude Code:
I see the validation issues. The HTML isn't using semantic tags. Let me fix that:
// blocks/weather_dashboard/block.ts (updated)
export async function weatherDashboard(city: string) {
const weather = await fetchWeather(city);
return {
html: `
<article role="region" aria-label="Weather Dashboard">
<header>
<h1>${city} Weather</h1>
</header>
<section aria-label="Current Conditions">
<p><strong>Temperature:</strong> ${weather.temp}°F</p>
<p><strong>Conditions:</strong> ${weather.condition}</p>
</section>
</article>
`
};
}Claude Code:
Running validation again:
$ blocks run weather_dashboardOutput:
📦 Validating: weather_dashboard
✓ schema ok
✓ shape ok
✓ domain ok
✅ Block "weather_dashboard" passed all validations
Claude Code:
Perfect! The block now passes all validations. It uses semantic HTML (
<article>,<header>,<section>) and includes proper ARIA labels for accessibility.
This is the feedback loop:
- AI writes code based on domain spec
- Blocks validates and provides specific feedback
- AI reads validation output and fixes issues
- Repeat until validation passes
The AI learns your domain semantics through validation feedback.
Let's see a more complex scenario: building 5 resume themes at once.
User:
Build 5 resume themes: modern, creative, minimal, professional, and tech. They should all follow our domain rules.
Claude Code:
I'll create 5 themes. Since they share domain rules, I'll define them once in
blocks.yml:
blocks:
domain_rules: # Inherited by ALL themes
- id: semantic_html
description: "Use semantic HTML5 tags (header, main, section, article)"
- id: accessibility
description: "Include ARIA labels and roles"
- id: responsive
description: "Include media queries for mobile"
theme.modern:
description: "Modern theme with bold typography"
path: "themes/modern"
# Inherits domain_rules automatically
theme.creative:
description: "Creative theme with artistic layout"
path: "themes/creative"
theme.minimal:
description: "Minimal theme with clean design"
path: "themes/minimal"
theme.professional:
description: "Traditional professional theme"
path: "themes/professional"
theme.tech:
description: "Tech-focused theme with code aesthetics"
path: "themes/tech"Claude Code:
Now I'll implement all 5 themes. Each will have
block.ts,template.hbs, andindex.ts.
# Claude creates directory structure
mkdir -p themes/{modern,creative,minimal,professional,tech}Claude Code:
Creating theme.modern:
// themes/modern/block.ts
export function modernTheme(resume: Resume) {
if (!resume.basics?.name) throw new Error("Name required");
return { html: template(resume) };
}Claude Code:
I'll create the other 4 themes similarly... (creates all implementations)
Now let me validate all themes at once:
$ blocks run --allOutput:
📦 Validating: theme.modern
✓ schema ok
✓ shape ok
✓ domain ok
📦 Validating: theme.creative
✓ schema ok
✓ shape ok
⚠ [domain] Missing responsive media queries
→ Suggestion: Add @media queries for mobile viewports
📦 Validating: theme.minimal
✓ schema ok
✓ shape ok
✓ domain ok
📦 Validating: theme.professional
✓ schema ok
✓ shape ok
✓ domain ok
📦 Validating: theme.tech
✓ schema ok
✓ shape ok
⚠ [domain] Section missing ARIA label
→ Suggestion: Add aria-label to <section> for skills
❌ 2 blocks have warnings
Claude Code:
I see 2 themes need fixes. Let me update them:
Claude Code:
Running validation again:
$ blocks run --allOutput:
📦 Validating all blocks...
✓ theme.modern - passed
✓ theme.creative - passed
✓ theme.minimal - passed
✓ theme.professional - passed
✓ theme.tech - passed
✅ All 5 blocks passed validation
Claude Code:
All themes now pass validation! They all follow the domain rules (semantic HTML, accessibility, responsiveness) that were defined once in
blocks.yml.
Key advantages:
- DRY domain rules - Define once, inherit everywhere
- Batch validation - Check all blocks with
--all - Consistent quality - AI follows same rules across all implementations
- Fast iteration - Validation catches issues before runtime
Blocks uses a three-layer validation pipeline:
Validates that inputs/outputs match blocks.yml specification.
✓ Block has all required inputs
✓ Output types match spec
Validates file structure and exports.
✓ index.ts exists and exports block function
✓ block.ts exists
✓ All required files present
This is the magic. The domain validator:
- Reads ALL files in the block directory (block.ts, template.hbs, styles, etc.)
- Passes complete source to AI with context:
- Project philosophy statements
- Domain entities, signals, measures
- Domain rules
- Block specification
- AI analyzes source code (not output!) for semantic compliance
- Returns actionable feedback
Example AI prompt:
Project Philosophy:
- "Themes must use semantic HTML and be accessible"
Domain Rules:
- semantic_html: "Use semantic HTML5 tags (header, main, section)"
- accessibility: "Include ARIA labels and roles"
Block Files:
--- block.ts ---
export function modernTheme(resume: Resume) { ... }
--- template.hbs ---
<header role="banner">
<h1>{{basics.name}}</h1>
</header>
Analyze these files. Does the template.hbs use semantic HTML tags?
Does it include ARIA labels? Return specific issues if not.
Why validate source, not output?
Templates are deterministic. If template.hbs passes validation once, it will ALWAYS generate correct HTML. No need to parse output at runtime.
CRITICAL: Blocks validates SOURCE CODE at development time, NOT runtime behavior.
Development Time (Blocks): Runtime (Your App):
┌─────────────────────┐ ┌──────────────────┐
│ Read source files │ │ Validate input │
│ (block.ts, .hbs) │ │ data only │
├─────────────────────┤ ├──────────────────┤
│ AI analyzes source │ │ Execute function │
│ for semantics │ │ │
├─────────────────────┤ ├──────────────────┤
│ Report issues │ │ Return output │
│ │ │ │
└─────────────────────┘ └──────────────────┘
↑ ↑
│ │
Trust validated Trust code
source is correct was validated
Consequence: Block implementations stay simple (~20 lines). All semantic validation happens at development time.
Blocks is a monorepo with focused packages:
- @blocksai/cli - Command-line interface
- @blocksai/schema - blocks.yml parser (Zod schemas)
- @blocksai/domain - Domain modeling and static analysis
- @blocksai/validators - Validator implementations
- @blocksai/ai - Multi-provider AI abstraction (OpenAI, Anthropic, Google)
Problem: Need to generate multiple resume themes that are all semantic, accessible, and responsive.
Solution: Define domain rules once, let AI generate themes, validate with Blocks.
See: examples/json-resume-themes/
Domain:
- Entity:
resume(JSON Resume schema) - Measure:
valid_html(semantic tags, ARIA labels, responsive) - Rules: semantic_html, accessibility, responsive_design
Validation: Blocks reads Handlebars template source, AI checks for semantic HTML patterns.
Problem: Blog posts should have humor, conversational tone, and proper structure.
Solution: Define content quality as domain constraints, validate markdown files.
See: examples/blog-content-validator/
Domain:
- Signals:
humor_presence,conversational_tone,biology_reference - Rules: humor_required, no_corporate_speak
Validation: Blocks reads markdown file, AI analyzes content for domain signals.
name: string # Project name
root?: string # Default directory for blocks (optional, defaults to "blocks")
# AI Configuration (optional - defaults to OpenAI gpt-4o-mini)
ai:
provider: openai | anthropic | google
model: string # OpenAI: gpt-4o-mini, gpt-4o
# Anthropic: claude-3-5-sonnet-20241022, claude-3-5-haiku-20241022
# Google: gemini-1.5-flash, gemini-1.5-pro
apiKey?: string # Or use env var (OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY)
philosophy:
- string # Design principles (guide AI validation)
domain:
entities:
[name]:
description: string # What this entity represents
fields: string[] # Entity fields
signals:
[name]:
description: string # What to detect/extract
extraction_hint?: string # How to extract/detect it
measures:
[name]:
constraints: string[] # Validation constraints
blocks:
domain_rules: # Default rules inherited by ALL blocks
- id: string
description: string
[name]:
type?: string # Optional type hint (function, theme, validator, etc.)
description: string # What this block does
path?: string # Custom directory (default: blocks/[name])
inputs?:
- name: string
type: string # Can reference: entity.*, string, number, etc.
description?: string # Input description
optional?: boolean
outputs?:
- name: string
type: string
measures?: string[] # Reference to domain.measures
constraints?: string[] # Additional constraints
domain_rules?: # Override defaults completely for this block
- id: string
description: string
validators?: # Optional - defaults to ["domain"] if omitted
- string # Built-in validator short name (e.g., "schema", "shape.ts", "domain")
OR
- name: string # Custom validator
run: string # Validator ID to execute
config?: any # Optional validator configuration# AI Provider API Keys
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_API_KEY=...TypeScript validates syntax and types. Blocks validates domain semantics.
Example:
- TypeScript: "This function returns a string" ✓
- Blocks: "This HTML string uses semantic tags and includes ARIA labels" ✓
Blocks operates at a higher level of abstraction.
No. Blocks is a development-time validator. Run it during development, not in production.
Your application just imports and executes the validated functions. No Blocks runtime overhead.
Schema and shape validation still work (fast, deterministic checks). You'll miss domain validation (AI-powered semantic checks).
Yes! Configure in blocks.yml:
ai:
provider: anthropic
model: claude-3-5-sonnet-latestBlocks uses Vercel AI SDK v6 internally, supporting OpenAI, Anthropic, and Google.
Domain validation uses AI, so there's a cost per validation:
- ~2-5 cents per block with GPT-4o-mini (default)
- ~0.5-1 cent per block with GPT-3.5-turbo
For 100 blocks: ~$2-5 per full validation run.
Use --all sparingly in large projects. Validate incrementally as you develop.
Not yet, but planned. Future: domain_rules: [] to opt out.
For now, domain validation falls back to warnings on failure (doesn't block).
No. Blocks only reads and validates. It never writes code.
AI agents (like Claude Code) read Blocks' validation output and modify code based on feedback.
# In CI pipeline
npm install -g @blocksai/cli
export OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}
blocks run --all
# Exits with error code if validation failsWorks with GitHub Actions, GitLab CI, etc.
Domain validation uses AI (slow). Strategies:
- Validate incrementally -
blocks run <name>after each change - Skip in CI - Only validate on pre-merge, not every commit
- Use faster models - gpt-4o-mini instead of gpt-4o
- Cache results - Future: validation result caching (planned)
Yes! Implement the Validator interface:
import { Validator, ValidatorContext, ValidationResult } from '@blocksai/validators';
export class MyValidator implements Validator {
id = "custom.my_validator.v1";
async validate(ctx: ValidatorContext): Promise<ValidationResult> {
// Your logic
return { valid: true, issues: [] };
}
}Add to CLI pipeline. See CLAUDE.md for details.
- Core schema and domain modeling
- Multi-layer validation (schema, shape, domain)
- AI-powered semantic validation
- Multi-provider support (OpenAI, Anthropic, Google)
- CLI with
initandruncommands - Validation result caching
- Lint validators (ESLint, Prettier)
- Chain validators (multi-step pipelines)
- Output validators (render + validate)
- Auto-healing (AI proposes fixes)
- VSCode extension
- GitHub Action for CI
Contributions welcome! See CLAUDE.md for architecture details.
# Setup
git clone https://github.com/yourusername/blocks
cd blocks
pnpm install
pnpm build
# Make changes
cd packages/validators
# ... edit code ...
pnpm build
# Test
cd examples/json-resume-themes
blocks run --all
# Create changeset
pnpm changesetBlocks draws from:
- Cube.dev - Semantic data modeling for analytics
- Malloy - Semantic layer for SQL
- PDDL - Planning domain definition language
- Type systems - But for domain semantics, not syntax
MIT
- Documentation - Architecture and guides
- Examples - Real-world usage
- NPM Package - Install the CLI
Built for the age of agentic coding.
If you're using Claude Code, Cursor, or any AI coding tool, Blocks helps you maintain consistency and teach your AI agents domain semantics through validation feedback.
Star if useful. Issues and PRs welcome.