Skip to content

Commit 94e4df4

Browse files
committed
docs(learnings): YAML authoring conventions (scalar coercion, frontmatter footguns)
Adds docs/learnings/yaml-conventions.md covering pitfalls that bite when authoring or auditing .yml / .md frontmatter resources: - YAML 1.1 boolean coercion (off/yes/no → boolean) and which parsers default to which spec version - whitespace-truthy gotchas - discriminated-union string sentinels (e.g. `field: 'off' | {provider: ...}`) where unquoted `off` silently becomes boolean false - deprecated-field footguns - multi-line block scalars (literal vs folded, chomping indicators) - anchors / aliases - frontmatter fence rules Indexed under both 'Topics' and 'Resource type runbooks' in docs/learnings/README.md.
1 parent b760259 commit 94e4df4

2 files changed

Lines changed: 192 additions & 0 deletions

File tree

docs/learnings/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Each file targets a specific topic so you can load only the context you need.
2626
| Bulk-dialing from a CSV (Outbound Call Campaigns) | [outbound-campaigns.md](outbound-campaigns.md) |
2727
| Voicemail detection / VM vs human classification | [voicemail-detection.md](voicemail-detection.md) |
2828
| Enforcing call time limits / graceful call ending | [call-duration.md](call-duration.md) |
29+
| Authoring YAML resource files (scalar coercion, frontmatter conventions) | [yaml-conventions.md](yaml-conventions.md) |
2930

3031
---
3132

@@ -43,6 +44,7 @@ Gotchas and silent defaults for each resource type:
4344
| [structured-outputs.md](structured-outputs.md) | Schema type gotchas, assistant_ids, default models, target modes, KPI patterns |
4445
| [simulations.md](simulations.md) | Personalities, evaluation comparators, chat-mode gotcha, missing references, full `/eval/simulation/*` API reference |
4546
| [webhooks.md](webhooks.md) | Default server messages, timeouts, unreachable servers, credential resolution, payload shape |
47+
| [yaml-conventions.md](yaml-conventions.md) | YAML 1.1 boolean coercion (`off`/`yes`/`no`), whitespace-truthy gotchas, discriminated-union sentinels, deprecated-field footguns, multi-line block scalars, anchors/aliases, frontmatter fence rules |
4648

4749
### Troubleshooting Runbooks
4850

docs/learnings/yaml-conventions.md

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# YAML Authoring Conventions
2+
3+
Common YAML pitfalls in gitops resource files — shapes that look right but break at runtime, plus parser-specific scalar coercion gotchas. Read before authoring or auditing any `.yml` / `.md` frontmatter resource.
4+
5+
---
6+
7+
## YAML 1.1 Boolean Coercion
8+
9+
YAML 1.1 (and many widely-used parsers) interprets these unquoted scalars as **booleans**, not the string values they appear to be:
10+
11+
| Unquoted | YAML 1.1 type | YAML 1.2 type |
12+
|---|---|---|
13+
| `yes` / `no` | boolean true / false | string |
14+
| `on` / `off` | boolean true / false | string |
15+
| `true` / `false` | boolean | boolean |
16+
| `null` / `~` | null | null |
17+
18+
This matters for any field whose runtime contract expects a specific string sentinel. A common pattern in resource APIs is `field: 'off' | { provider: ..., ... }` — the string `'off'` disables a feature, and a populated object configures it. If a YAML 1.1 parser silently coerces unquoted `off` to boolean `false`, the request body sends a boolean where the schema expects a string, and class-validator (or the equivalent) rejects it.
19+
20+
```yaml
21+
field: off # ⚠️ becomes boolean `false` in YAML 1.1 parsers
22+
field: 'off' # ✅ stays the string "off"
23+
field: "off" # ✅ stays the string "off"
24+
```
25+
26+
### Parsers that default to YAML 1.1
27+
- Symfony YAML
28+
- ruamel.yaml (non-strict mode)
29+
- PyYAML (default — has been YAML 1.1 since the project started)
30+
- js-yaml v3 (legacy)
31+
32+
### Parsers that default to YAML 1.2
33+
- js-yaml v4+
34+
- libyaml-based parsers (most modern Go and Rust YAML libraries)
35+
36+
The gitops engine here uses js-yaml v4, which is YAML 1.2 — `off` parses as a string. But if your CI/CD pipeline pre-processes resource files (templating, validation, splicing) through a different tool, the round-trip can corrupt scalars before the engine ever sees them.
37+
38+
### Defense
39+
40+
**Default rule:** quote any string scalar that resembles a boolean / null sentinel.
41+
42+
```yaml
43+
status: 'off' # not: status: off
44+
flag: 'no' # not: flag: no
45+
state: 'null' # only quote if 'null' is a literal string value, not actual null
46+
```
47+
48+
Better still, **omit the field entirely** when the desired state is "default / disabled / unset" — that is unambiguous regardless of parser:
49+
50+
```yaml
51+
# Cleanest — relies on schema default
52+
assistant:
53+
name: "..."
54+
model: { ... }
55+
# (no `field` key — disabled by default)
56+
57+
# Acceptable — explicit-off for audit visibility in diffs
58+
assistant:
59+
field: 'off' # always quote
60+
61+
# Avoid
62+
assistant:
63+
field: off # ⚠️ parser-dependent
64+
```
65+
66+
---
67+
68+
## Whitespace-Only Strings Are Truthy
69+
70+
Many runtime code paths use truthiness checks (`if (config.field)`) to gate behavior. Empty strings are falsy, but **whitespace-only strings are truthy**:
71+
72+
| Value | JS truthiness |
73+
|---|---|
74+
| `""` | falsy |
75+
| `" "` (single space) | **truthy** |
76+
| `"\n"` | **truthy** |
77+
| `null` / `undefined` | falsy |
78+
79+
If you intend "do nothing / silent / disabled," use the empty string `""` or omit the field. A YAML field accidentally written as `field: ` with a trailing space then newline can produce `field: " "` after some parsers — which then triggers behavior the author thought was disabled.
80+
81+
Audit pattern: when grepping for "disabled" config in resource files, search for both empty-string AND whitespace-only forms (`'^field:\s+$'` matches the dangerous case).
82+
83+
---
84+
85+
## Discriminated Unions: Sentinels Live at the Parent Level
86+
87+
Some fields accept either a sentinel string OR a structured object — e.g. `field: 'off' | { provider: '...', ... }`. The validator's discriminated union does NOT recognize a `provider: 'off'` object as the disable case. Use the top-level scalar.
88+
89+
```yaml
90+
# ✅ Valid — top-level scalar sentinel
91+
field: 'off'
92+
93+
# ❌ Invalid — provider doesn't have an 'off' variant in the union
94+
field:
95+
provider: 'off'
96+
```
97+
98+
Generalized rule: when a field type is `Sentinel | Object`, the sentinel always lives at the parent level, never as an inner field of the object form. Treating "off" as just another provider is intuitive but wrong — disable is a top-level concept, not a per-provider setting.
99+
100+
---
101+
102+
## Deprecated-Field Footguns
103+
104+
When a schema migrates from `featureEnabled?: boolean` (deprecated) to `feature: 'off' | Object`, both fields may still be visible in dashboard / API responses simultaneously. The runtime usually short-circuits on the new field — so:
105+
106+
```yaml
107+
feature: 'off'
108+
featureEnabled: true # ⚠️ ignored at runtime, but visible in dashboard UI
109+
```
110+
111+
Customer confusion is common because both fields appear in dashboard UI but only one drives runtime behavior. When authoring resource files, drop the deprecated field entirely if the schema offers a direct replacement.
112+
113+
If you find yourself auditing a resource where the new field and the deprecated field disagree, the new field always wins — but fix the file to use only the new field so future readers don't have to know that.
114+
115+
---
116+
117+
## Multi-Line Strings: `|` vs `|+` vs `|-`
118+
119+
Three block-scalar indicators with different newline-trimming behavior:
120+
121+
| Indicator | Trailing newlines |
122+
|---|---|
123+
| `\|` (clip, default) | Single `\n` preserved |
124+
| `\|-` (strip) | All trailing newlines removed |
125+
| `\|+` (keep) | All trailing newlines preserved |
126+
127+
For prompt-body content that is consumed as a string (system prompts, tool descriptions), prefer `|-` if the consumer trims whitespace anyway, and `|+` if you want intentional padding (e.g., a trailing blank line before user content is appended).
128+
129+
```yaml
130+
# Trailing-newline-sensitive — use the explicit form
131+
description: |-
132+
Hang up the call when the driver explicitly declines.
133+
134+
# Padding-intentional — use keep
135+
template: |+
136+
Here is the transcript:
137+
138+
{{transcript}}
139+
140+
```
141+
142+
Mixing `|` and `|-` inconsistently across resources makes the rendered output hard to diff visually. Pick one convention per file or per field family.
143+
144+
---
145+
146+
## Anchors and Aliases: Engine Support Varies
147+
148+
YAML supports `&anchor` and `*alias` for DRY-ing repeated structures. **Many gitops engines do NOT round-trip anchors faithfully** — they parse them, but on save they expand the alias inline. This means:
149+
150+
- A resource file you wrote with anchors will get rewritten with the anchors expanded after the next push.
151+
- The diff after `npm run pull` will look enormous because every alias becomes its full expansion.
152+
- Reviewers reading the rewritten file lose the "this section is intentionally a copy of X" signal.
153+
154+
**Recommendation:** avoid anchors/aliases in gitops resource YAML even if your local parser supports them. Duplicate the content explicitly. The maintenance cost of duplication is real but visible; the maintenance cost of silently-expanding anchors is invisible until someone tries to update both copies and only finds one.
155+
156+
---
157+
158+
## Quoting in Frontmatter Markdown Files
159+
160+
Assistant resource files use YAML frontmatter (`---` fenced) followed by a Markdown prompt body. The frontmatter follows all the rules above, but with one extra gotcha: **the `---` close-fence must be on its own line, immediately followed by a newline**. A common mistake:
161+
162+
```markdown
163+
---
164+
name: My Assistant
165+
model:
166+
provider: openai
167+
---# Role ← BROKEN: fence not isolated
168+
```
169+
170+
Most parsers will treat this as a malformed frontmatter and fail to extract the YAML. Always:
171+
172+
```markdown
173+
---
174+
name: My Assistant
175+
model:
176+
provider: openai
177+
---
178+
179+
# Role
180+
```
181+
182+
The blank line after `---` is conventional; the strict requirement is just that `---` ends the line cleanly. But humans grep for `\n---\n\n# ` patterns when looking for prompt boundaries, so the blank line aids readability.
183+
184+
---
185+
186+
## Cross-references
187+
188+
- `docs/learnings/assistants.md` — assistant-specific frontmatter authoring
189+
- `docs/learnings/tools.md` — tool YAML conventions (function descriptions, message blocks)
190+
- `docs/learnings/squads.md` — squad YAML conventions (member overrides, handoff destinations)

0 commit comments

Comments
 (0)