Add social cost proxies to impact tracking hooks

Extend pre-compact-snapshot.sh to extract 5 new per-conversation
metrics from the transcript: automation ratio (deskilling proxy),
model ID (monoculture tracking), test pass/fail counts (code quality
proxy), file churn (edits per unique file), and public push detection
(data pollution risk flag). Update show-impact.sh to display them.

New plan: quantify-social-costs.md — roadmap for moving non-environmental
cost categories from qualitative to proxy-measurable.

Tasks 19-24 done. Task 25 (methodology update) pending.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude 2026-03-16 15:05:53 +00:00
parent e6e0bf4616
commit af6062c1f9
8 changed files with 554 additions and 25 deletions

View file

@ -11,6 +11,11 @@ A PreCompact hook that runs before each context compaction, capturing:
- Energy consumption estimate (Wh)
- CO2 emissions estimate (grams)
- Financial cost estimate (USD)
- Model ID
- Automation ratio (AI output vs. user input — deskilling proxy)
- File churn (edits per file — code quality proxy)
- Test pass/fail counts
- Public push detection (data pollution risk flag)
Data is logged to a JSONL file for analysis over time.
@ -63,8 +68,10 @@ consider these complementary tools:
- **[Hugging Face AI Energy Score](https://huggingface.github.io/AIEnergyScore/)** —
Benchmarks model energy efficiency. Useful for choosing between models.
These tools focus on environmental metrics only. This toolkit and the
methodology also cover financial, social, epistemic, and political costs.
These tools focus on environmental metrics only. This toolkit also
tracks financial cost and proxy metrics for social costs (automation
ratio, file churn, test outcomes, public push detection). The
accompanying methodology covers additional dimensions in depth.
## Limitations

View file

@ -35,27 +35,102 @@ if [ -f "$TRANSCRIPT_PATH" ]; then
# The transcript contains .message.usage with input_tokens,
# cache_creation_input_tokens, cache_read_input_tokens, output_tokens.
USAGE_DATA=$(python3 -c "
import json, sys
import json, sys, re
input_tokens = 0
cache_creation = 0
cache_read = 0
output_tokens = 0
turns = 0
model_id = ''
user_bytes = 0
edited_files = {} # file_path -> edit count
test_passes = 0
test_failures = 0
has_public_push = 0
with open(sys.argv[1]) as f:
for line in f:
try:
d = json.loads(line.strip())
u = d.get('message', {}).get('usage')
if u and 'input_tokens' in u:
turns += 1
input_tokens += u.get('input_tokens', 0)
cache_creation += u.get('cache_creation_input_tokens', 0)
cache_read += u.get('cache_read_input_tokens', 0)
output_tokens += u.get('output_tokens', 0)
msg = d.get('message', {})
role = msg.get('role')
content = msg.get('content', '')
# Track user message size (proxy for user contribution)
if role == 'user':
if isinstance(content, str):
user_bytes += len(content.encode('utf-8', errors='replace'))
elif isinstance(content, list):
for block in content:
if isinstance(block, dict) and block.get('type') == 'text':
user_bytes += len(block.get('text', '').encode('utf-8', errors='replace'))
# Extract usage data and model from assistant messages
if role == 'assistant':
m = msg.get('model', '')
if m:
model_id = m
u = msg.get('usage')
if u and 'input_tokens' in u:
turns += 1
input_tokens += u.get('input_tokens', 0)
cache_creation += u.get('cache_creation_input_tokens', 0)
cache_read += u.get('cache_read_input_tokens', 0)
output_tokens += u.get('output_tokens', 0)
# Parse tool use blocks
if isinstance(content, list):
for block in content:
if not isinstance(block, dict) or block.get('type') != 'tool_use':
continue
name = block.get('name', '')
inp = block.get('input', {})
# File churn: count Edit/Write per file
if name in ('Edit', 'Write'):
fp = inp.get('file_path', '')
if fp:
edited_files[fp] = edited_files.get(fp, 0) + 1
# Public push detection
if name == 'Bash':
cmd = inp.get('command', '')
if re.search(r'git\s+push', cmd):
has_public_push = 1
# Test results from tool_result blocks (user role, tool_result type)
if role == 'user' and isinstance(content, list):
for block in content:
if isinstance(block, dict) and block.get('type') == 'tool_result':
text = ''
rc = block.get('content', '')
if isinstance(rc, str):
text = rc
elif isinstance(rc, list):
text = ' '.join(b.get('text', '') for b in rc if isinstance(b, dict))
# Detect test outcomes from common test runner output
if re.search(r'(\d+)\s+(tests?\s+)?passed', text, re.I):
test_passes += 1
if re.search(r'(\d+)\s+(tests?\s+)?failed|FAIL[ED]?|ERROR', text, re.I):
test_failures += 1
except Exception:
pass
# Print as tab-separated for easy shell parsing
print(f'{turns}\t{input_tokens}\t{cache_creation}\t{cache_read}\t{output_tokens}')
user_tokens_est = user_bytes // 4 # rough byte-to-token estimate
unique_files = len(edited_files)
total_edits = sum(edited_files.values())
churn = round(total_edits / unique_files, 2) if unique_files > 0 else 0
# automation_ratio: 0 = all human, 1 = all AI (as permille for integer arithmetic)
if output_tokens + user_tokens_est > 0:
auto_ratio_pm = output_tokens * 1000 // (output_tokens + user_tokens_est)
else:
auto_ratio_pm = 0
print(f'{turns}\t{input_tokens}\t{cache_creation}\t{cache_read}\t{output_tokens}\t{model_id}\t{auto_ratio_pm}\t{user_tokens_est}\t{unique_files}\t{total_edits}\t{test_passes}\t{test_failures}\t{has_public_push}')
" "$TRANSCRIPT_PATH" 2>/dev/null || echo "")
if [ -n "$USAGE_DATA" ] && [ "$(echo "$USAGE_DATA" | cut -f1)" -gt 0 ] 2>/dev/null; then
@ -66,6 +141,14 @@ print(f'{turns}\t{input_tokens}\t{cache_creation}\t{cache_read}\t{output_tokens}
CACHE_CREATION=$(echo "$USAGE_DATA" | cut -f3)
CACHE_READ=$(echo "$USAGE_DATA" | cut -f4)
OUTPUT_TOKENS=$(echo "$USAGE_DATA" | cut -f5)
MODEL_ID=$(echo "$USAGE_DATA" | cut -f6)
AUTO_RATIO_PM=$(echo "$USAGE_DATA" | cut -f7)
USER_TOKENS_EST=$(echo "$USAGE_DATA" | cut -f8)
UNIQUE_FILES=$(echo "$USAGE_DATA" | cut -f9)
TOTAL_EDITS=$(echo "$USAGE_DATA" | cut -f10)
TEST_PASSES=$(echo "$USAGE_DATA" | cut -f11)
TEST_FAILURES=$(echo "$USAGE_DATA" | cut -f12)
HAS_PUBLIC_PUSH=$(echo "$USAGE_DATA" | cut -f13)
# Cumulative input = all tokens that went through the model.
# Cache reads are cheaper (~10-20% of full compute), so we weight them.
@ -93,6 +176,14 @@ print(f'{turns}\t{input_tokens}\t{cache_creation}\t{cache_read}\t{output_tokens}
CACHE_CREATION=0
CACHE_READ=0
INPUT_TOKENS=0
MODEL_ID=""
AUTO_RATIO_PM=0
USER_TOKENS_EST=0
UNIQUE_FILES=0
TOTAL_EDITS=0
TEST_PASSES=0
TEST_FAILURES=0
HAS_PUBLIC_PUSH=0
fi
# --- Cost estimates ---
@ -127,12 +218,20 @@ else
CO2_G=0
COST_CENTS=0
TOKEN_SOURCE="none"
MODEL_ID=""
AUTO_RATIO_PM=0
USER_TOKENS_EST=0
UNIQUE_FILES=0
TOTAL_EDITS=0
TEST_PASSES=0
TEST_FAILURES=0
HAS_PUBLIC_PUSH=0
fi
# --- Write log entry ---
cat >> "$LOG_FILE" <<EOF
{"timestamp":"$TIMESTAMP","session_id":"$SESSION_ID","trigger":"$TRIGGER","token_source":"$TOKEN_SOURCE","transcript_bytes":$TRANSCRIPT_BYTES,"transcript_lines":$TRANSCRIPT_LINES,"assistant_turns":$ASSISTANT_TURNS,"tool_uses":$TOOL_USES,"cumulative_input_tokens":$CUMULATIVE_INPUT,"cumulative_input_raw":$CUMULATIVE_INPUT_RAW,"cache_creation_tokens":$CACHE_CREATION,"cache_read_tokens":$CACHE_READ,"output_tokens":$OUTPUT_TOKENS,"energy_wh":$ENERGY_WH,"co2_g":$CO2_G,"cost_cents":$COST_CENTS}
{"timestamp":"$TIMESTAMP","session_id":"$SESSION_ID","trigger":"$TRIGGER","token_source":"$TOKEN_SOURCE","transcript_bytes":$TRANSCRIPT_BYTES,"transcript_lines":$TRANSCRIPT_LINES,"assistant_turns":$ASSISTANT_TURNS,"tool_uses":$TOOL_USES,"cumulative_input_tokens":$CUMULATIVE_INPUT,"cumulative_input_raw":$CUMULATIVE_INPUT_RAW,"cache_creation_tokens":$CACHE_CREATION,"cache_read_tokens":$CACHE_READ,"output_tokens":$OUTPUT_TOKENS,"energy_wh":$ENERGY_WH,"co2_g":$CO2_G,"cost_cents":$COST_CENTS,"model_id":"$MODEL_ID","automation_ratio_pm":$AUTO_RATIO_PM,"user_tokens_est":$USER_TOKENS_EST,"unique_files_edited":$UNIQUE_FILES,"total_file_edits":$TOTAL_EDITS,"test_passes":$TEST_PASSES,"test_failures":$TEST_FAILURES,"has_public_push":$HAS_PUBLIC_PUSH}
EOF
exit 0

View file

@ -49,6 +49,35 @@ while IFS= read -r line; do
printf " Cache: %s created, %s read\n" "$cache_create" "$cache_read"
fi
LC_NUMERIC=C printf " Energy: ~%s Wh CO2: ~%sg Cost: ~\$%.2f\n" "$energy" "$co2" "$(echo "$cost / 100" | bc -l 2>/dev/null || echo "$cost cents")"
# Social cost proxies (if present in log entry)
model=$(echo "$line" | jq -r '.model_id // empty')
auto_pm=$(echo "$line" | jq -r '.automation_ratio_pm // empty')
user_tok=$(echo "$line" | jq -r '.user_tokens_est // empty')
files_ed=$(echo "$line" | jq -r '.unique_files_edited // empty')
total_ed=$(echo "$line" | jq -r '.total_file_edits // empty')
t_pass=$(echo "$line" | jq -r '.test_passes // empty')
t_fail=$(echo "$line" | jq -r '.test_failures // empty')
pub_push=$(echo "$line" | jq -r '.has_public_push // empty')
if [ -n "$model" ]; then
printf " Model: %s\n" "$model"
fi
if [ -n "$auto_pm" ] && [ "$auto_pm" != "0" ]; then
auto_pct=$(( auto_pm / 10 ))
auto_dec=$(( auto_pm % 10 ))
printf " Automation ratio: %d.%d%% (user ~%s tokens, AI ~%s tokens)\n" \
"$auto_pct" "$auto_dec" "$user_tok" "$output"
fi
if [ -n "$files_ed" ] && [ "$files_ed" != "0" ]; then
printf " File churn: %s edits across %s files\n" "$total_ed" "$files_ed"
fi
if [ -n "$t_pass" ] && [ -n "$t_fail" ] && { [ "$t_pass" != "0" ] || [ "$t_fail" != "0" ]; }; then
printf " Tests: %s passed, %s failed\n" "$t_pass" "$t_fail"
fi
if [ "$pub_push" = "1" ]; then
printf " Public push: yes (data pollution risk)\n"
fi
echo ""
done < "$LOG_FILE"
@ -62,3 +91,26 @@ echo "=== Totals ($TOTAL_ENTRIES snapshots) ==="
LC_NUMERIC=C printf " Energy: ~%s Wh CO2: ~%sg Cost: ~\$%.2f\n" \
"$TOTAL_ENERGY" "$TOTAL_CO2" \
"$(echo "$TOTAL_COST / 100" | bc -l 2>/dev/null || echo "$TOTAL_COST cents")"
# Show annotations if they exist
ANNOT_FILE="$PROJECT_DIR/.claude/impact/annotations.jsonl"
if [ -f "$ANNOT_FILE" ] && [ -s "$ANNOT_FILE" ]; then
echo ""
echo "=== Value Annotations ==="
echo ""
while IFS= read -r line; do
sid=$(echo "$line" | jq -r '.session_id')
if ! echo "$sid" | grep -q "$FILTER"; then
continue
fi
ts=$(echo "$line" | jq -r '.timestamp')
summary=$(echo "$line" | jq -r '.value_summary')
reach=$(echo "$line" | jq -r '.estimated_reach')
cf=$(echo "$line" | jq -r '.counterfactual')
net=$(echo "$line" | jq -r '.net_assessment')
printf "%s session=%s\n" "$ts" "${sid:0:12}..."
printf " Value: %s\n" "$summary"
printf " Reach: %s Counterfactual: %s Net: %s\n" "$reach" "$cf" "$net"
echo ""
done < "$ANNOT_FILE"
fi