Skip to content

Commit 80cef9f

Browse files
committed
Document production-ready ramp and add run diagnostics
Update docs around the project goal: liquid-spec is an implementation harness for gradually building a full production-ready Liquid engine. Align AGENTS/CLAUDE, README, SPECS, generated init docs, and the complexity guide with the current 0-1000 ramp: trivial passthrough first, then literals, variables, control flow, standard features, compatibility quirks, and production recordings. Add dumb-adapter audit guidance and warn that raw pass counts can be misleading for naive implementations; Max complexity reached is the better progress signal. Document using source-echo, always-empty, and always-raise adapters to catch weak specs and confusing failures. Add runner diagnostics: - --list-passed prints passing specs with complexity/source for ramp audits. - --json emits a single non-benchmark JSON summary with totals, failures, skipped suites, max complexity, and optional passed specs. Also include a well-hinted complexity-500 self[...] nested-loop scope spec for dynamic lookup publication across nested loops.
1 parent 18eca66 commit 80cef9f

7 files changed

Lines changed: 468 additions & 250 deletions

File tree

CLAUDE.md

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## What This Project Is
66

7-
liquid-spec is a test suite and CLI for testing Liquid template implementations. It captures test cases from the reference Shopify/liquid implementation and can verify that any Liquid implementation produces correct output.
7+
liquid-spec is a test suite and CLI for testing Liquid template implementations. Its purpose is to act as a harness for the gradual construction of a full, production-ready Liquid implementation: start with trivial passthrough specs, then progressively add variables, filters, control flow, partials, compatibility quirks, and finally production/theme recordings. It captures test cases from the reference Shopify/liquid implementation and can verify that any Liquid implementation produces correct output.
88

99
## CLI Usage
1010

@@ -38,6 +38,25 @@ liquid-spec my_adapter.rb -l
3838
liquid-spec my_adapter.rb --list-suites
3939
```
4040

41+
42+
### Dumb Adapter Ramp Audits
43+
44+
When changing early complexity scores or adding beginner specs, play dumb and verify the harness still behaves like an implementation curriculum:
45+
46+
```bash
47+
# Source echo adapter: should pass only raw text, then fail on first object output
48+
liquid-spec /tmp/echo_adapter.rb -s basics --max-failures 3 --list-passed
49+
50+
# Always-empty adapter: may pass many empty-output specs accidentally; check max complexity
51+
liquid-spec /tmp/empty_adapter.rb -s basics --json --list-passed > /tmp/empty-results.json
52+
53+
# Always-raise adapters: should fail at complexity 0 with clear Error + Hint output
54+
liquid-spec /tmp/raise_compile_adapter.rb -s basics --max-failures 3
55+
liquid-spec /tmp/raise_render_adapter.rb -s basics --max-failures 3
56+
```
57+
58+
Use `--list-passed` to inspect accidental passes and `--json` for tooling. Prefer `Max complexity reached` / `max_complexity_reached` over raw pass count when judging partial or deliberately naive adapters.
59+
4160
### Result Logging
4261

4362
Each test run appends results to `/tmp/liquid-spec-results.jsonl` with the format:
@@ -73,10 +92,11 @@ LiquidSpec.setup do |ctx|
7392
end
7493

7594
LiquidSpec.configure do |config|
76-
config.suite = :liquid_ruby # :all, :liquid_ruby, :shopify_theme_dawn
77-
config.features = [
78-
:core, # Basic Liquid parsing/rendering
79-
]
95+
config.suite = :liquid_ruby # :all, :basics, :liquid_ruby, :shopify_theme_dawn
96+
97+
# Declare what your adapter cannot support yet. Specs requiring these
98+
# features are skipped so you can build incrementally.
99+
config.missing_features = [:shopify_tags, :shopify_filters]
80100
end
81101

82102
LiquidSpec.compile do |ctx, source, parse_options|
@@ -230,6 +250,17 @@ Spec-level settings override source-level settings. For example, a spec with its
230250

231251
## Good Specs
232252

253+
Good specs preserve the project goal: help someone build a production-ready Liquid implementation gradually. A spec should teach one behavior at the right time, fail with an actionable message, and point to implementation guidance when the behavior is not obvious.
254+
255+
### Ramp discipline
256+
257+
- First-contact specs for a feature must be tiny, gentle, and hinted. If needed, score the first spec one point lower than follow-up specs so it appears first.
258+
- Keep the 0-50 band boring: passthrough, literals, missing variables, simple variable lookup, a few simple filters, and assign.
259+
- Keep whitespace control (`{{-`, `-}}`, `{%-`, `-%}`), drop/to_liquid boundaries, generated filter matrices, parser recovery, date/time quirks, and filesystem/security quirks out of the beginner band.
260+
- Generated specs should not flood the early ramp. Prefer curated beginner specs early; generated compatibility breadth generally starts at 120+ or much later.
261+
- If a dumb adapter that returns the input, returns `""`, or raises for everything passes a spec unexpectedly, either the spec is too weak or the complexity/hint needs review.
262+
- When judging naive adapters, prefer `Max complexity reached` over total pass count. An always-empty adapter can pass later specs whose correct output is empty, but it should not advance through the contiguous ramp.
263+
233264
### Error specs: prefer raised errors over inline errors
234265

235266
Most specs that exercise an error path should let the error **raise** and
@@ -376,20 +407,20 @@ defaults:
376407
377408
### Complexity Scoring
378409
379-
Each spec should have a `complexity` field indicating implementation difficulty. Lower scores = simpler features to implement first. Specs without explicit complexity default to 1000 or the suite's `minimum_complexity`.
410+
Each spec should have a `complexity` field indicating implementation difficulty. Lower scores = simpler features to implement first. Specs without explicit complexity default to 1000 or the suite's `minimum_complexity`. Complexity is capped at 1000; do not score specs above 1000.
380411

381412
| Range | Feature |
382413
|-------|---------|
383-
| 10-20 | Literals, raw text output |
384-
| 30-50 | Variables, filters, assign |
385-
| 55-60 | Whitespace control, if/else/unless |
386-
| 70-80 | For loops, operators, filter chains |
387-
| 85-100 | Math filters, forloop object, capture, case/when |
388-
| 105-130 | String filters, increment, comment, raw, echo, liquid tag |
389-
| 140-180 | Array filters, property access, truthy/falsy, cycle, tablerow |
390-
| 190-220 | Advanced: offset:continue, parentloop, partials |
391-
| 300-500 | Edge cases, deprecated features |
392-
| 1000+ | Production recordings, unscored specs (default) |
414+
| 0-1 | Foundation: empty template, literal passthrough, whitespace/newline preservation |
415+
| 5-20 | First object output and literal breadth: strings, numbers, booleans, nil-as-empty |
416+
| 30-50 | Variables, missing variables, very simple filters, assign |
417+
| 55-65 | Basic if/else/unless and simple boolean composition |
418+
| 70-100 | Gentle loops, comparisons, forloop basics, capture, simple case/when |
419+
| 105-150 | Common filters/tags: string filters, comment/raw, increment, interrupts, loop modifiers, whitespace control |
420+
| 160-220 | Generated filter breadth, truthy/falsy edge cases, cycle/tablerow, first partials/filesystem, Ruby/reference quirks |
421+
| 230-400 | Long-tail standard behavior: advanced lookup, parser edge cases, scope/filesystem interactions |
422+
| 500-900 | Mature compatibility: parser mutation matrices, recursion/resource limits, security-sensitive quirks, date/time/Ruby quirks |
423+
| 1000 | Production recordings and unscored specs (default) |
393424

394425
See [`liquid-spec docs complexity`](`liquid-spec docs complexity`) for the full guide with examples.
395426
See [SPECS.md](SPECS.md) for guidelines on writing effective specs.
@@ -402,41 +433,33 @@ See [SPECS.md](SPECS.md) for guidelines on writing effective specs.
402433

403434
### Features
404435

405-
Adapters declare which features they support. Suites and individual specs can require specific features:
436+
Adapters declare which features they do **not** support yet. Suites and individual specs can require capabilities; any required capability listed in `missing_features` is skipped so implementations can grow incrementally:
406437

407438
```ruby
408439
LiquidSpec.configure do |config|
409-
config.features = [:core, :shopify_tags]
440+
# Empty means "try every spec". Add unsupported capabilities here.
441+
config.missing_features = [:shopify_tags]
410442
end
411443
```
412444

413445
### Available Features
414446

415-
The `:core` feature is the recommended target for most implementations. It's an alias that automatically expands to include other essential features:
416-
417-
```ruby
418-
# From lib/liquid/spec/cli/adapter_dsl.rb
419-
FEATURE_EXPANSIONS = {
420-
core: [:runtime_drops, :inline_errors],
421-
}
422-
```
423-
424-
**Core features (most implementations should declare `:core`):**
425-
- `:core` - Full Liquid implementation. Expands to include `:runtime_drops` and `:inline_errors`
426-
- `:runtime_drops` - Supports bidirectional communication for drop callbacks (test harness invokes adapter to access drop properties)
427-
- `:inline_errors` - Errors are rendered inline in output rather than raised as exceptions
428-
- `:strict_parsing` - Supports error_mode: :strict (default for most implementations)
447+
Feature selection is denylist-based. Leave `missing_features` empty to try everything, or add unsupported capabilities while the implementation is still growing.
429448

430-
**Optional features:**
431-
- `:lax_parsing` - Supports error_mode: :lax for lenient parsing
432-
- `:ruby_types` - Supports Ruby-specific types in environment (Integer, Float, Range, etc.)
449+
**Common features to list in `missing_features`:**
450+
- `:runtime_drops` - Adapter cannot support bidirectional drop callbacks yet
451+
- `:inline_errors` - Adapter cannot render errors inline yet
452+
- `:lax_parsing` - Adapter does not support `error_mode: :lax`
453+
- `:ruby_types` / `:ruby_drops` / `:binary_data` - Adapter cannot consume Ruby-specific values from specs
454+
- `:template_factory` - Adapter cannot support template factory/artifact callbacks
433455

434456
**Shopify-specific features:**
435457
- `:shopify_tags` - Shopify-specific tags (schema, style, section)
436458
- `:shopify_objects` - Shopify-specific objects (section, block)
437459
- `:shopify_filters` - Shopify-specific filters (asset_url, image_url)
460+
- `:shopify_includes`, `:shopify_blank`, `:shopify_error_handling`, `:shopify_error_format`, `:shopify_string_access` - Shopify platform/theme behavior beyond portable Liquid
438461

439-
**JSON-RPC adapters** that can't support bidirectional communication for runtime drops should declare `features = []` to opt out of `:core` and `:runtime_drops`. They will still run all specs except those requiring `:runtime_drops`.
462+
**JSON-RPC adapters** that can't support bidirectional communication for runtime drops should set `config.missing_features = [:runtime_drops, :ruby_types, :ruby_drops, :binary_data]` (plus any Shopify capabilities they lack).
440463

441464
## The Eval Tool
442465

@@ -490,7 +513,7 @@ For complex tests, use YAML input via stdin or file:
490513
cat <<EOF | liquid-spec eval adapter.rb --compare
491514
name: test_for_loop_with_break
492515
hint: "break should exit the loop immediately"
493-
complexity: 75
516+
complexity: 120
494517
template: |
495518
{% for i in (1..5) %}
496519
{% if i == 3 %}{% break %}{% endif %}

README.md

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
[![CI](https://github.com/Shopify/liquid-spec/actions/workflows/ruby.yml/badge.svg)](https://github.com/Shopify/liquid-spec/actions/workflows/ruby.yml)
44

5-
A conformance test suite for [Liquid](https://github.com/Shopify/liquid) template implementations. Run **4,600+ test cases** extracted from Shopify's reference implementation to verify your Liquid parser/renderer produces correct output.
5+
A conformance test suite for [Liquid](https://github.com/Shopify/liquid) template implementations. Run **7,000+ test cases** extracted from Shopify's reference implementation, curated basics, parser-error matrices, Dawn theme fixtures, and production recordings to verify your Liquid parser/renderer produces correct output.
66

77
## Why liquid-spec?
88

99
Building a Liquid implementation (compiler, interpreter, or transpiler)? liquid-spec helps you:
1010

11+
- **Build gradually** from an empty-template renderer into a production-ready Liquid implementation
1112
- **Verify correctness** against the reference Shopify/liquid behavior
1213
- **Catch regressions** when optimizing or refactoring
1314
- **Discover edge cases** you might not have considered
@@ -152,30 +153,36 @@ regular runs.
152153

153154
| Suite | Tests | Description |
154155
|-------|-------|-------------|
155-
| **basics** | 183 | Essential Liquid features - start here! Ordered by complexity with implementation hints |
156-
| **liquid_ruby** | ~1,700 | Core Liquid specs from [Shopify/liquid](https://github.com/Shopify/liquid) integration tests |
157-
| **shopify_production_recordings** | ~3,000 | Recorded behavior from Shopify's production Liquid compiler |
156+
| **basics** | 850 | Essential Liquid features - start here! Ordered by complexity with implementation hints |
157+
| **liquid_ruby** | 2,101 | Core Liquid specs from [Shopify/liquid](https://github.com/Shopify/liquid) integration tests |
158+
| **liquid_ruby_lax** | 121 | Lax-mode reference behavior |
159+
| **parser_errors** | 1,901 | Strict parser error compatibility and mutation matrices |
160+
| **partials** | 12 | Include/render focused compatibility specs |
161+
| **shopify_production_recordings** | 2,260 | Recorded behavior from Shopify's production Liquid compiler |
158162
| **shopify_theme_dawn** | 26 | Real-world templates from [Shopify Dawn](https://github.com/Shopify/dawn) theme |
159163

160164
### The Basics Suite
161165

162166
If you're building a new Liquid implementation, **start with the basics suite**. It runs first and covers all fundamental features from the [official Liquid documentation](https://shopify.github.io/liquid/).
163167

164-
Specs are ordered by complexity so you can implement features progressively:
168+
Specs are ordered by complexity so you can implement features progressively. The goal is a smooth ramp: a toy renderer should pass only the trivial first specs, then fail on a small, actionable next behavior.
165169

166170
| Complexity | Features |
167171
|------------|----------|
168-
| 10-20 | Raw text output, string/number/boolean literals |
169-
| 30-40 | Variables, basic filters (upcase, size, default) |
170-
| 50-60 | Assign tag, simple if/else conditionals |
171-
| 70-80 | For loops, filter chains, comparison operators |
172-
| 85-90 | Math filters, forloop object, capture tag |
173-
| 100-110 | Case/when, elsif, string manipulation filters |
174-
| 115-130 | Increment/decrement, comments, echo, liquid tag |
175-
| 140-150 | Array filters, property access (dot/bracket notation) |
176-
| 170-180 | Truthy/falsy edge cases, cycle, tablerow |
177-
178-
Each spec includes a detailed `hint` explaining how the feature should be implemented.
172+
| 0-1 | Empty template and literal passthrough |
173+
| 5-20 | First object output, literal strings/numbers/booleans/nil |
174+
| 30-50 | Variables, missing variables, very simple filters, assign |
175+
| 55-65 | Basic if/else/unless and simple boolean composition |
176+
| 70-100 | Gentle loops, comparisons, forloop basics, capture, simple case/when |
177+
| 105-150 | Common filters/tags, comments/raw, interrupts, loop modifiers, whitespace control |
178+
| 160-220 | Generated filter breadth, truthy/falsy edges, cycle/tablerow, first partials/filesystem |
179+
| 230-400 | Long-tail standard behavior and parser/scope/filesystem edge cases |
180+
| 500-900 | Mature compatibility: parser mutations, recursion/resource limits, date/time/Ruby quirks |
181+
| 1000 | Production recordings and unscored specs |
182+
183+
Each non-trivial spec includes a detailed `hint` explaining how the feature should be implemented. If the first failure is surprising or unactionable, the spec probably needs a better hint or a higher complexity score.
184+
185+
**Read `Max complexity reached`, not just total passes.** A naive adapter that always returns `""` can accidentally pass many later specs whose expected output is empty, but its max reached complexity should remain at 0. The max-complexity line tells you how far the implementation progressed through the ordered curriculum.
179186

180187
### Feature-Based Suite Selection
181188

@@ -215,6 +222,9 @@ Run Options:
215222
--list-suites List available test suites
216223
--max-failures N Stop after N failures (default: 10)
217224
--no-max-failures Run all specs without stopping
225+
--list-passed List specs that passed after the run (ramp/debug audits)
226+
--json Output a single JSON summary (for tools)
227+
--jsonl Output one JSON event per line (for benchmark streaming/tools)
218228
-h, --help Show help
219229

220230
Examples:
@@ -228,6 +238,24 @@ Examples:
228238
liquid-spec inspect my_adapter.rb -n "case" # Debug specific specs
229239
```
230240

241+
242+
### Auditing the Ramp with Dumb Adapters
243+
244+
When changing complexity scores or adding early specs, test the harness with intentionally bad adapters:
245+
246+
- an adapter that returns the template source unchanged
247+
- an adapter that always returns `""`
248+
- an adapter that raises during compile or render
249+
250+
Use `--list-passed` to see accidental passes and `--json` for machine-readable analysis:
251+
252+
```bash
253+
liquid-spec /tmp/echo_adapter.rb -s basics --max-failures 3 --list-passed
254+
liquid-spec /tmp/empty_adapter.rb -s basics --json --list-passed > empty-results.json
255+
```
256+
257+
A source-echo adapter should only pass raw-text specs before failing on first object output. An always-empty adapter may pass many empty-output specs, so judge progress by `max_complexity_reached`, not by total passes.
258+
231259
### Matrix Command
232260

233261
The `matrix` command runs specs across multiple adapters simultaneously and shows differences between implementations. This is useful for comparing behavior across different Liquid implementations or configurations.
@@ -442,15 +470,17 @@ Specs are automatically saved to `/tmp/liquid-spec-{date}.yml` for easy contribu
442470
```
443471
$ liquid-spec examples/liquid_ruby.rb
444472
445-
Features: core, lax_parsing
473+
Missing features: shopify_tags, shopify_objects, shopify_filters
446474
447-
Basics ................................. 183/183 passed
448-
Liquid Ruby ............................ 1683/1683 passed
449-
Liquid Ruby (Lax Mode) ................. 6/6 passed
450-
Shopify Production Recordings .......... 2338/2338 passed
451-
Shopify Theme Dawn ..................... skipped (needs shopify_tags, shopify_objects, shopify_filters)
475+
Basics ................................. 850/850 passed
476+
Liquid Ruby ............................ 2101/2101 passed
477+
Liquid Ruby (Lax Mode) ................. 121/121 passed
478+
Parser Errors .......................... 1901/1901 passed
479+
Partials ............................... 12/12 passed
480+
Shopify Production Recordings .......... 2260/2260 passed
481+
Shopify Theme Dawn ..................... skipped (adapter opts out of: shopify_filters)
452482
453-
Total: 4210 passed, 0 failed, 0 errors
483+
Total: 7245 passed, 0 failed, 0 errors
454484
```
455485

456486
## Example Adapters
@@ -484,7 +514,7 @@ Each spec defines:
484514
- **template** - Liquid source to compile and render
485515
- **environment** - Variables available during rendering
486516
- **expected** - Expected output string
487-
- **complexity** - Optional: ordering hint (lower = simpler, runs first)
517+
- **complexity** - Optional: ordering hint (lower = simpler, runs first; defaults to 1000 and must not exceed 1000)
488518
- **hint** - Optional: implementation guidance for this feature
489519
- **error_mode** - Optional: `:lax` or `:strict`
490520
- **filesystem** - Optional: mock files for include/render tags

0 commit comments

Comments
 (0)