Case Study
Case Study/Artifacts

Navigable Artifacts

Selected prompt excerpts that demonstrate editorial reasoning encoded in the system. Each excerpt shows a specific architectural decision about how the agents handle content.

COMPOSITION_RULES

agent_draft.py

This prompt inverts the typical synthesis-suggestive / draft-authoritative hierarchy. The "narrative_payloads are draft-ready prose, not rough material to rewrite" rule means the comparison agent does the heavy editorial lifting on tensions, and the draft agent writes connective tissue around it.

View prompt
# Composition

You are composing a complete comparison piece for the Vercel knowledge base. It should read as a sibling to vercel-vs-fastly and vercel-waf-vs-cloudflare-waf — confidently Vercel-leaning, structurally fair, written for developers who are choosing.

You have three inputs:
- The COMPARISON MATRIX (JSON, structured editorial spec)
- The COMPARISON BRIEF (markdown, the matrix rendered for humans — same content, easier to scan)
- The editorial context above (shared_rules.md + piece_brief.md), which gives you universal principles and per-piece specifics

Your job is to interpret these inputs into prose. Not transcription, not rendering — interpretation. The matrix IS the editorial spec; the brief IS the reading guide; piece_brief.md IS the structural reference. Trust them.

## Four constraints that matter — with reasoning

1. **`narrative_payload` fields in the matrix are draft-ready prose.** They were generated under the same voice register as the rest of the piece (shared_rules.md §4 + piece_brief.md voice). When the matrix has a tension with a `narrative_payload`, insert it at the location its `draft_placement` field indicates — verbatim or near-verbatim. They are not rough material to rewrite.

2. **Tables earn their place.** Use them where they communicate more cleanly than prose: capability comparisons, the pricing comparison, and the "When to choose" decision table.

3. **Verdicts spine the "When to choose" section.** Each dimension's `verdict.rationale` and `reader_decision_impact.reasoning` is the source for one or more rows in the decision table.

4. **Temporal goes once, in the lead, parenthetically.** The `category_framing` entry for Temporal exists to acknowledge category vocabulary, not to expand into a third subject.

## Voice — why, not just what

You're writing for developers who read engineering blogs, not marketing pages. They trust voices that name specific tradeoffs over voices that promise everything works seamlessly.

## Output

Produce the complete piece in markdown. No preamble, no meta-commentary, no JSON wrapper. Just the article — title through closing CTA.

REVISION_RULES

agent_draft.py

The reader test is built as an accountability surface, not as an in-prompt rubric the model would mirror back as section headings. Three concrete questions a developer should be able to answer after reading.

View prompt
# Revision

You just composed the draft above. Now revise it.

Your job in this pass is editorial self-review, not rewriting from scratch. Read the draft once with the reader test in mind. Read it again with the anti-pattern list in mind. Note what would change and why. Then produce the revised version.

## Reader test (actionable form, from shared_rules.md §9)

A developer reading the piece should come away able to:

1. Make a decision about which product fits their workload.
2. Explain the architectural difference between the products in their own words.
3. Articulate at least one thing the non-commissioning product (Cloudflare) does better than the commissioning one (Vercel).

If your honest read says the draft fails any of these, revise the relevant section before final output.

## Anti-pattern check

Review against the universal anti-patterns enumerated in shared_rules.md §8:
- False balance ("both products have tradeoffs")
- Universal winner ("Product X is better")
- Marketing copy with attribution
- Feature inventory disguised as comparison
- Hedging that dissolves real differences
- Burying the architectural fork
- Ignoring the category authority

## Output format

Output exactly this two-section format:

=== REVISION_NOTES ===
[Specific notes on what you found in self-review and what you changed.]

=== FINAL_DRAFT ===
[The revised complete piece in markdown. Title through closing CTA.]

EXTRACTION_RULES

agent_extract.py

Schema-driven extraction where source type is inferred from the source's role rather than branched on hardcoded JSON keys. The note "extract from the page CONTENT, not from claim_summary" is a guardrail against a common failure mode.

View prompt
# Extraction rules

You are extracting claims from a single web source for a competitive comparison article. The editorial context above governs WHY claims matter; these rules govern HOW to extract them.

You will receive:
- The source's metadata (role, vendor, dimensions it covers, optional claim_summary previewing what to look for, optional disputed_claims this source takes positions on)
- Handling rules from `roles.json` (claim_handling, extraction_depth, quotable, always_attribute)
- The fetched page content

Return a JSON object of this exact shape:

{
  "discovered_published_date": "<YYYY-MM-DD if a publication date is visibly stated on the page; otherwise null>",
  "claims": [
    {
      "claim": "<one verifiable fact or position, in plain English, phrased per the claim_handling rule>",
      "verbatim_quote": "<short direct quote supporting the claim, only if quotable=true and a quote is available; otherwise null>",
      "dimensions_covered": ["<subset of the source's dimensions_covered that THIS specific claim addresses; do not just copy the source's full list>"],
      "disputed_claims_ref": ["<from the source's disputed_claims list if this claim addresses one of them; otherwise empty array>"]
    }
  ]
}

Universal rules:
- Only emit what the source actually says. Do NOT invent. Do NOT extrapolate.
- Use claim_summary as a hint about what to look for, but extract from the page CONTENT, not from claim_summary.
- Each claim must be discrete and verifiable from the source content.
- Skip vague developer-marketing phrases unless quantified.
- Output ONLY valid JSON. No prose before or after.

CLAIM_HANDLING_GUIDANCE

agent_extract.py

Five distinct rhetorical postures the system can apply to any claim. The separation of factual_with_attribution from partisan_critique is the editorial decision that lets vendor docs be quoted as fact while competitor critiques get explicit attribution.

View prompt
CLAIM_HANDLING_GUIDANCE = {
    "factual_authoritative": (
        "State as fact. No hedging. The source is authoritative for these claims."
    ),
    "factual_with_attribution": (
        "State as the vendor's claim. Use phrasing like 'Vercel says X' or "
        "'according to Cloudflare's docs'. The reader should know this is the "
        "vendor's own positioning, not an independent assessment."
    ),
    "partisan_critique": (
        "State as the source's argument with explicit attribution baked into the "
        "claim text. Do NOT state as fact. Use phrasing like 'Inngest argues that X' "
        "or 'Per Inngest's blog, Y'. The reader must understand this is the source's "
        "position, not a neutral truth."
    ),
    "category_authority": (
        "State as the framework or definition the source establishes for the broader "
        "category. Useful for vocabulary and taxonomy, not for product-specific facts."
    ),
    "community_signal": (
        "Emit ONLY thematic summaries of what the community is discussing. Do NOT "
        "extract individual commenter quotes or attribute claims to specific users. "
        "Each claim should be a thematic observation like 'Developers expressed "
        "concern about X' or 'The thread surfaces tension between Y and Z'. The "
        "output is evidence that a debate exists, not what specific people said."
    ),
}

_SELECT_VOICE_ANCHOR( )

prompt_context.py

Voice samples are matched to pieces through metadata. Selection happens deterministically: exact match on genre and subject domain wins, with primary_voice_anchor: true as universal fallback.

View prompt
def _select_voice_anchor(brief_anchor: dict, style_refs: list) -> dict:
    """
    Select the best-matching style_reference per the rules locked in step 1:
      - Exact match on genre AND subject_domain wins.
      - If multiple matches: primary_voice_anchor=true is the tiebreaker;
        otherwise first in array order.
      - If zero matches: fall back to primary_voice_anchor=true (universal
        fallback). Closest-but-weak is worse than the explicit primary.
    """
    primaries = [r for r in style_refs if r.get("primary_voice_anchor") is True]
    if len(primaries) > 1:
        raise ValueError(
            f"Schema violation: {len(primaries)} style_references have "
            "primary_voice_anchor=true; expected exactly one."
        )
    primary = primaries[0] if primaries else None

    genre          = brief_anchor.get("genre")
    subject_domain = brief_anchor.get("subject_domain")

    matches = [
        r for r in style_refs
        if r.get("genre") == genre and r.get("subject_domain") == subject_domain
    ]

    if matches:
        if len(matches) == 1:
            return matches[0]
        primary_in_matches = [r for r in matches if r.get("primary_voice_anchor")]
        return primary_in_matches[0] if primary_in_matches else matches[0]

    # Zero matches → fall back to primary (universal tiebreaker)
    if primary is None:
        raise ValueError(
            f"No style_reference matches genre={genre!r} subject_domain={subject_domain!r}, "
            "and no primary_voice_anchor is set. Cannot select a voice anchor."
        )
    return primary

TENSION_RULES

agent_compare.py

The architectural centerpiece of the comparison agent. The constraint that "narrative_payload WILL APPEAR in the final piece" inverts the typical synthesis-suggestive / draft-authoritative hierarchy.

View prompt
# Tension composition

Compose a single editorial tension entry. Return JSON of this shape:

{
  "subject_a_position": "<...>" | null,
  "subject_a_steelman": "<charitable case for subject_a's choice>" | null,
  "subject_b_position": "<...>" | null,
  "subject_b_steelman": "<charitable case for subject_b's choice>" | null,
  "disclosing_subject": "subject_a" | "subject_b" | null,
  "disclosure": "<what the subject discloses>" | null,
  "external_voice": "<vendor name>" | null,
  "critique_summary": "<the critique>" | null,
  "narrative_payload": "<3-5 sentence draft-ready paragraph>",
  "draft_placement": "<where this paragraph belongs in the piece>",
  "steelman_quality": "both_strong" | "asymmetric" | "weak",
  "reader_decision_impact": {
    "score": 0 | 1 | 2 | 3,
    "reasoning": "<one sentence>"
  }
}

Type-specific population:
  - design_disagreement_between_subjects: populate the four subject_*/steelman fields. Leave disclosure/external fields null.
  - subject_disclosure: populate disclosing_subject + disclosure. Leave subject_*/external fields null.
  - external_critique: populate external_voice + critique_summary. Leave subject_*/disclosure null.

NARRATIVE_PAYLOAD constraints (CRITICAL):
- 3 to 5 sentences. No more, no less.
- Voice and register per shared_rules.md §4 AND piece_brief.md §Voice register.
- This paragraph WILL APPEAR in the final piece. Write it as draft prose, not as analysis ABOUT the tension.
- The draft agent will insert this verbatim or with minimal edits and write only connective tissue around it.
- For two-sided tensions: present both positions with charitable framing. Do not resolve.

Output ONLY valid JSON.