Skip to content

Commit 1e75ccd

Browse files
committed
Congrats addendum with dynamic skipped features, strict2 clearability
Runner: - "Complexity level cleared: NN of MM" — level is the highest complexity present below the first failing level (real ramp level, not interpolated); MM is the max complexity in the run. - Congrats addendum at 100% (0 failures, level = max): 3 paths forward. The optional-feature list is dynamic — built from features that ACTUALLY had specs skipped this run (tracked in filter_by_missing), filtered through Features::FEATURE_DOCS to :optional, excluding :unnecessary Ruby-interop ones. Adapters that skipped no optional specs get the "push beyond the suite" suggestion instead. - Path shortening (<gem>/.../basename:line) and skipped count retained. strict2 clearability: - Gate the 7 "NEW STRICT2 CONTRACT" blank-body strict2 specs in blank_body_error_handling.yml behind a new strict2_blank_body_errors feature. liquid-ruby 5.13 renders "" for these (suppresses blank-body errors even in strict2), so they were unimplementable and blocked clearing for any adapter implementing strict2 + inline_errors. - Register strict2_blank_body_errors in Features::FEATURE_DOCS as :unnecessary (aspirational, not yet implemented by liquid-ruby). - Add to reference adapter (examples/liquid_ruby.rb) and JSON-RPC template missing_features so they opt out by default. Documented in CLAUDE.md.
1 parent dd7440a commit 1e75ccd

6 files changed

Lines changed: 45 additions & 25 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,7 @@ Feature selection is denylist-based. Leave `missing_features` empty to try every
536536
- `:lax_parsing` - Adapter does not support `error_mode: :lax`
537537
- `:ruby_types` / `:ruby_drops` / `:binary_data` - Adapter cannot consume Ruby-specific values from specs
538538
- `:template_factory` - Adapter cannot support template factory/artifact callbacks
539+
- `:strict2_blank_body_errors` - Aspirational "NEW STRICT2 CONTRACT" (strict2 surfaces inline errors even for blank block bodies); liquid-ruby 5.13 does not implement this, so opt out until your adapter deliberately adopts it
539540

540541
**Shopify-specific features:**
541542
- `:shopify_tags` - Shopify-specific tags (schema, style, section)

examples/liquid_ruby.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
end
2121

2222
LiquidSpec.configure do |config|
23-
config.missing_features = [:self_environment_shadowing, :drop_class_output, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access, :lax_parsing]
23+
config.missing_features = [:self_environment_shadowing, :drop_class_output, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access, :lax_parsing, :strict2_blank_body_errors]
2424
end
2525

2626
LiquidSpec.compile do |ctx, source, parse_options|

lib/liquid/spec/cli/features.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ module Features
9797
recommendation: :unnecessary,
9898
note: "Binary data can't be transmitted in JSON without base64 encoding.",
9999
},
100+
strict2_blank_body_errors: {
101+
description: "strict2 surfaces inline errors even when the block body is blank",
102+
recommendation: :unnecessary,
103+
note: "Aspirational 'NEW STRICT2 CONTRACT' not yet implemented by liquid-ruby 5.13 (it still suppresses blank-body errors). Opt out until your implementation deliberately adopts this contract.",
104+
},
100105
}.freeze
101106

102107
RECOMMENDATION_LABELS = {

lib/liquid/spec/cli/init.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ module Init
324324
:shopify_error_handling,
325325
:shopify_error_format,
326326
:shopify_string_access,
327+
:strict2_blank_body_errors,
327328
]
328329
end
329330
@@ -393,7 +394,7 @@ def self.agents_md_content(filename, json_rpc: false)
393394
4. Implement the smallest change that makes the spec pass for the RIGHT reason.
394395
5. Re-run. Every previously-passing spec must still pass — a fix that breaks
395396
earlier specs is wrong, not a tradeoff.
396-
6. Judge progress by **`Max complexity reached`**, not the raw pass count. A
397+
6. Judge progress by **`Complexity level cleared`**, not the raw pass count. A
397398
partial implementation accidentally passes many later specs whose expected
398399
output happens to be empty; complexity-reached is the honest meter.
399400

lib/liquid/spec/cli/runner.rb

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -302,18 +302,19 @@ def run_specs_frozen(config, options, run_id)
302302
has_prioritized = options[:add_specs] && !options[:add_specs].empty?
303303
prioritized_specs = has_prioritized ? load_additional_specs(options[:add_specs]) : []
304304
total_skipped = 0
305+
skipped_features = Set.new
305306
prioritized_specs = filter_specs(prioritized_specs, config.filter) if config.filter && prioritized_specs.any?
306307
if prioritized_specs.any?
307308
total_skipped += prioritized_specs.size
308-
prioritized_specs = filter_by_missing(prioritized_specs, missing_features)
309+
prioritized_specs = filter_by_missing(prioritized_specs, missing_features, skipped_features: skipped_features)
309310
total_skipped -= prioritized_specs.size
310311
end
311312

312313
# Auto-discover local specs from ./specs/*.yml
313314
local_specs = discover_local_specs
314315
local_specs = filter_specs(local_specs, config.filter) if config.filter && local_specs.any?
315316
total_skipped += local_specs.size
316-
local_specs = filter_by_missing(local_specs, missing_features)
317+
local_specs = filter_by_missing(local_specs, missing_features, skipped_features: skipped_features)
317318
total_skipped -= local_specs.size
318319
if local_specs.any? && config.verbose && !json_mode
319320
puts "Auto-including: #{local_specs.size} specs from ./specs/*.yml"
@@ -393,7 +394,7 @@ def run_specs_frozen(config, options, run_id)
393394

394395
suite_specs = filter_specs(suite_specs, config.filter) if config.filter
395396
total_skipped += suite_specs.size
396-
suite_specs = filter_by_missing(suite_specs, missing_features)
397+
suite_specs = filter_by_missing(suite_specs, missing_features, skipped_features: skipped_features)
397398
total_skipped -= suite_specs.size
398399
suite_specs = sort_by_complexity(suite_specs)
399400

@@ -525,7 +526,7 @@ def run_specs_frozen(config, options, run_id)
525526
passed_specs: all_passed,
526527
list_passed: options[:list_passed],
527528
max_failures: max_failures,
528-
missing_features: missing_features
529+
skipped_features: skipped_features
529530
)
530531
end
531532

@@ -1422,7 +1423,7 @@ def sort_result_entries(entries)
14221423
end
14231424
end
14241425

1425-
def print_run_summary(total_passed:, total_failed:, total_errors:, max_complexity_reached:, max_possible:, total_skipped:, failures:, known_fixed:, passed_specs:, list_passed:, max_failures:, missing_features:)
1426+
def print_run_summary(total_passed:, total_failed:, total_errors:, max_complexity_reached:, max_possible:, total_skipped:, failures:, known_fixed:, passed_specs:, list_passed:, max_failures:, skipped_features:)
14261427
failures_count = total_failed + total_errors
14271428
if failures.any?
14281429
puts "Next best specs to work on:"
@@ -1431,41 +1432,40 @@ def print_run_summary(total_passed:, total_failed:, total_errors:, max_complexit
14311432
end
14321433
print_known_fixed(known_fixed) if known_fixed.any?
14331434
print_passed_specs(passed_specs) if list_passed
1434-
parts = ["Complexity level cleared: #{max_complexity_reached}", "#{total_passed} passes", "#{failures_count} failures"]
1435+
parts = ["Complexity level cleared: #{max_complexity_reached} of #{max_possible}", "#{total_passed} passes", "#{failures_count} failures"]
14351436
parts << "#{total_skipped} skipped" if total_skipped > 0
14361437
puts parts.join(", ") + "."
14371438

1438-
# Congrats addendum: 100% of the run passes (no failures, bar at max).
1439-
print_congrats(total_skipped, missing_features) if failures_count == 0 && max_complexity_reached == max_possible
1439+
# Congrats addendum: 100% of the run passes (no failures, level = max).
1440+
print_congrats(skipped_features) if failures_count == 0 && max_complexity_reached == max_possible
14401441
end
14411442

14421443
# Printed when an adapter clears the full complexity bar (0 failures, bar = max).
14431444
# Suggests 3 paths forward: implement more features, benchmark, or give back.
1444-
def print_congrats(total_skipped, missing_features)
1445+
def print_congrats(skipped_features)
14451446
puts ""
14461447
puts "Congrats! You cleared the complexity bar — every spec you ran passes."
14471448
puts ""
14481449
puts "3 paths forward:"
14491450
puts ""
14501451
docs = Features::FEATURE_DOCS
1451-
optional = missing_features.select { |f| docs.dig(f.to_sym, :recommendation) == :optional }
1452-
skipped_other = missing_features.size - optional.size
1452+
optional = skipped_features.select { |f| docs.dig(f.to_sym, :recommendation) == :optional }
1453+
other = skipped_features.size - optional.size
14531454
if optional.any?
1454-
puts " 1. Implement an optional feature you currently opt out of. Each unblocks skipped specs:"
1455+
puts " 1. Implement an optional feature you skipped specs for this run. Each unblocks more:"
14551456
optional.each do |f|
1456-
d = docs[f.to_sym]
1457-
puts " - #{f}: #{d[:description]}"
1457+
puts " - #{f}: #{docs[f.to_sym][:description]}"
14581458
end
14591459
puts " Pros: more coverage, closer to production/theme-ready."
14601460
puts " Cons: each needs design + testing; Shopify features need platform context."
1461-
if skipped_other > 0
1462-
puts " (#{skipped_other} other opted-out feature#{skipped_other == 1 ? "" : "s"} are Ruby-interop or core and not worth implementing for a non-Ruby adapter.)"
1461+
if other > 0
1462+
puts " (#{other} other skipped feature#{other == 1 ? "" : "s"} are Ruby-interop or core not worth implementing for a non-Ruby adapter.)"
14631463
end
1464-
elsif missing_features.any?
1465-
puts " 1. Your opted-out features (#{missing_features.join(", ")}) are all Ruby-interop or core —"
1466-
puts " not worth implementing for a non-Ruby adapter. Try the shopify_theme_dawn suite"
1467-
puts " (-s shopify_theme_dawn) for real-world theme specs instead. Pros: production-shaped"
1468-
puts " coverage. Cons: requires Shopify-specific tags/objects/filters."
1464+
elsif skipped_features.any?
1465+
puts " 1. The features you skipped specs for (#{skipped_features.map(&:to_s).join(", ")}) are all"
1466+
puts " Ruby-interop or core — not worth implementing for a non-Ruby adapter. Try the"
1467+
puts " shopify_theme_dawn suite (-s shopify_theme_dawn) for real-world theme specs."
1468+
puts " Pros: production-shaped coverage. Cons: requires Shopify tags/objects/filters."
14691469
else
14701470
puts " 1. Push beyond the suite: try the shopify_theme_dawn suite (-s shopify_theme_dawn)"
14711471
puts " for real-world theme specs. Pros: production-shaped coverage."
@@ -2008,9 +2008,15 @@ def load_additional_specs(globs)
20082008
specs
20092009
end
20102010

2011-
def filter_by_missing(specs, missing_features)
2011+
def filter_by_missing(specs, missing_features, skipped_features: nil)
20122012
missing_set = Set.new(missing_features.map(&:to_sym))
2013-
specs.reject { |s| s.skipped_by?(missing_set) }
2013+
specs.reject do |s|
2014+
skipped = s.skipped_by?(missing_set)
2015+
if skipped && skipped_features
2016+
(s.features & missing_set.to_a).each { |f| skipped_features << f }
2017+
end
2018+
skipped
2019+
end
20142020
end
20152021

20162022
def parse_filter_pattern(pattern)

specs/liquid_ruby/blank_body_error_handling.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ specs:
200200
template: "{% if 5 > \"x\" %}{% endif %}"
201201
error_mode: strict2
202202
render_errors: true
203+
features: [strict2_blank_body_errors]
203204
expected: "Liquid error (line 1): comparison of Integer with String failed"
204205
complexity: 750
205206
hint: |
@@ -211,6 +212,7 @@ specs:
211212
template: "{% if 5 > \"x\" %} {% endif %}"
212213
error_mode: strict2
213214
render_errors: true
215+
features: [strict2_blank_body_errors]
214216
expected: "Liquid error (line 1): comparison of Integer with String failed"
215217
complexity: 750
216218
hint: |
@@ -221,6 +223,7 @@ specs:
221223
template: "{% if 5 > \"x\" %}{% assign a = 1 %}{% endif %}"
222224
error_mode: strict2
223225
render_errors: true
226+
features: [strict2_blank_body_errors]
224227
expected: "Liquid error (line 1): comparison of Integer with String failed"
225228
complexity: 750
226229
hint: |
@@ -232,6 +235,7 @@ specs:
232235
template: "{% if 5 > \"x\" %}{% comment %}c{% endcomment %}{% endif %}"
233236
error_mode: strict2
234237
render_errors: true
238+
features: [strict2_blank_body_errors]
235239
expected: "Liquid error (line 1): comparison of Integer with String failed"
236240
complexity: 750
237241
hint: |
@@ -242,6 +246,7 @@ specs:
242246
template: "{% if 5 > \"x\" %}{% capture c %}text{% endcapture %}{% endif %}"
243247
error_mode: strict2
244248
render_errors: true
249+
features: [strict2_blank_body_errors]
245250
expected: "Liquid error (line 1): comparison of Integer with String failed"
246251
complexity: 750
247252
hint: |
@@ -253,6 +258,7 @@ specs:
253258
template: "{% unless 5 > \"x\" %} {% endunless %}"
254259
error_mode: strict2
255260
render_errors: true
261+
features: [strict2_blank_body_errors]
256262
expected: "Liquid error (line 1): comparison of Integer with String failed"
257263
complexity: 750
258264
hint: |
@@ -265,6 +271,7 @@ specs:
265271
xs: "bad"
266272
error_mode: strict2
267273
render_errors: true
274+
features: [strict2_blank_body_errors]
268275
expected: "Liquid error (line 1): invalid integer"
269276
complexity: 750
270277
hint: |

0 commit comments

Comments
 (0)