Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a51b0f7
feat(signal-score): project setup
divideby0 Feb 22, 2026
5cda707
chore(infra): add .env.example
divideby0 Feb 22, 2026
b6f21cd
docs(infra): align env examples with README
divideby0 Feb 22, 2026
94e0072
feat(signal-score): ruby scoring pipeline
divideby0 Feb 22, 2026
b1811b5
feat(signal-score): add analysis notebooks and pre-scorer features
divideby0 Feb 22, 2026
6982afc
feat(signal-score): add config, cache, prompt builder, and specs
divideby0 Feb 22, 2026
7ca7c46
feat(signal-score): enrich categories with names and descriptions
divideby0 Feb 22, 2026
e3af86a
refactor(signal-score): wire score_grants to PromptBuilder and cache
divideby0 Feb 22, 2026
61475ec
feat(signal-score): add config model, migration, and scorer class
divideby0 Feb 22, 2026
f595d74
docs: add Signal Score UI prototype screenshots
divideby0 Feb 24, 2026
7088d6f
feat(signal-score): live scoring pipeline and UI
divideby0 Feb 24, 2026
7e5c9a9
fix(signal-score): match score filter dropdown to chapter dropdown style
divideby0 Feb 24, 2026
1cd1a84
docs: replace screenshots with synthetic-only data
divideby0 Feb 24, 2026
bbc4a16
fix(signal-score): align filter dropdown with checkboxes, add sort di…
divideby0 Feb 24, 2026
1c54e02
fix(signal-score): align all filter controls to same row
divideby0 Feb 24, 2026
56d799a
fix(signal-score): rename Trust Equation breakdown to Scoring factors
divideby0 Feb 24, 2026
c100646
feat(signal-score): hide scores by default, add sort modes
divideby0 Feb 24, 2026
a88e3ac
fix(signal-score): clean up sort labels, default to newest first
divideby0 Feb 24, 2026
5acd98e
fix(signal-score): client-side score toggle, simplify sort labels
divideby0 Feb 24, 2026
e518da3
docs: update screenshots, README setup guide, fix detail view badge
divideby0 Feb 24, 2026
e94983f
docs: cropped screenshots with scoring factors expanded
divideby0 Feb 24, 2026
347cf0d
fix(signal-score): address all code review findings
divideby0 Feb 24, 2026
d8720b7
test: add signal score specs + fix retry bug in ScoreGrantJob
divideby0 Feb 24, 2026
40777a5
fix: CI migration + preserve filter/sort/score state across interactions
divideby0 Feb 24, 2026
662ce37
fix: default score_min to 0 (show all) instead of 0.15
divideby0 Feb 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Environment variables for Awesomebits
# Copy to .env for local development (gitignored)
#
# The README documents .env as the standard location.
# For extra safety, dotenv-rails also loads .env.local
# (which takes precedence) — use that for personal API keys.

# AWS (S3 uploads)
# AWS_ACCESS_KEY_ID=XXX
# AWS_SECRET_ACCESS_KEY=YYY
# AWS_BUCKET=your-bucket-name

# Database (defaults work for local dev)
# DB_NAME=awesomefoundation_development
# POSTGRES_HOST=localhost
# POSTGRES_USER=
# POSTGRES_PASSWORD=

# Spam detection
# SPAM_REGEXP=
9 changes: 9 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Personal API keys — takes precedence over .env
# Copy to .env.local and fill in your keys (gitignored)
#
# dotenv-rails loads .env.local after .env, so values here
# override anything in .env. Use this for secrets you don't
# want to accidentally commit.

# Signal Score (LLM scoring via Anthropic Batch API)
# ANTHROPIC_API_KEY=sk-ant-...
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
.ruby-gemset
.bundle
.env
.env.local
.envrc
.scratch/
context/memory/.basic-memory/
db/*.sqlite3
log/*.log
tmp/
Expand Down
113 changes: 113 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# CLAUDE.md — Instructions for Claude Code Sessions

This file is read automatically at the start of every Claude Code session in this workspace.

## Project

**Awesomebits** — the web application for the Awesome Foundation, a global micro-granting community that gives $1,000 grants to "awesome" projects. This is a fork (`divideby0/awesomebits`) of the upstream repo (`awesomefoundation/awesomebits`).

We're building **Signal Score** — an AI-assisted application pre-screening system that helps trustees prioritize grant applications by surfacing quality signals. See GitHub issue #590.

## Project Structure

```
.
├── app/ # Rails application
│ ├── extras/ # SpamChecker, SpamClassifier
│ ├── models/ # ActiveRecord models (Project, Chapter, etc.)
│ └── ...
├── context/
│ └── memory/ # basic-memory knowledge base for Signal Score
├── scripts/
│ └── signal-score/ # Signal Score tooling (Ruby)
├── .scratch/ # Local data, never committed
│ └── data/ # Parquet, CSV, DuckDB files
├── CLAUDE.md # This file
└── ...
```

## Tech Stack

- **Ruby on Rails** — existing app (Ruby 3.3.6, Rails 7.2.3)
- **PostgreSQL** — production database
- **Ruby** — Signal Score scripts (same toolchain, future Rails integration)
- **DuckDB** — local analytical queries on historical grant data

## Secrets & Configuration

- **`.env.local`** — personal API keys and secrets (gitignored). Never use `.env` for secrets.
- `dotenv-rails` is in the Gemfile and loads `.env.local` automatically.
- Required keys:
```
ANTHROPIC_API_KEY=sk-ant-...
```

## Git & Commits

### Commit Lint
- **Header max 50 characters** — this is strict, plan for it
- **Body lines max 72 characters**
- **Format:** `type(scope): description`
- **Types:** feat, fix, refactor, chore, docs, test, perf, ci
- **Valid scopes:** signal-score, spam, data, scripts, context, infra, docs, deps, repo
- **References required:** Include `Refs: #591` or `Closes: #N` in the body
- **Subject must be lowercase** — no sentence-case, start-case, pascal-case, or upper-case

### Branch Convention
- Feature branches: `feat/<issue-number>-<name>` (e.g. `feat/0591-signal-score-scripts`)
- Always branch from `master` (upstream default)

### Git Remotes
- `origin` → `awesomefoundation/awesomebits` (upstream, read-only for us)
- `fork` → `divideby0/awesomebits` (our fork, push here)
- Push to `fork`, PR against `origin`

## Data

### Historical Grant Data
Historical application data is exported from the Awesome Foundation production database and stored in a privileged Google Drive folder. Contact @divideby0 for access.

### Local Data Setup
Data files live in `.scratch/data/` (gitignored). Expected files:
- `projects.csv` → `projects.parquet` → loaded into `awesomebits.duckdb`
- `chapters.csv` → `chapters.parquet` → loaded into `awesomebits.duckdb`
- Additional: `comments.csv`, sample sets, validation scores

### DuckDB
```bash
duckdb .scratch/data/awesomebits.duckdb
```

Use for analytical queries on historical data. Never connect to production.

## Signal Score Architecture

### Scoring Pipeline
1. Application text → LLM batch API (Anthropic)
2. Structured JSON output with Trust Equation dimensions
3. Composite score 0.0–1.0 + feature breakdown + flags

### Trust Equation
`T = (Credibility + Reliability + Intimacy) / (1 + Self-Interest)`

### Key Files
- `scripts/signal-score/score_grants.rb` — batch scoring via Anthropic API
- `scripts/signal-score/import_data.rb` — CSV → Parquet → DuckDB
- `context/memory/` — research notes, analysis, pattern discovery

## Context: Existing Spam Detection

Two existing systems (bot detection only, not content analysis):
- `app/extras/spam_checker.rb` — blocklist + identical fields
- `app/extras/spam_classifier.rb` — behavioral JS metadata analysis

Signal Score is complementary — it analyzes content, not form behavior.

## Notifying Evie

When you finish a task, get stuck, or need feedback:
```bash
openclaw agent --agent main --message "[CC: <brief task name>] <your message>" --timeout 30
```

Keep messages concise — they'll be read on a phone.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ group :test do
gem 'shoulda-matchers', '~> 3'
gem 'timecop'
gem 'turnip'
gem 'webmock'
end

group :staging, :production do
Expand Down
9 changes: 9 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ GEM
concurrent-ruby (1.3.4)
connection_pool (2.5.0)
content_disposition (1.0.0)
crack (1.0.1)
bigdecimal
rexml
crass (1.0.6)
csv (3.3.3)
cucumber-gherkin (28.0.0)
Expand Down Expand Up @@ -200,6 +203,7 @@ GEM
activerecord (>= 4.0.0)
globalid (1.2.1)
activesupport (>= 6.1)
hashdiff (1.2.1)
high_voltage (4.0.0)
honeypot-captcha (1.0.1)
htmlentities (4.3.4)
Expand Down Expand Up @@ -475,6 +479,10 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.26.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1)
websocket (1.2.11)
websocket-driver (0.8.0)
Expand Down Expand Up @@ -560,6 +568,7 @@ DEPENDENCIES
tus-server (~> 2.3)
uppy-s3_multipart (~> 1.2)
web-console
webmock
will_paginate (~> 3.3.1)
xmlrpc

Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,44 @@ AWS_SECRET_ACCESS_KEY=YYY
AWS_BUCKET=your-bucket-name
```

### Signal Score (AI-Assisted Pre-Screening)

Signal Score uses the Anthropic API to pre-screen grant applications, helping trustees
prioritize their review. Scores are advisory only and never auto-hide applications.

**Setup:**

1. Get an Anthropic API key from [console.anthropic.com](https://console.anthropic.com)
2. Add it to `.env.local` (create this file if it doesn't exist — it's gitignored):

```shell
ANTHROPIC_API_KEY=sk-ant-...
```

3. Enable scoring for a chapter by inserting a config row:

```sql
INSERT INTO signal_score_configs (chapter_id, scoring_config, prompt_overrides, category_config, enabled, created_at, updated_at)
VALUES (NULL, '{"model": "claude-haiku-4-5-20251001"}'::jsonb, '{}'::jsonb, '{}'::jsonb, true, NOW(), NOW());
```

Set `chapter_id` to NULL for a global default, or a specific chapter ID for per-chapter config.

**How it works:**
- New applications are scored asynchronously via `ScoreGrantJob` (SuckerPunch)
- Each score costs ~$0.01 via the Anthropic Haiku model (~2 seconds per app)
- Results are stored in `projects.metadata['signal_score']` as JSONB
- Trustees can filter by score threshold and sort by score on the dashboard
- Score badges are hidden by default — trustees opt in via "Show scores" checkbox
- The scoring rubric uses a Trust Equation framework: `T = (C + R + I) / (1 + S)`

**Dashboard controls:**
- **Show scores** — toggle score badges on individual applications
- **Sort** — Latest, Earliest, Score highest, Score lowest, Random (stable hash)
- **Filter** — All apps, Hide low signal (default), Borderline+, Solid+, Strong only

**Cost at scale:** ~$0.01/app. A chapter receiving 50 apps/month = ~$0.50/month.

Subdomains
----------

Expand Down
46 changes: 46 additions & 0 deletions app/assets/javascripts/application-score-filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* Score filter dropdowns — each .score-filter-wrapper is independent */

$('a.score-selection').on('click', function(event) {
event.preventDefault();
const $wrapper = $(this).closest('.score-filter-wrapper');
const $selector = $wrapper.find('.score-selector');
const isOpen = $selector.hasClass('expanded');

// Close all dropdowns first
$('.score-selector').removeClass('expanded');
$('a.score-selection').removeClass('expanded');

if (!isOpen) {
$selector.addClass('expanded');
$(this).addClass('expanded');
}
});

$(document).click(function(e) {
if (!$(e.target).closest('.score-filter-wrapper').length) {
$('.score-selector').removeClass('expanded');
$('a.score-selection').removeClass('expanded');
}
});

/* Show/hide score badges — client-side toggle with localStorage persistence */
(function() {
var STORAGE_KEY = 'awesomebits-show-scores';
var $checkbox = $('#show-scores');

// Restore saved state on page load
if (localStorage.getItem(STORAGE_KEY) === 'true') {
$checkbox.prop('checked', true);
$('.signal-score').show();
}

$checkbox.on('change', function() {
var checked = $(this).is(':checked');
localStorage.setItem(STORAGE_KEY, checked);
if (checked) {
$('.signal-score').show();
} else {
$('.signal-score').hide();
}
});
})();
Loading