| name | add-source |
| description | Step-by-step guide for adding a new data source to the Federal Website Index |
| trigger | Use when user wants to add a new source list, integrate a new dataset, or expand coverage with additional data sources |
Add Source Skill
Guide for adding a new data source to the Federal Website Index.
When to Add a Source
Add a new source list when:
- You discover a dataset containing federal government URLs
- You want to improve coverage of a specific agency or domain type
- You have a curated list of sites that should be included
Source List Requirements
A valid source must:
- Contain URLs or domains of federal websites
- Be accessible via HTTP/HTTPS URL or local file path
- Be in CSV format (or convertible to CSV)
- Be relatively stable/reliable (not frequently offline)
Step-by-Step Process
1. Add Source Enum
Edit builder/src/types/config.ts:
export enum SourceList {
MY_NEW_SOURCE = "MY_NEW_SOURCE",
}
Use SCREAMING_SNAKE_CASE for the enum name.
2. Add Source Configuration
Edit builder/src/config/source-list.config.ts:
export const sourceListConfig: SourceListConfigMap = {
[SourceList.MY_NEW_SOURCE]: {
shortName: "my_source",
sourceUrl: "https://example.gov/data.csv",
sourceColumnName: "source_list_my_source",
hasHeaders: true,
},
};
Notes:
shortName: lowercase, underscores, no spaces
sourceColumnName: prefix with source_list_ for consistency
sourceUrl: Can be HTTP URL or local path using path.join(__dirname, '...')
hasHeaders: Set to false if CSV has no header row
3. Create Source List Class
Create builder/src/services/source-lists/MyNewSourceList.ts:
import { DataFrame } from "dataframe-js";
import { sourceListConfig } from "../../config/source-list.config";
import { SourceList } from "../../types/config";
export class MyNewSourceList {
static async loadData(): Promise<DataFrame> {
const config = sourceListConfig[SourceList.MY_NEW_SOURCE];
const df = await DataFrame.fromCSV(config.sourceUrl);
let processedDf = df.select("domain").rename("domain", "initial_url");
processedDf = processedDf.withColumn(
config.sourceColumnName,
() => "TRUE"
);
processedDf = processedDf.withColumn("initial_url", (row) => {
let url = row.get("initial_url");
url = url.replace(/^https?:\/\//, "");
url = url.replace(/^www\./, "");
url = url.split("/")[0];
return url;
});
const snapshotPath = path.join(
__dirname,
`../../../data/source-list-snapshots/${config.shortName}.csv`
);
await processedDf.toCSV(true, snapshotPath);
return processedDf;
}
}
Key patterns:
- Must have
static async loadData() method returning Promise<DataFrame>
- Must create
initial_url column (normalized URL)
- Must add source tracking column (e.g.,
source_list_my_source)
- Must write snapshot to
data/source-list-snapshots/
- URL cleaning should match existing patterns
4. Import and Register
Edit builder/src/main.ts:
Add import at top:
import { MyNewSourceList } from './services/source-lists/MyNewSourceList';
Add to fetchAllSourceListData() function:
async function fetchAllSourceListData(): Promise<DataFrame[]> {
return Promise.all([
MyNewSourceList.loadData(),
]);
}
Add column default in setSourceListColumnDefaults():
function setSourceListColumnDefaults(allSites: DataFrame) {
return allSites.replace("", "FALSE", [
sourceListConfig[SourceList.MY_NEW_SOURCE].sourceColumnName,
]);
}
5. Test the Integration
Run the builder:
cd builder
bun src/main.ts
Check outputs:
-
Source snapshot: data/source-list-snapshots/my_source.csv
- Verify it has
initial_url and source_list_my_source columns
- Check URLs are properly normalized
-
After union: data/process-snapshots/after-union.csv
- Verify your URLs appear in combined list
- Check your source column exists
-
Final output: data/site-scanning-target-url-list.csv
- Find entries where
source_list_my_source = TRUE
- Verify they made it through filters
-
Analysis report: data/site-scanning-target-url-list-analysis.csv
- Check row counts to see impact of your source
Common Issues
URLs Not Appearing in Final Output
Possible reasons:
- Filtered as non-public: Check if URLs match patterns in
criteria/ignore-list-*.csv
- Not federal domains: URLs must have base domain in federal .gov registry
- Marked as dead: Check if URLs are in
criteria/suspected-dead-sites.csv
- Duplicate URLs: May be deduplicated if already in another source
Check data/process-snapshots/after-dead-sites-filter-removed.csv to see what was removed.
Source Fetch Fails
- Verify URL is accessible:
curl <url>
- Check CSV format is valid
- For local files, verify path is correct
- Add error handling if source is unreliable
Column Name Conflicts
- Ensure
sourceColumnName is unique
- Follow naming convention:
source_list_<name>
- Check no other source uses same name
Source List Patterns
Remote CSV
sourceUrl: "https://example.gov/data.csv"
Local CSV
sourceUrl: path.join(__dirname, "../../../data/source-lists/my-data.csv")
CSV Without Headers
hasHeaders: false
Then in class, manually add column names:
let df = await DataFrame.fromCSV(config.sourceUrl, false);
df = df.rename("0", "initial_url");
Testing Best Practices
- Start with small source: Test with 10-100 URLs first
- Review snapshots: Check each pipeline step
- Compare before/after: Run analysis before and after adding source
- Check for duplicates: See how many URLs are new vs. existing
- Validate federal domains: Ensure URLs have valid federal base domains
Documentation
After adding a source, update:
README.md - Add to list of datasets
data/source-descriptions/ - Create markdown file describing the source
- Commit message should note source addition and expected impact