From 57860aff1cbde6a58f3f7866f21d3dbe5d7a5916 Mon Sep 17 00:00:00 2001 From: Trevin Chow Date: Tue, 9 Jun 2026 16:33:29 -0700 Subject: [PATCH] feat(skill): scope narrative lens to first-party entities, gate on fetched positioning The lens criterion is now explicit: an identifiable first party with a fetchable public pitch - companies, products, services. People NEVER qualify, even founders whose companies would (MrBeast the company can get the lens; Jimmy Donaldson the person cannot; "Garry Tan vs Sam Altman" gets N/A across the row). Ownerless topics (Bitcoin) fail the same test - a foundation or fan site is not an authoritative first party. Comparison-row verdicts are also artifact-gated to match the single-entity beat: a verdict requires positioning fetched THIS run; if item 6 couldn't run (no WebSearch), the cell is Unclear - the pitch is never supplied from memory. --- CHANGELOG.md | 2 +- skills/last30days/SKILL.md | 10 +++++----- skills/last30days/scripts/lib/render.py | 8 ++++++-- tests/test_render_comparison_multi.py | 3 +++ 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1640096..8050048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **Narrative lens for company / product / service topics.** Comparison tables gain a `Setting the narrative?` axis that judges whether each entity's community conversation is actually about what the entity *pitches* (its first-party positioning) or about something else — pricing, rivals, an incident, a ToS change. A new mandatory research step captures each entity's current stated positioning from first-party sources (homepage, docs, pricing) rather than from memory, and single-entity company runs get a `narrative-check` synthesis beat surfacing the same signal. The mismatch is the point: companies usually don't control their own conversation. +- **Narrative lens for company / product / service topics.** Comparison tables gain a `Setting the narrative?` axis that judges whether each entity's community conversation is actually about what the entity *pitches* (its first-party positioning) or about something else — pricing, rivals, an incident, a ToS change. A new mandatory research step captures each entity's current stated positioning from first-party sources (homepage, docs, pricing) rather than from memory, and single-entity company runs get a `narrative-check` synthesis beat surfacing the same signal. The mismatch is the point: companies usually don't control their own conversation. The lens is scoped to entities with an identifiable first party (companies, products, services): people always get N/A — even founders whose companies would qualify — as do events, abstract concepts, and ownerless topics like Bitcoin, and verdicts require positioning fetched during the run, never from memory. ### Fixed diff --git a/skills/last30days/SKILL.md b/skills/last30days/SKILL.md index ad09a10..34aa650 100644 --- a/skills/last30days/SKILL.md +++ b/skills/last30days/SKILL.md @@ -641,7 +641,7 @@ Topic A (the main topic, first in the vs-string) uses outer `--x-handle`, `--x-r **Then do WebSearch supplements** for: `{TOPIC_A} vs {TOPIC_B} comparison {YEAR}` and `{TOPIC_A} vs {TOPIC_B} which is better` — these catch rivalry articles that per-entity passes might not surface. -**Fill the `Setting the narrative?` row using `RESOLVED_POSITIONING` + the community evidence.** Run Step 0.55 item 6 (first-party positioning) per entity - it gives you each entity's CURRENT stated pitch, fetched fresh, not from memory. Then judge: is the community conversation actually ABOUT what the entity pitches, or about something else (pricing, rivals, an incident, a ToS change)? Start the cell with **Yes / Partly / No / Unclear**, then name the topic the community is really on, anchored to a real item with its engagement (e.g. "No - pitches uptime, but the top thread is friendly-fraud, 323pt HN"). The mismatch is the signal: companies usually don't control their own conversation. Use **Unclear** when the evidence is thin or polluted with unrelated brand-name matches (e.g. someone's `.vercel.app` app, an exam "calc") - do NOT infer a verdict from vibes. Write N/A for entities with no public pitch (people, abstract concepts). +**Fill the `Setting the narrative?` row using `RESOLVED_POSITIONING` + the community evidence.** Run Step 0.55 item 6 (first-party positioning) per entity - it gives you each entity's CURRENT stated pitch, fetched fresh, not from memory. Then judge: is the community conversation actually ABOUT what the entity pitches, or about something else (pricing, rivals, an incident, a ToS change)? Start the cell with **Yes / Partly / No / Unclear**, then name the topic the community is really on, anchored to a real item with its engagement (e.g. "No - pitches uptime, but the top thread is friendly-fraud, 323pt HN"). The mismatch is the signal: companies usually don't control their own conversation. Use **Unclear** when the evidence is thin or polluted with unrelated brand-name matches (e.g. someone's `.vercel.app` app, an exam "calc") - do NOT infer a verdict from vibes. Write N/A for entities with no public pitch: people (ALWAYS, even founders/CEOs whose companies would qualify - "Garry Tan vs Sam Altman" gets N/A across the row), events, abstract concepts, and ownerless topics (Bitcoin - a foundation or fan site is not an authoritative first party). And only write a verdict when positioning was actually FETCHED this run: if item 6 could not run (e.g. no WebSearch), write Unclear - never supply the pitch from memory. **Skip the normal Step 1 below** - go directly to the comparison synthesis format (see "If QUERY_TYPE = COMPARISON" in the synthesis section). @@ -750,7 +750,7 @@ Store as `RESOLVED_IG_CREATORS`. Store as `RESOLVED_YT_QUERIES`. -**6. First-party positioning** - **MANDATORY when WebSearch is available, for company / product / service topics.** If the topic (or, in a vs-run, an entity) is a company, product, or service with a public presence, fetch its CURRENT stated positioning. Do **NOT** rely on memory - homepages and positioning go stale as companies rewrite copy and pivot, and a stale claim produces a false gap. Anchor on first-party sources: the homepage tagline, docs, pricing, or a "compare/why-us" page. Fold this into the per-entity passes above where you can (e.g. add `official site` to a query); otherwise run one focused search per entity (`{TOPIC} official site`, `{TOPIC} pricing`). Capture the one-line value prop and any explicit claims ("zero-config", "fastest", "open source"). Store as `RESOLVED_POSITIONING`. This is what the entity *pitches*; the engine's community data is what people *actually talk about*. Whether those two line up feeds the `Setting the narrative?` synthesis (the conversation is often on a different topic than the pitch - that mismatch is the signal). Skip (and omit `RESOLVED_POSITIONING`) for people, events, or abstract concepts - they make no comparable public claim. +**6. First-party positioning** - **MANDATORY when WebSearch is available, for company / product / service topics.** If the topic (or, in a vs-run, an entity) is a company, product, or service with a public presence, fetch its CURRENT stated positioning. Do **NOT** rely on memory - homepages and positioning go stale as companies rewrite copy and pivot, and a stale claim produces a false gap. Anchor on first-party sources: the homepage tagline, docs, pricing, or a "compare/why-us" page. Fold this into the per-entity passes above where you can (e.g. add `official site` to a query); otherwise run one focused search per entity (`{TOPIC} official site`, `{TOPIC} pricing`). Capture the one-line value prop and any explicit claims ("zero-config", "fastest", "open source"). Store as `RESOLVED_POSITIONING`. This is what the entity *pitches*; the engine's community data is what people *actually talk about*. Whether those two line up feeds the `Setting the narrative?` synthesis (the conversation is often on a different topic than the pitch - that mismatch is the signal). Skip (and omit `RESOLVED_POSITIONING`) for people, events, abstract concepts, and ownerless topics - they make no comparable public claim. The test is an identifiable first party with a fetchable pitch, and people NEVER pass it - not even founders/creators whose companies would qualify. The lens can apply to MrBeast (a company) but never to Jimmy Donaldson (a person); a person-vs-person run ("Garry Tan vs Sam Altman") gets no positioning research at all. Ownerless topics fail the same test: Bitcoin has no authoritative first party, and a foundation or fan site does not count. **Concrete examples:** @@ -807,7 +807,7 @@ Resolved: - Positioning: "{one-line stated value prop}" (first-party) ``` -Only show lines for platforms where something was resolved. Skip empty lines. On the Reddit line, the trailing `(+ {category_id} peers)` annotation appears when Step 0.55 Section 2a added category-peer subs. Omit the annotation when the topic had no matching category. The `Positioning:` line appears for company / product / service topics (from Step 0.55 item 6); omit it for people, events, and abstract concepts. This display replaces the old "Parsed intent" block with something more useful. +Only show lines for platforms where something was resolved. Skip empty lines. On the Reddit line, the trailing `(+ {category_id} peers)` annotation appears when Step 0.55 Section 2a added category-peer subs. Omit the annotation when the topic had no matching category. The `Positioning:` line appears for company / product / service topics (from Step 0.55 item 6); omit it for people, events, abstract concepts, and ownerless topics. This display replaces the old "Parsed intent" block with something more useful. --- @@ -1305,7 +1305,7 @@ Voice contract LAWs 1, 3, 5 apply to comparisons unchanged (no `Sources:` block, | Best for | ... | ... | ... | | Install | ... | ... | ... | -(Engine emits this scaffold; fill the cells with 5-15 words each. If an axis does not apply to the topic class, write "N/A" or a topic-appropriate substitute rather than inventing data. For the `Setting the narrative?` row, judge whether each entity's community conversation is about what the entity itself pitches: start the cell with **Yes / Partly / No / Unclear**, then name the topic the community is ACTUALLY on, anchored to a real item with engagement, e.g. "No - pitches uptime, but the top thread is friendly-fraud (323pt HN)". The mismatch is the signal - companies usually don't control their own conversation. Use "Unclear" when evidence is thin or polluted with unrelated brand-name matches; write N/A for entities with no public pitch, like people or abstract concepts. Do NOT infer a verdict from vibes.) +(Engine emits this scaffold; fill the cells with 5-15 words each. If an axis does not apply to the topic class, write "N/A" or a topic-appropriate substitute rather than inventing data. For the `Setting the narrative?` row, judge whether each entity's community conversation is about what the entity itself pitches: start the cell with **Yes / Partly / No / Unclear**, then name the topic the community is ACTUALLY on, anchored to a real item with engagement, e.g. "No - pitches uptime, but the top thread is friendly-fraud (323pt HN)". The mismatch is the signal - companies usually don't control their own conversation. Use "Unclear" when evidence is thin or polluted with unrelated brand-name matches; write N/A for entities with no public pitch - people (even famous founders), events, abstract concepts, and ownerless topics like Bitcoin. Verdicts require positioning fetched THIS run; if it wasn't fetched, write Unclear rather than pitching from memory. Do NOT infer a verdict from vibes.) ## The Bottom Line @@ -1447,7 +1447,7 @@ At render time the `@handle`, `r/sub`, and publication-name placeholders become Headlines should be specific and newsy ("BULLY dropped and it's dominating", "Europe is banning him one country at a time"), not generic ("Album release", "Tour updates"). -**Narrative-check beat (company / product / service topics).** If the topic is a company, product, or service and you captured `RESOLVED_POSITIONING` in Step 0.55, work in ONE bold-lead-in paragraph on whether the entity is setting its own narrative - i.e. is the community actually talking about what it pitches, or about something else (pricing, rivals, an incident, a ToS change)? Anchor the verdict to the real top-discussed item with its engagement - e.g. `**Vercel is losing the narrative to its pricing** - it still sells "the AI Cloud" and zero-overhead speed, but the loudest community thread this month is about cost and a June 1 ToS change, not performance`. When the conversation DOES track the pitch, say so (that is the "Yes" case, equally worth stating). Keep it a normal newsy bold-lead-in paragraph with a specific headline (per the rule above), NOT a new `##` section (LAW 4 still holds). Skip it silently for people, events, and abstract concepts, or when the evidence is too thin or noise-polluted to tell - do not manufacture a verdict. +**Narrative-check beat (company / product / service topics).** If the topic is a company, product, or service and you captured `RESOLVED_POSITIONING` in Step 0.55, work in ONE bold-lead-in paragraph on whether the entity is setting its own narrative - i.e. is the community actually talking about what it pitches, or about something else (pricing, rivals, an incident, a ToS change)? Anchor the verdict to the real top-discussed item with its engagement - e.g. `**Vercel is losing the narrative to its pricing** - it still sells "the AI Cloud" and zero-overhead speed, but the loudest community thread this month is about cost and a June 1 ToS change, not performance`. When the conversation DOES track the pitch, say so (that is the "Yes" case, equally worth stating). Keep it a normal newsy bold-lead-in paragraph with a specific headline (per the rule above), NOT a new `##` section (LAW 4 still holds). Skip it silently for people (always - the beat can cover MrBeast the company, never Jimmy Donaldson the person), events, abstract concepts, and ownerless topics (Bitcoin), or when the evidence is too thin or noise-polluted to tell - do not manufacture a verdict. **THEN - Quality Nudge (if present in the output):** diff --git a/skills/last30days/scripts/lib/render.py b/skills/last30days/scripts/lib/render.py index a0d1ed2..845155a 100644 --- a/skills/last30days/scripts/lib/render.py +++ b/skills/last30days/scripts/lib/render.py @@ -571,8 +571,12 @@ def _render_comparison_scaffold(topic: str) -> list[str]: "what the entity itself pitches: start the cell with Yes / Partly / No / Unclear, then name the topic " "the community is ACTUALLY on, anchored to a real item (e.g. \"No - pitches uptime, but the top thread " "is friendly-fraud (323pt HN)\"). Use Unclear when evidence is thin or polluted with unrelated " - "brand-name matches; do NOT infer a verdict from vibes. Write N/A for entities with no public pitch " - "(people, abstract concepts). This scaffold matches the April 9 launch-video exemplar shape." + "brand-name matches; do NOT infer a verdict from vibes. Write N/A for entities with no public pitch: " + "people (always, even famous founders - the lens applies to companies and products, never persons), " + "events, abstract concepts, and ownerless topics with no authoritative first party (e.g. Bitcoin). " + "Only fill a verdict when the entity's first-party positioning was actually fetched during this " + "run's research; if it was not, write Unclear - never supply the pitch from memory. " + "This scaffold matches the April 9 launch-video exemplar shape." ) return [ diff --git a/tests/test_render_comparison_multi.py b/tests/test_render_comparison_multi.py index e616f33..ee44f07 100644 --- a/tests/test_render_comparison_multi.py +++ b/tests/test_render_comparison_multi.py @@ -103,6 +103,9 @@ class RenderComparisonMultiTests(unittest.TestCase): self.assertIn("| Dimension | OpenAI | Anthropic | xAI |", rendered) # The narrative-lens axis is emitted as a scaffold row self.assertIn("| Setting the narrative? |", rendered) + # Fill instructions carry the scope + artifact gate (no verdicts for + # people/ownerless topics, no pitch from memory) + self.assertIn("never supply the pitch from memory", rendered) # Envelope scaffolding present self.assertIn("EVIDENCE FOR SYNTHESIS", rendered) self.assertIn("END OF last30days CANONICAL OUTPUT", rendered)