Skip to content

Commit ea78f19

Browse files
committed
Add truthiness curriculum spec, clarify standalone-library in init AGENTS.md
truthiness.yml: 5-spec curriculum (c=5,20,40,60,80) covering all Liquid truthiness cases in dependency order. File-level hint states the core rule (only false and nil are falsy). Level 5 documents the default-filter quirk (default uses blank semantics, not if-truthiness — "", [], {} get the fallback but 0 passes through). Verified against liquid-ruby 5.13. init.rb: generated AGENTS.md now clarifies that the implementer is building a standalone Liquid library in their language, and the adapter (Ruby or JSON-RPC) is purely a test harness with no intrinsic value to the library. Intro note applies to all modes; JSON-RPC section reiterates for non-Ruby.
1 parent 435419d commit ea78f19

2 files changed

Lines changed: 126 additions & 0 deletions

File tree

lib/liquid/spec/cli/init.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,14 @@ def self.agents_md_content(filename, json_rpc: false)
361361
to climb that ramp. The suite is the definition of done — when it is green, you
362362
have a real Liquid implementation.
363363
364+
**Your Liquid library is standalone.** The adapter file(s) in this directory are
365+
test harnesses that run liquid-spec against your library — they are NOT part of your
366+
library and have no intrinsic value to it. Your library's public API, architecture,
367+
and distribution are entirely your own. The Ruby adapter calls your code directly;
368+
the JSON-RPC adapter talks to your engine over stdin/stdout. Both are disposable
369+
shims that exist only so liquid-spec can exercise your implementation. Build your
370+
library as a library; wire up the adapter afterward.
371+
364372
#{json_rpc ? non_ruby_notice : ""}
365373
## Which adapter file?
366374
@@ -603,6 +611,15 @@ def self.non_ruby_notice
603611
<<~MARKDOWN
604612
## Not Using Ruby?
605613
614+
**You are building a standalone Liquid library in your language.**
615+
The JSON-RPC adapter generated here is purely a harness for running the
616+
liquid-spec test suite against your library — it is NOT part of your library
617+
and has no intrinsic value to it. Your library's public API, architecture,
618+
and distribution are entirely your own; the adapter is a thin test shim that
619+
translates liquid-spec's Ruby protocol calls into JSON-RPC requests your
620+
server can answer. Once your library passes the specs, the adapter is
621+
disposable.
622+
606623
**This adapter uses JSON-RPC** to communicate with your Liquid implementation over stdin/stdout.
607624
You can implement your Liquid engine in **any language** (Rust, Go, Python, Node.js, etc.).
608625

specs/basics/truthiness.yml

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
---
2+
_metadata:
3+
hint: |
4+
Liquid truthiness: ONLY `false` and `nil` are falsy. Everything else is
5+
truthy — including 0, "", '0', [], and {}. This matches Ruby semantics, not
6+
JavaScript/Python. Getting this wrong breaks hundreds of specs. NOTE: the
7+
`default` filter is the one exception — it uses "blank" semantics (nil,
8+
false, "", [], {} all get the fallback), NOT if-truthiness.
9+
specs:
10+
- name: truthiness_only_false_and_nil_are_falsy
11+
template: |-
12+
false:{% if false %}T{% else %}F{% endif %}
13+
nil:{% if nil %}T{% else %}F{% endif %}
14+
expected: |-
15+
false:F
16+
nil:F
17+
complexity: 5
18+
hint: |
19+
The foundation of Liquid truthiness: only `false` and `nil` are falsy.
20+
Every other value — numbers, strings, arrays, hashes — is truthy. This
21+
matches Ruby (where any object that exists is truthy), not JavaScript
22+
(where 0 and "" are falsy) or Python (where 0, "", [] are falsy).
23+
24+
- name: truthiness_zero_and_empty_string_are_truthy
25+
template: |-
26+
0:{% if 0 %}T{% else %}F{% endif %}
27+
empty:{% if '' %}T{% else %}F{% endif %}
28+
str0:{% if '0' %}T{% else %}F{% endif %}
29+
expected: |-
30+
0:T
31+
empty:T
32+
str0:T
33+
complexity: 20
34+
hint: |
35+
The three biggest surprises for implementers from other languages:
36+
- 0 is truthy (not falsy like JS/Python).
37+
- "" (empty string) is truthy (not falsy like JS/Python).
38+
- '0' (string zero) is truthy — Liquid does NOT coerce strings to numbers
39+
for truthiness. Use {% if num != 0 %} or {{ str | size }} > 0 to test
40+
for non-zero / non-empty.
41+
42+
- name: truthiness_empty_collections_are_truthy
43+
template: |-
44+
arr:{% if items %}T{% else %}F{% endif %}
45+
hash:{% if h %}T{% else %}F{% endif %}
46+
expected: |-
47+
arr:T
48+
hash:T
49+
environment:
50+
items: []
51+
h: {}
52+
complexity: 40
53+
hint: |
54+
Empty arrays [] and empty hashes {} are truthy in {% if %}. This is
55+
unlike most languages. To check for non-empty, use {% if items != empty %}
56+
or {{ items | size }} > 0. The `empty` keyword compares against the empty
57+
state of any collection.
58+
59+
- name: truthiness_in_contexts
60+
template: |-
61+
unless_false:{% unless false %}T{% endunless %}
62+
unless_nil:{% unless nil %}T{% endunless %}
63+
undefined:{% if missing %}T{% else %}F{% endif %}
64+
nil_prop:{% if obj.nope %}T{% else %}F{% endif %}
65+
or:{% if nil or 0 %}T{% else %}F{% endif %}
66+
and:{% if nil and true %}T{% else %}F{% endif %}
67+
expected: |-
68+
unless_false:T
69+
unless_nil:T
70+
undefined:F
71+
nil_prop:F
72+
or:T
73+
and:F
74+
environment:
75+
obj:
76+
x: 1
77+
complexity: 60
78+
hint: |
79+
Truthiness applies the same in all conditional contexts:
80+
- {% unless X %} renders when X is falsy (inverse of if).
81+
- Undefined variables resolve to nil → falsy. This is the idiomatic
82+
"variable exists?" check: {% if maybe %}.
83+
- Accessing a missing property (obj.nope) returns nil → falsy.
84+
- `or` returns the first truthy operand (nil or 0 → 0 is truthy → T).
85+
- `and` returns false if either operand is falsy (nil and true → F).
86+
87+
- name: truthiness_default_filter_uses_blank_not_truthy
88+
template: |-
89+
nil:{{ x | default: 'fb' }}
90+
false:{{ f | default: 'fb' }}
91+
empty:{{ s | default: 'fb' }}
92+
zero:{{ z | default: 'fb' }}
93+
expected: |-
94+
nil:fb
95+
false:fb
96+
empty:fb
97+
zero:0
98+
environment:
99+
s: ""
100+
z: 0
101+
complexity: 80
102+
hint: |
103+
QUIRK: the `default` filter does NOT use if-truthiness. It uses "blank"
104+
semantics — nil, false, "" (empty string), [] (empty array), and {} (empty
105+
hash) ALL get the fallback. But 0 is NOT blank, so it passes through.
106+
This is the one place truthiness differs from {% if %}: 0 is truthy in
107+
if-conditions AND passes through `default`, but "" is truthy in if-conditions
108+
yet gets replaced by `default`. Implement `default` by checking blankness
109+
(nil || false || empty), not boolean truthiness.

0 commit comments

Comments
 (0)