userType: external

gitBranch: HEAD

slug: cached-frolicking-dewdrop

type: user

role: user

content:
Explore /root/projects/ai_usecases_explorer/src/ai_usecases_explorer/ directory. Read and return the FULL content of these files:

1. collectors/base.py
2. collectors/reddit.py (Exa.ai SDK usage pattern)
3. processors/filter.py
4. processors/classifier.py
5. processors/utils.py (parse_llm_json)
6. models/usecase.py

I need to understand:
- How RawItem and BaseCollector are structured
- How Exa.ai SDK is used (search pattern, result handling)
- How LLM calls are made (Claude API pattern: _call_llm, run methods)
- How JSON responses are parsed from LLM
- What Pydantic models look like in this project

Return the full file contents.


type: message

role: assistant

type: text

text: I'll explore the directory and read all the specified files for you.

service_tier: standard

type: assistant


Bash

type: direct

speed: standard


content:
总计 16
drwxr-xr-x 8 root root  160  2月27日 23:18 .
drwxr-xr-x 3 root root   34  2月27日 10:26 ..
drwxr-xr-x 3 root root  114  2月27日 23:17 collectors
-rw-r--r-- 1 root root    0  2月27日 10:26 __init__.py
-rw-r--r-- 1 root root 9024  2月27日 23:18 main.py
drwxr-xr-x 3 root root   62  2月27日 11:23 models
drwxr-xr-x 3 root root  121  2月27日 19:16 processors
drwxr-xr-x 2 root root   98  2月27日 23:21 __pycache__
drwxr-xr-x 3 root root   63  2月27日 18:56 reporters
-rw-r--r-- 1 root root 1431  2月27日 21:52 settings.py
drwxr-xr-x 3 root root   63  2月27日 10:41 storage


text: Now let me read all the requested files:


content:
     1→"""Abstract base class for all data collectors."""
     2→from abc import ABC, abstractmethod
     3→from dataclasses import dataclass
     4→from datetime import datetime
     5→
     6→
     7→@dataclass
     8→class RawItem:
     9→    """Minimal raw item returned by collectors before LLM processing."""
    10→
    11→    title: str
    12→    source_url: str
    13→    source_platform: str
    14→    author: str
    15→    published_at: datetime
    16→    raw_content: str
    17→    score: int = 0  # platform engagement: HN points, GitHub stars, 0 for Reddit
    18→
    19→
    20→class BaseCollector(ABC):
    21→    """Every collector must implement fetch() and return a list of RawItems."""
    22→
    23→    @abstractmethod
    24→    def fetch(self) -> list[RawItem]:
    25→        """Fetch items from the source and return filtered raw items."""
    26→        ...
    27→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


type: progress

hookEvent: PostToolUse

command: callback


content:
     1→"""Reddit collector using Exa.ai neural search, one request per subreddit."""
     2→import logging
     3→from datetime import UTC, datetime, timedelta
     4→
     5→from exa_py import Exa  # type: ignore[import-untyped]
     6→
     7→from ai_usecases_explorer.collectors.base import BaseCollector, RawItem
     8→
     9→logger = logging.getLogger(__name__)
    10→
    11→# Broad AI query — semantically covers all target subreddits
    12→_SEARCH_QUERY = "AI workflow automation use case tool productivity"
    13→
    14→_DEFAULT_SUBREDDITS = ["openclaw", "vibecoding", "ClaudeCode", "singularity"]
    15→
    16→
    17→class RedditCollector(BaseCollector):
    18→    """Fetches posts from specific subreddits via Exa.ai (one request per subreddit)."""
    19→
    20→    def __init__(
    21→        self,
    22→        subreddits: list[str],
    23→        api_key: str = "",
    24→    ) -> None:
    25→        self._subreddits = subreddits
    26→        self._api_key = api_key
    27→
    28→    def fetch(self) -> list[RawItem]:
    29→        if not self._api_key:
    30→            logger.warning("EXA_API_KEY not set — skipping Reddit/Exa collection")
    31→            return []
    32→
    33→        exa = Exa(api_key=self._api_key)
    34→        start_date = (datetime.now(tz=UTC) - timedelta(hours=24)).strftime("%Y-%m-%dT%H:%M:%SZ")
    35→
    36→        seen: set[str] = set()
    37→        items: list[RawItem] = []
    38→
    39→        for sub in self._subreddits:
    40→            try:
    41→                response = exa.search_and_contents(
    42→                    _SEARCH_QUERY,
    43→                    include_domains=[f"reddit.com/r/{sub}"],
    44→                    start_published_date=start_date,
    45→                    text=True,
    46→                    num_results=10,
    47→                )
    48→            except Exception as exc:
    49→                logger.warning("Exa search failed for r/%s: %s", sub, exc)
    50→                continue
    51→
    52→            for result in response.results:
    53→                url = result.url or ""
    54→                if not url or url in seen:
    55→                    continue
    56→                # Post-filter: Exa doesn't strictly enforce include_domains
    57→                if f"/r/{sub.lower()}/" not in url.lower():
    58→                    continue
    59→                seen.add(url)
    60→
    61→                title = result.title or url
    62→                text = result.text or ""
    63→                raw = f"{title}\n\n{text}".strip()
    64→
    65→                published_at = _parse_date(getattr(result, "published_date", None))
    66→
    67→                items.append(
    68→                    RawItem(
    69→                        title=title,
    70→                        source_url=url,
    71→                        source_platform="reddit",
    72→                        author=getattr(result, "author", None) or "unknown",
    73→                        published_at=published_at,
    74→                        raw_content=raw,
    75→                    )
    76→                )
    77→
    78→        return items
    79→
    80→
    81→def _parse_date(value: str | None) -> datetime:
    82→    """Parse an ISO-8601 date string, falling back to now(UTC)."""
    83→    if not value:
    84→        return datetime.now(tz=UTC)
    85→    try:
    86→        return datetime.fromisoformat(value.replace("Z", "+00:00"))
    87→    except ValueError:
    88→        return datetime.now(tz=UTC)
    89→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


content:
     1→"""Step 1 — Value filter: determines if a raw item has real AI use-case value."""
     2→import anthropic
     3→
     4→from ai_usecases_explorer.collectors.base import RawItem
     5→from ai_usecases_explorer.processors.utils import parse_llm_json
     6→
     7→_SYSTEM = """\
     8→You are an AI use case filter. Your job is to decide whether a post represents
     9→a genuinely valuable AI use case.
    10→
    11→PASS criteria (at least one must be true):
    12→- The user integrates AI with local/personal data (files, DB, notes, etc.)
    13→- The user automates a specific workflow step — not just asking questions
    14→
    15→REJECT criteria:
    16→- Pure Q&A replacing ChatGPT with no local data or workflow change
    17→- Marketing demos without real user implementation
    18→- Generic "AI is great" commentary
    19→
    20→Respond with ONLY valid JSON (no markdown fences), exactly this shape:
    21→{"is_real_value": bool, "reason": "...", "has_local_data": bool, "has_workflow_opt": bool}
    22→"""
    23→
    24→
    25→class ValueFilter:
    26→    """Calls Claude to decide whether a RawItem has real use-case value."""
    27→
    28→    def __init__(
    29→        self, client: anthropic.Anthropic, model: str = "claude-haiku-4-5-20251001"
    30→    ) -> None:
    31→        self._client = client
    32→        self._model = model
    33→
    34→    def run(self, item: RawItem) -> dict:
    35→        """Return a dict with is_real_value, has_local_data, has_workflow_opt, reason."""
    36→        return self._call_llm(item)
    37→
    38→    def _call_llm(self, item: RawItem) -> dict:
    39→        content = f"Title: {item.title}\n\nContent:\n{item.raw_content[:3000]}"
    40→        message = self._client.messages.create(
    41→            model=self._model,
    42→            max_tokens=256,
    43→            system=_SYSTEM,
    44→            messages=[{"role": "user", "content": content}],
    45→        )
    46→        return parse_llm_json(message.content[0].text)
    47→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


content:
     1→"""Step 2 — Classifier: assigns scenario type, tools, score, and summary."""
     2→import anthropic
     3→
     4→from ai_usecases_explorer.collectors.base import RawItem
     5→from ai_usecases_explorer.models.usecase import ScenarioType
     6→from ai_usecases_explorer.processors.utils import parse_llm_json
     7→
     8→_SCENARIO_VALUES = "\n".join(f'- "{s.value}"' for s in ScenarioType)
     9→
    10→_SYSTEM = f"""\
    11→You are an AI use case classifier. Classify the post into the most appropriate
    12→scenario type, extract the tools mentioned, give a value score, and write a
    13→2-3 sentence summary in Chinese.
    14→
    15→Scenario types (use EXACTLY one of these string values):
    16→{_SCENARIO_VALUES}
    17→
    18→Respond with ONLY valid JSON (no markdown fences), exactly this shape:
    19→{{"scenario_type": "...", "tools_used": ["..."], "value_score": 0.0, "summary": "..."}}
    20→
    21→value_score: 0.0–1.0. Higher = more practical, more local-data usage, clearer workflow benefit.
    22→"""
    23→
    24→
    25→class Classifier:
    26→    """Calls Claude to classify a RawItem and generate its summary."""
    27→
    28→    def __init__(
    29→        self, client: anthropic.Anthropic, model: str = "claude-haiku-4-5-20251001"
    30→    ) -> None:
    31→        self._client = client
    32→        self._model = model
    33→
    34→    def run(self, item: RawItem) -> dict:
    35→        """Return scenario_type (ScenarioType enum), tools_used, value_score, summary."""
    36→        raw = self._call_llm(item)
    37→        # Map string value to enum, fall back to OTHER if unknown
    38→        scenario_str = raw.get("scenario_type", "")
    39→        if not isinstance(scenario_str, ScenarioType):
    40→            try:
    41→                raw["scenario_type"] = ScenarioType(scenario_str)
    42→            except ValueError:
    43→                raw["scenario_type"] = ScenarioType.OTHER
    44→        return raw
    45→
    46→    def _call_llm(self, item: RawItem) -> dict:
    47→        content = f"Title: {item.title}\n\nContent:\n{item.raw_content[:3000]}"
    48→        message = self._client.messages.create(
    49→            model=self._model,
    50→            max_tokens=512,
    51→            system=_SYSTEM,
    52→            messages=[{"role": "user", "content": content}],
    53→        )
    54→        return parse_llm_json(message.content[0].text)
    55→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


content:
     1→"""Pydantic data models for AI use cases."""
     2→import hashlib
     3→from datetime import datetime
     4→from enum import StrEnum
     5→
     6→from pydantic import BaseModel, Field
     7→
     8→
     9→def make_id(source_url: str) -> str:
    10→    """Return first 12 hex chars of sha256(source_url)."""
    11→    return hashlib.sha256(source_url.encode()).hexdigest()[:12]
    12→
    13→
    14→class ScenarioType(StrEnum):
    15→    LOCAL_DATA_INTEGRATION = "本地数据集成"
    16→    CODE_GEN_REVIEW = "代码生成与审查"
    17→    WORKFLOW_AUTOMATION = "工作流自动化"
    18→    DOCUMENT_PROCESSING = "文档与内容处理"
    19→    TERMINAL_CLI = "终端与CLI增强"
    20→    CREATIVE_PRODUCTION = "创意内容生产"
    21→    DATA_ANALYSIS = "数据分析与可视化"
    22→    PERSONAL_KNOWLEDGE = "个人知识管理"
    23→    BUSINESS_PROCESS = "商业流程自动化"
    24→    OTHER = "其他"
    25→
    26→
    27→class Novelty(StrEnum):
    28→    NEW = "new"
    29→    SIMILAR = "similar"
    30→    REPEAT = "repeat"
    31→
    32→
    33→class UseCase(BaseModel):
    34→    id: str
    35→    title: str
    36→    summary: str
    37→    source_url: str
    38→    source_platform: str
    39→    author: str
    40→    published_at: datetime
    41→    collected_at: datetime
    42→    scenario_type: ScenarioType
    43→    tools_used: list[str]
    44→    has_local_data: bool
    45→    has_workflow_opt: bool
    46→    value_score: float = Field(ge=0.0, le=1.0)
    47→    is_real_value: bool
    48→    novelty: Novelty
    49→    similar_to_ids: list[str]
    50→    raw_content: str
    51→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


content:
     1→"""Shared utilities for LLM processors."""
     2→import json
     3→import re
     4→
     5→
     6→def parse_llm_json(text: str) -> dict:
     7→    """Parse JSON from LLM output, stripping markdown code fences if present."""
     8→    text = text.strip()
     9→    if not text:
    10→        raise ValueError("LLM returned empty response")
    11→    # Strip ```json ... ``` or ``` ... ``` fences
    12→    text = re.sub(r"^```(?:json)?\s*", "", text)
    13→    text = re.sub(r"\s*```$", "", text)
    14→    return json.loads(text.strip())
    15→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


text:
Perfect! I've retrieved all the requested files. Here's the complete content:

---

## File 1: `collectors/base.py`

```python
"""Abstract base class for all data collectors."""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime

@dataclass
class RawItem:
    """Minimal raw item returned by collectors before LLM processing."""

    title: str
    source_url: str
    source_platform: str
    author: str
    published_at: datetime
    raw_content: str
    score: int = 0  # platform engagement: HN points, GitHub stars, 0 for Reddit

class BaseCollector(ABC):
    """Every collector must implement fetch() and return a list of RawItems."""

    @abstractmethod
    def fetch(self) -> list[RawItem]:
        """Fetch items from the source and return filtered raw items."""
        ...
```

---

## File 2: `collectors/reddit.py` (Exa.ai SDK)

```python
"""Reddit collector using Exa.ai neural search, one request per subreddit."""
import logging
from datetime import UTC, datetime, timedelta

from exa_py import Exa  # type: ignore[import-untyped]

from ai_usecases_explorer.collectors.base import BaseCollector, RawItem

logger = logging.getLogger(__name__)

# Broad AI query — semantically covers all target subreddits
_SEARCH_QUERY = "AI workflow automation use case tool productivity"

_DEFAULT_SUBREDDITS = ["openclaw", "vibecoding", "ClaudeCode", "singularity"]

class RedditCollector(BaseCollector):
    """Fetches posts from specific subreddits via Exa.ai (one request per subreddit)."""

    def __init__(
        self,
        subreddits: list[str],
        api_key: str = "",
    ) -> None:
        self._subreddits = subreddits
        self._api_key = api_key

    def fetch(self) -> list[RawItem]:
        if not self._api_key:
            logger.warning("EXA_API_KEY not set — skipping Reddit/Exa collection")
            return []

        exa = Exa(api_key=self._api_key)
        start_date = (datetime.now(tz=UTC) - timedelta(hours=24)).strftime("%Y-%m-%dT%H:%M:%SZ")

        seen: set[str] = set()
        items: list[RawItem] = []

        for sub in self._subreddits:
            try:
                response = exa.search_and_contents(
                    _SEARCH_QUERY,
                    include_domains=[f"reddit.com/r/{sub}"],
                    start_published_date=start_date,
                    text=True,
                    num_results=10,
                )
            except Exception as exc:
                logger.warning("Exa search failed for r/%s: %s", sub, exc)
                continue

            for result in response.results:
                url = result.url or ""
                if not url or url in seen:
                    continue
                # Post-filter: Exa doesn't strictly enforce include_domains
                if f"/r/{sub.lower()}/" not in url.lower():
                    continue
                seen.add(url)

                title = result.title or url
                text = result.text or ""
                raw = f"{title}\n\n{text}".strip()

                published_at = _parse_date(getattr(result, "published_date", None))

                items.append(
                    RawItem(
                        title=title,
                        source_url=url,
                        source_platform="reddit",
                        author=getattr(result, "author", None) or "unknown",
                        published_at=published_at,
                        raw_content=raw,
                    )
                )

        return items

def _parse_date(value: str | None) -> datetime:
    """Parse an ISO-8601 date string, falling back to now(UTC)."""
    if not value:
        return datetime.now(tz=UTC)
    try:
        return datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError:
        return datetime.now(tz=UTC)
```

---

## File 3: `processors/filter.py`

```python
"""Step 1 — Value filter: determines if a raw item has real AI use-case value."""
import anthropic

from ai_usecases_explorer.collectors.base import RawItem
from ai_usecases_explorer.processors.utils import parse_llm_json

_SYSTEM = """\
You are an AI use case filter. Your job is to decide whether a post represents
a genuinely valuable AI use case.

PASS criteria (at least one must be true):
- The user integrates AI with local/personal data (files, DB, notes, etc.)
- The user automates a specific workflow step — not just asking questions

REJECT criteria:
- Pure Q&A replacing ChatGPT with no local data or workflow change
- Marketing demos without real user implementation
- Generic "AI is great" commentary

Respond with ONLY valid JSON (no markdown fences), exactly this shape:
{"is_real_value": bool, "reason": "...", "has_local_data": bool, "has_workflow_opt": bool}
"""

class ValueFilter:
    """Calls Claude to decide whether a RawItem has real use-case value."""

    def __init__(
        self, client: anthropic.Anthropic, model: str = "claude-haiku-4-5-20251001"
    ) -> None:
        self._client = client
        self._model = model

    def run(self, item: RawItem) -> dict:
        """Return a dict with is_real_value, has_local_data, has_workflow_opt, reason."""
        return self._call_llm(item)

    def _call_llm(self, item: RawItem) -> dict:
        content = f"Title: {item.title}\n\nContent:\n{item.raw_content[:3000]}"
        message = self._client.messages.create(
            model=self._model,
            max_tokens=256,
            system=_SYSTEM,
            messages=[{"role": "user", "content": content}],
        )
        return parse_llm_json(message.content[0].text)
```

---

## File 4: `processors/classifier.py`

```python
"""Step 2 — Classifier: assigns scenario type, tools, score, and summary."""
import anthropic

from ai_usecases_explorer.collectors.base import RawItem
from ai_usecases_explorer.models.usecase import ScenarioType
from ai_usecases_explorer.processors.utils import parse_llm_json

_SCENARIO_VALUES = "\n".join(f'- "{s.value}"' for s in ScenarioType)

_SYSTEM = f"""\
You are an AI use case classifier. Classify the post into the most appropriate
scenario type, extract the tools mentioned, give a value score, and write a
2-3 sentence summary in Chinese.

Scenario types (use EXACTLY one of these string values):
{_SCENARIO_VALUES}

Respond with ONLY valid JSON (no markdown fences), exactly this shape:
{{"scenario_type": "...", "tools_used": ["..."], "value_score": 0.0, "summary": "..."}}

value_score: 0.0–1.0. Higher = more practical, more local-data usage, clearer workflow benefit.
"""

class Classifier:
    """Calls Claude to classify a RawItem and generate its summary."""

    def __init__(
        self, client: anthropic.Anthropic, model: str = "claude-haiku-4-5-20251001"
    ) -> None:
        self._client = client
        self._model = model

    def run(self, item: RawItem) -> dict:
        """Return scenario_type (ScenarioType enum), tools_used, value_score, summary."""
        raw = self._call_llm(item)
        # Map string value to enum, fall back to OTHER if unknown
        scenario_str = raw.get("scenario_type", "")
        if not isinstance(scenario_str, ScenarioType):
            try:
                raw["scenario_type"] = ScenarioType(scenario_str)
            except ValueError:
                raw["scenario_type"] = ScenarioType.OTHER
        return raw

    def _call_llm(self, item: RawItem) -> dict:
        content = f"Title: {item.title}\n\nContent:\n{item.raw_content[:3000]}"
        message = self._client.messages.create(
            model=self._model,
            max_tokens=512,
            system=_SYSTEM,
            messages=[{"role": "user", "content": content}],
        )
        return parse_llm_json(message.content[0].text)
```

---

## File 5: `processors/utils.py`

```python
"""Shared utilities for LLM processors."""
import json
import re

def parse_llm_json(text: str) -> dict:
    """Parse JSON from LLM output, stripping markdown code fences if present."""
    text = text.strip()
    if not text:
        raise ValueError("LLM returned empty response")
    # Strip ```json ... ``` or ``` ... ``` fences
    text = re.sub(r"^```(?:json)?\s*", "", text)
    text = re.sub(r"\s*```$", "", text)
    return json.loads(text.strip())
```

---

## File 6: `models/usecase.py`

```python
"""Pydantic data models for AI use cases."""
import hashlib
from datetime import datetime
from enum import StrEnum

from pydantic import BaseModel, Field

def make_id(source_url: str) -> str:
    """Return first 12 hex chars of sha256(source_url)."""
    return hashlib.sha256(source_url.encode()).hexdigest()[:12]

class ScenarioType(StrEnum):
    LOCAL_DATA_INTEGRATION = "本地数据集成"
    CODE_GEN_REVIEW = "代码生成与审查"
    WORKFLOW_AUTOMATION = "工作流自动化"
    DOCUMENT_PROCESSING = "文档与内容处理"
    TERMINAL_CLI = "终端与CLI增强"
    CREATIVE_PRODUCTION = "创意内容生产"
    DATA_ANALYSIS = "数据分析与可视化"
    PERSONAL_KNOWLEDGE = "个人知识管理"
    BUSINESS_PROCESS = "商业流程自动化"
    OTHER = "其他"

class Novelty(StrEnum):
    NEW = "new"
    SIMILAR = "similar"
    REPEAT = "repeat"

class UseCase(BaseModel):
    id: str
    title: str
    summary: str
    source_url: str
    source_platform: str
    author: str
    published_at: datetime
    collected_at: datetime
    scenario_type: ScenarioType
    tools_used: list[str]
    has_local_data: bool
    has_workflow_opt: bool
    value_score: float = Field(ge=0.0, le=1.0)
    is_real_value: bool
    novelty: Novelty
    similar_to_ids: list[str]
    raw_content: str
```

---

## Summary of Key Architectural Patterns

**RawItem & BaseCollector** (base.py):
- `RawItem` is a simple dataclass with minimal fields: title, URL, platform, author, date, content, score
- `BaseCollector` is an abstract base class requiring subclasses to implement `fetch() -> list[RawItem]`

**Exa.ai SDK Usage** (reddit.py):
- Instantiate with `Exa(api_key=...)` and call `exa.search_and_contents(query, include_domains=[...], start_published_date=..., text=True, num_results=10)`
- Results are accessed via `response.results` with attributes: `.url`, `.title`, `.text`, `.author`, `.published_date`
- No score returned (defaults to 0), deduplication via URL checking, post-filter for correct subreddit

**LLM Call Pattern** (filter.py, classifier.py):
- Each processor has `run(item) -> dict` and `_call_llm(item) -> dict` methods
- `_call_llm` uses `self._client.messages.create(model=..., max_tokens=..., system=..., messages=[...])`
- Response JSON is extracted from `message.content[0].text` and passed to `parse_llm_json()`
- `Classifier.run()` does post-processing to map string → enum before returning

**JSON Parsing** (utils.py):
- `parse_llm_json()` strips markdown code fences (both `` ```json `` and `` ``` ``) then parses with `json.loads()`
- Handles Haiku's tendency to wrap JSON in markdown despite instructions

**Pydantic Models** (usecase.py):
- `ScenarioType` and `Novelty` are `StrEnum` with Chinese/English string values
- `UseCase` is a `BaseModel` with type hints on all fields, including Field validators (e.g., `value_score` must be 0.0–1.0)
- Helper function `make_id()` creates deterministic 12-char hex IDs from URL SHA256 hashes