Skip to content

Commit 37fadf7

Browse files
dmealingclaude
andcommitted
docs(prompts): best-practice — conditional content is data + flags, never branched prose
Adds a section to the metaobjects-prompts skill: when prompt wording varies along a dimension (audience, tier, mode, locale, variant), the variation belongs in the payload (vocabulary as typed data) and the template (presence as section flags / provider-selected variant text) — never branched and concatenated in a service. Reinforces the pillar's existing "text external, pre-format on the payload, deterministic render" stance with an explicit anti-pattern + WRONG/RIGHT example. Conformance fixtures synced (assemble == expected, 4/4). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a29e5f6 commit 37fadf7

5 files changed

Lines changed: 205 additions & 0 deletions

File tree

  • agent-context/skills/metaobjects-prompts
  • fixtures/agent-context-conformance
    • java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-prompts
    • java-react/expected/.claude/skills/metaobjects-prompts
    • python/expected/.claude/skills/metaobjects-prompts
    • ts-react-tanstack/expected/.claude/skills/metaobjects-prompts

agent-context/skills/metaobjects-prompts/SKILL.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,47 @@ For the `xml`-format example above with payload `{ displayName: "Ada", postCount
123123
bytes. You render the prompt, call your LLM client (provider-agnostic — codegen
124124
emits no provider-side schema), then parse the response.
125125

126+
## Conditional content: data and flags, never branched prose
127+
128+
When a prompt's wording varies along some dimension — audience, tier, mode,
129+
locale, entitlement, a domain variant — do NOT branch the prose in code and
130+
concatenate strings. Branching prompt text in a service is the anti-pattern this
131+
pillar exists to remove: it scatters the same distinction across call sites, each
132+
re-encoded and free to drift, and none of it snapshot-tested. The variation
133+
belongs in exactly two places, with a third for the rare genuine divergence:
134+
135+
- **Vocabulary as payload data.** The words and values that differ become typed
136+
payload fields, pre-computed once from the varying dimension — a noun, a label,
137+
a set of verbs (a list), an example. The template stays single and references
138+
`{{term}}` / `{{#items}}…{{/items}}`. The prose *structure* is identical across
139+
variants; only the data differs, so there is nothing to branch.
140+
- **Presence as boolean flags.** When a whole block exists-or-not for a variant,
141+
gate it with a section flag the payload sets: `{{#showBlock}}…{{/showBlock}}`.
142+
Reserve flags for entire blocks — never mid-sentence word swaps, which are
143+
vocabulary.
144+
- **Variant text only when prose truly diverges.** If a section's wording — not
145+
just its vocabulary — genuinely differs, select a per-variant text through the
146+
provider seam (a `@textRef` variant, or an included partial) so the shared
147+
prose still lives in one place. Expect to need this rarely.
148+
149+
A single resolver maps the varying dimension to that payload (the flags + the
150+
vocabulary), so the distinction is defined ONCE and every template that depends
151+
on it stays consistent.
152+
153+
```
154+
// WRONG — prose branched and concatenated in a service:
155+
if (tier.isPremium()) sb.append("Your plan includes priority support.");
156+
else sb.append("Upgrade any time for priority support.");
157+
```
158+
```mustache
159+
{{! RIGHT — text in the template; the variant is data + a flag }}
160+
{{supportLine}}
161+
{{#isPremium}}(Priority queue enabled.){{/isPremium}}
162+
```
163+
164+
This stays deterministic and golden-testable per variant: render the template
165+
against each value of the dimension and snapshot every variant.
166+
126167
## `verify` fails the build on prompt-drift
127168

128169
For every template, the verify step resolves the text, parses each `{{...}}`

fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-prompts/SKILL.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,47 @@ For the `xml`-format example above with payload `{ displayName: "Ada", postCount
123123
bytes. You render the prompt, call your LLM client (provider-agnostic — codegen
124124
emits no provider-side schema), then parse the response.
125125

126+
## Conditional content: data and flags, never branched prose
127+
128+
When a prompt's wording varies along some dimension — audience, tier, mode,
129+
locale, entitlement, a domain variant — do NOT branch the prose in code and
130+
concatenate strings. Branching prompt text in a service is the anti-pattern this
131+
pillar exists to remove: it scatters the same distinction across call sites, each
132+
re-encoded and free to drift, and none of it snapshot-tested. The variation
133+
belongs in exactly two places, with a third for the rare genuine divergence:
134+
135+
- **Vocabulary as payload data.** The words and values that differ become typed
136+
payload fields, pre-computed once from the varying dimension — a noun, a label,
137+
a set of verbs (a list), an example. The template stays single and references
138+
`{{term}}` / `{{#items}}…{{/items}}`. The prose *structure* is identical across
139+
variants; only the data differs, so there is nothing to branch.
140+
- **Presence as boolean flags.** When a whole block exists-or-not for a variant,
141+
gate it with a section flag the payload sets: `{{#showBlock}}…{{/showBlock}}`.
142+
Reserve flags for entire blocks — never mid-sentence word swaps, which are
143+
vocabulary.
144+
- **Variant text only when prose truly diverges.** If a section's wording — not
145+
just its vocabulary — genuinely differs, select a per-variant text through the
146+
provider seam (a `@textRef` variant, or an included partial) so the shared
147+
prose still lives in one place. Expect to need this rarely.
148+
149+
A single resolver maps the varying dimension to that payload (the flags + the
150+
vocabulary), so the distinction is defined ONCE and every template that depends
151+
on it stays consistent.
152+
153+
```
154+
// WRONG — prose branched and concatenated in a service:
155+
if (tier.isPremium()) sb.append("Your plan includes priority support.");
156+
else sb.append("Upgrade any time for priority support.");
157+
```
158+
```mustache
159+
{{! RIGHT — text in the template; the variant is data + a flag }}
160+
{{supportLine}}
161+
{{#isPremium}}(Priority queue enabled.){{/isPremium}}
162+
```
163+
164+
This stays deterministic and golden-testable per variant: render the template
165+
against each value of the dimension and snapshot every variant.
166+
126167
## `verify` fails the build on prompt-drift
127168

128169
For every template, the verify step resolves the text, parses each `{{...}}`

fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-prompts/SKILL.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,47 @@ For the `xml`-format example above with payload `{ displayName: "Ada", postCount
123123
bytes. You render the prompt, call your LLM client (provider-agnostic — codegen
124124
emits no provider-side schema), then parse the response.
125125

126+
## Conditional content: data and flags, never branched prose
127+
128+
When a prompt's wording varies along some dimension — audience, tier, mode,
129+
locale, entitlement, a domain variant — do NOT branch the prose in code and
130+
concatenate strings. Branching prompt text in a service is the anti-pattern this
131+
pillar exists to remove: it scatters the same distinction across call sites, each
132+
re-encoded and free to drift, and none of it snapshot-tested. The variation
133+
belongs in exactly two places, with a third for the rare genuine divergence:
134+
135+
- **Vocabulary as payload data.** The words and values that differ become typed
136+
payload fields, pre-computed once from the varying dimension — a noun, a label,
137+
a set of verbs (a list), an example. The template stays single and references
138+
`{{term}}` / `{{#items}}…{{/items}}`. The prose *structure* is identical across
139+
variants; only the data differs, so there is nothing to branch.
140+
- **Presence as boolean flags.** When a whole block exists-or-not for a variant,
141+
gate it with a section flag the payload sets: `{{#showBlock}}…{{/showBlock}}`.
142+
Reserve flags for entire blocks — never mid-sentence word swaps, which are
143+
vocabulary.
144+
- **Variant text only when prose truly diverges.** If a section's wording — not
145+
just its vocabulary — genuinely differs, select a per-variant text through the
146+
provider seam (a `@textRef` variant, or an included partial) so the shared
147+
prose still lives in one place. Expect to need this rarely.
148+
149+
A single resolver maps the varying dimension to that payload (the flags + the
150+
vocabulary), so the distinction is defined ONCE and every template that depends
151+
on it stays consistent.
152+
153+
```
154+
// WRONG — prose branched and concatenated in a service:
155+
if (tier.isPremium()) sb.append("Your plan includes priority support.");
156+
else sb.append("Upgrade any time for priority support.");
157+
```
158+
```mustache
159+
{{! RIGHT — text in the template; the variant is data + a flag }}
160+
{{supportLine}}
161+
{{#isPremium}}(Priority queue enabled.){{/isPremium}}
162+
```
163+
164+
This stays deterministic and golden-testable per variant: render the template
165+
against each value of the dimension and snapshot every variant.
166+
126167
## `verify` fails the build on prompt-drift
127168

128169
For every template, the verify step resolves the text, parses each `{{...}}`

fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-prompts/SKILL.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,47 @@ For the `xml`-format example above with payload `{ displayName: "Ada", postCount
123123
bytes. You render the prompt, call your LLM client (provider-agnostic — codegen
124124
emits no provider-side schema), then parse the response.
125125

126+
## Conditional content: data and flags, never branched prose
127+
128+
When a prompt's wording varies along some dimension — audience, tier, mode,
129+
locale, entitlement, a domain variant — do NOT branch the prose in code and
130+
concatenate strings. Branching prompt text in a service is the anti-pattern this
131+
pillar exists to remove: it scatters the same distinction across call sites, each
132+
re-encoded and free to drift, and none of it snapshot-tested. The variation
133+
belongs in exactly two places, with a third for the rare genuine divergence:
134+
135+
- **Vocabulary as payload data.** The words and values that differ become typed
136+
payload fields, pre-computed once from the varying dimension — a noun, a label,
137+
a set of verbs (a list), an example. The template stays single and references
138+
`{{term}}` / `{{#items}}…{{/items}}`. The prose *structure* is identical across
139+
variants; only the data differs, so there is nothing to branch.
140+
- **Presence as boolean flags.** When a whole block exists-or-not for a variant,
141+
gate it with a section flag the payload sets: `{{#showBlock}}…{{/showBlock}}`.
142+
Reserve flags for entire blocks — never mid-sentence word swaps, which are
143+
vocabulary.
144+
- **Variant text only when prose truly diverges.** If a section's wording — not
145+
just its vocabulary — genuinely differs, select a per-variant text through the
146+
provider seam (a `@textRef` variant, or an included partial) so the shared
147+
prose still lives in one place. Expect to need this rarely.
148+
149+
A single resolver maps the varying dimension to that payload (the flags + the
150+
vocabulary), so the distinction is defined ONCE and every template that depends
151+
on it stays consistent.
152+
153+
```
154+
// WRONG — prose branched and concatenated in a service:
155+
if (tier.isPremium()) sb.append("Your plan includes priority support.");
156+
else sb.append("Upgrade any time for priority support.");
157+
```
158+
```mustache
159+
{{! RIGHT — text in the template; the variant is data + a flag }}
160+
{{supportLine}}
161+
{{#isPremium}}(Priority queue enabled.){{/isPremium}}
162+
```
163+
164+
This stays deterministic and golden-testable per variant: render the template
165+
against each value of the dimension and snapshot every variant.
166+
126167
## `verify` fails the build on prompt-drift
127168

128169
For every template, the verify step resolves the text, parses each `{{...}}`

fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-prompts/SKILL.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,47 @@ For the `xml`-format example above with payload `{ displayName: "Ada", postCount
123123
bytes. You render the prompt, call your LLM client (provider-agnostic — codegen
124124
emits no provider-side schema), then parse the response.
125125

126+
## Conditional content: data and flags, never branched prose
127+
128+
When a prompt's wording varies along some dimension — audience, tier, mode,
129+
locale, entitlement, a domain variant — do NOT branch the prose in code and
130+
concatenate strings. Branching prompt text in a service is the anti-pattern this
131+
pillar exists to remove: it scatters the same distinction across call sites, each
132+
re-encoded and free to drift, and none of it snapshot-tested. The variation
133+
belongs in exactly two places, with a third for the rare genuine divergence:
134+
135+
- **Vocabulary as payload data.** The words and values that differ become typed
136+
payload fields, pre-computed once from the varying dimension — a noun, a label,
137+
a set of verbs (a list), an example. The template stays single and references
138+
`{{term}}` / `{{#items}}…{{/items}}`. The prose *structure* is identical across
139+
variants; only the data differs, so there is nothing to branch.
140+
- **Presence as boolean flags.** When a whole block exists-or-not for a variant,
141+
gate it with a section flag the payload sets: `{{#showBlock}}…{{/showBlock}}`.
142+
Reserve flags for entire blocks — never mid-sentence word swaps, which are
143+
vocabulary.
144+
- **Variant text only when prose truly diverges.** If a section's wording — not
145+
just its vocabulary — genuinely differs, select a per-variant text through the
146+
provider seam (a `@textRef` variant, or an included partial) so the shared
147+
prose still lives in one place. Expect to need this rarely.
148+
149+
A single resolver maps the varying dimension to that payload (the flags + the
150+
vocabulary), so the distinction is defined ONCE and every template that depends
151+
on it stays consistent.
152+
153+
```
154+
// WRONG — prose branched and concatenated in a service:
155+
if (tier.isPremium()) sb.append("Your plan includes priority support.");
156+
else sb.append("Upgrade any time for priority support.");
157+
```
158+
```mustache
159+
{{! RIGHT — text in the template; the variant is data + a flag }}
160+
{{supportLine}}
161+
{{#isPremium}}(Priority queue enabled.){{/isPremium}}
162+
```
163+
164+
This stays deterministic and golden-testable per variant: render the template
165+
against each value of the dimension and snapshot every variant.
166+
126167
## `verify` fails the build on prompt-drift
127168

128169
For every template, the verify step resolves the text, parses each `{{...}}`

0 commit comments

Comments
 (0)