You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ai_synth/docs/superpowers/plans/2026-03-23-source-diversity...

10 KiB

Source Diversity via Recent History — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Inject recently-used domains into the LLM search prompt to encourage source diversity across syntheses.

Architecture: New source_diversity_window setting (default 3, 0=disabled). At generation time, load recent syntheses, extract domains from JSONB sections, pass to prompt builder which appends a soft avoidance instruction.

Tech Stack: Rust (sqlx, serde_json, url crate), SolidJS, PostgreSQL

Spec: docs/superpowers/specs/2026-03-23-source-diversity-history-design.md


Task 1: Migration + backend model

Files:

  • Create: backend/migrations/20260323000013_add_source_diversity_window.sql

  • Modify: backend/src/models/settings.rs

  • Modify: backend/src/db/settings.rs

  • Modify: CLAUDE.md

  • Step 1: Create migration

ALTER TABLE settings ADD COLUMN source_diversity_window INTEGER NOT NULL DEFAULT 3;
  • Step 2: Add field to all structs in models/settings.rs

Add pub source_diversity_window: i32 to UserSettings, SettingsResponse, UpdateSettingsRequest (after max_articles_per_source).

Add to From<UserSettings> for SettingsResponse:

source_diversity_window: s.source_diversity_window,

Add validation in UpdateSettingsRequest::validate():

if !(0..=10).contains(&self.source_diversity_window) {
    return Err("source_diversity_window must be between 0 and 10".into());
}

Add to impl Default for UserSettings:

source_diversity_window: 3,
  • Step 3: Add column to DB queries in db/settings.rs

Add source_diversity_window: i32 to SettingsRow. Add to TryFrom<SettingsRow>:

source_diversity_window: row.source_diversity_window,

Add to both SQL queries (get_or_create_default and upsert): INSERT column list, VALUES placeholder, RETURNING clause, .bind() call, and ON CONFLICT SET (upsert only). The new column goes after max_articles_per_source.

  • Step 4: Update CLAUDE.md migration count to 13

  • Step 5: Add validation tests in models/settings.rs

Add source_diversity_window: 3 to the valid_request() test helper. Then add tests:

    #[test]
    fn test_source_diversity_window_zero_is_valid() {
        let mut req = valid_request();
        req.source_diversity_window = 0;
        assert!(req.validate().is_ok());
    }

    #[test]
    fn test_source_diversity_window_ten_is_valid() {
        let mut req = valid_request();
        req.source_diversity_window = 10;
        assert!(req.validate().is_ok());
    }

    #[test]
    fn test_source_diversity_window_below_range() {
        let mut req = valid_request();
        req.source_diversity_window = -1;
        assert!(req.validate().is_err());
    }

    #[test]
    fn test_source_diversity_window_above_range() {
        let mut req = valid_request();
        req.source_diversity_window = 11;
        assert!(req.validate().is_err());
    }
  • Step 6: Run tests

Run: cd backend && cargo test --lib Expected: all tests pass

  • Step 7: Commit
git add backend/migrations/20260323000013_add_source_diversity_window.sql backend/src/models/settings.rs backend/src/db/settings.rs CLAUDE.md
git commit -m "feat: add source_diversity_window setting (migration + model + DB)"

Task 2: Prompt modification + tests

Files:

  • Modify: backend/src/services/prompts.rs

  • Step 1: Add recent_domains parameter to build_search_prompt

Change signature from:

pub fn build_search_prompt(
    settings: &UserSettings,
    sources: &[Source],
    current_date: &str,
) -> (String, String) {

To:

pub fn build_search_prompt(
    settings: &UserSettings,
    sources: &[Source],
    current_date: &str,
    recent_domains: &[String],
) -> (String, String) {
  • Step 2: Append avoidance instruction when domains are non-empty

At the end of the user_prompt format string (after the JSON instruction line, before the closing "), add a conditional block. After the format!() call that builds user_prompt, append:

    let user_prompt = if recent_domains.is_empty() {
        user_prompt
    } else {
        let domains_list = recent_domains.join(", ");
        format!(
            "{}\n\nEvite si possible les sources deja utilisees dans les syntheses precedentes : {}.",
            user_prompt, domains_list
        )
    };
  • Step 3: Update test fixture

In the test_settings() function (~line 137), add:

source_diversity_window: 3,
  • Step 4: Update existing test calls

All existing tests that call build_search_prompt need the 4th argument. Add &[] (empty slice) to each existing call. Search for build_search_prompt( in the test module and add , &[] before the closing ).

  • Step 5: Add new tests
    #[test]
    fn search_prompt_includes_recent_domains_avoidance() {
        let settings = test_settings();
        let sources = vec![];
        let date = "lundi 17 mars 2026";
        let domains = vec!["techcrunch.com".to_string(), "theverge.com".to_string()];
        let (_, user_prompt) = build_search_prompt(&settings, &sources, date, &domains);
        assert!(user_prompt.contains("Evite si possible"));
        assert!(user_prompt.contains("techcrunch.com"));
        assert!(user_prompt.contains("theverge.com"));
    }

    #[test]
    fn search_prompt_no_avoidance_when_domains_empty() {
        let settings = test_settings();
        let sources = vec![];
        let date = "lundi 17 mars 2026";
        let (_, user_prompt) = build_search_prompt(&settings, &sources, date, &[]);
        assert!(!user_prompt.contains("Evite si possible"));
    }
  • Step 6: Run tests

Run: cd backend && cargo test --lib Expected: all tests pass

  • Step 7: Commit
git add backend/src/services/prompts.rs
git commit -m "feat: build_search_prompt accepts recent_domains for source diversity"

Task 3: Pipeline integration — extract domains + wire prompt

Files:

  • Modify: backend/src/services/synthesis.rs

  • Step 1: Add domain extraction from recent syntheses

Before the build_search_prompt call (~line 303), add a new step that loads recent syntheses and extracts domains. Insert between the rate limit check (step 5) and the search pass (step 6):

    // Step 5b: Load recently-used domains for source diversity
    let recent_domains = if settings.source_diversity_window > 0 {
        let recent = db::syntheses::list_for_user(
            &state.pool,
            user_id,
            settings.source_diversity_window as i64,
            0,
        )
        .await
        .unwrap_or_default();

        let mut domains: Vec<String> = recent
            .iter()
            .filter_map(|s| {
                serde_json::from_value::<Vec<crate::models::synthesis::NewsSection>>(
                    s.sections.clone(),
                )
                .ok()
            })
            .flat_map(|sections| {
                sections
                    .into_iter()
                    .flat_map(|sec| sec.items.into_iter())
                    .filter_map(|item| extract_domain(&item.url))
            })
            .collect();

        domains.sort();
        domains.dedup();
        domains
    } else {
        Vec::new()
    };
  • Step 2: Update the build_search_prompt call

Change line ~304 from:

    let (system_prompt, user_prompt) =
        prompts::build_search_prompt(&settings, &sources, &current_date);

To:

    let (system_prompt, user_prompt) =
        prompts::build_search_prompt(&settings, &sources, &current_date, &recent_domains);
  • Step 3: Run tests

Run: cd backend && cargo test --lib Expected: all tests pass

  • Step 4: Commit
git add backend/src/services/synthesis.rs
git commit -m "feat: extract recent domains and pass to search prompt for diversity"

Task 4: Frontend setting

Files:

  • Modify: frontend/src/types.ts

  • Modify: frontend/src/i18n/fr.ts

  • Modify: frontend/src/pages/Settings.tsx

  • Step 1: Add field to frontend types

In frontend/src/types.ts, add to UserSettings interface (after max_articles_per_source):

source_diversity_window: number;

Add to DEFAULT_SETTINGS:

source_diversity_window: 3,
  • Step 2: Add i18n label

In frontend/src/i18n/fr.ts, add after settings.maxArticlesPerSource:

'settings.diversityWindow': 'Syntheses a examiner pour diversite',
  • Step 3: Add number input to Settings page

In frontend/src/pages/Settings.tsx, inside the generation settings grid (after maxArticlesPerSource), add:

            <div>
              <label
                for="diversityWindow"
                class="block text-sm font-medium text-gray-700"
              >
                {t('settings.diversityWindow')}
              </label>
              <div class="mt-1">
                <input
                  type="number"
                  id="diversityWindow"
                  min="0"
                  max="10"
                  class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md py-2 px-3 border"
                  value={settings().source_diversity_window}
                  onInput={(e) =>
                    setSettings((prev) => ({
                      ...prev,
                      source_diversity_window:
                        parseInt(e.currentTarget.value) || 3,
                    }))
                  }
                />
              </div>
            </div>
  • Step 4: Run frontend tests

Run: cd frontend && npx tsc --noEmit && npx vitest run Expected: type check passes, all tests pass

  • Step 5: Commit
git add frontend/src/types.ts frontend/src/i18n/fr.ts frontend/src/pages/Settings.tsx
git commit -m "feat: add source_diversity_window setting to frontend"