# Project export: The First Match

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: The only foster matching system that allows children to speak for themselves.
- Devpost: https://devpost.com/software/the-first-match
- GitHub: https://github.com/ramlukn/treehacks
- Team: 2 GitHub contributor(s) — ramlukn (3 commits), ramlukn (2 commits)

## Devpost submission (written by the team)

### Inspiration

Every year, over 400,000 children sit in the U.S. foster care system — and nearly 50% of placements disrupt. Caseworkers juggling 20+ cases rely on gut instinct and spreadsheets to make life-altering matches. Nikhil has extensive experience in developmental therapy through his startup SportsMind, working intensively with high-performing athletes on psychological resilience. Julio has worked in Ed and now EdTech, specifically with students from marginalized contexts. In a conversation about one of Julio's previous students, the foster care system came up — how flawed the matching process is. Julio saw firsthand the severity that a familial mismatch could lead to: behavioral regression, broken trust, and a child who becomes harder to place with every failed attempt. They decided to build the solution themselves. What It Does Sunny, the AI Counselor — Children talk to a warm, live video avatar (HeyGen + Claude) instead of filling out clinical forms. Sunny adapts to the child's age, extracts 30+ psychosocial dimensions and 6 safety flags from a natural 5-minute conversation. No clipboards. Family Profiling — Families complete guided onboarding capturing parenting style, emotional capacity, trauma training, cultural practices, and environment details. Caseworker Note Intelligence — Caseworkers paste free-text notes; Claude extracts structured clinical metrics and cross-references them with the AI counselor's findings. 7-Layer Matching — Every child-family pair runs through a 1,142-line matching engine: $$\text{MatchScore} = \sum_{i=1}^{7} w_i \cdot L_i(\text{child}, \text{family}, \text{context})$$ L1: Profile Normalization L2: Hard Constraints L3: 7 Weighted Dimensions (Personality, Emotional, Attachment, Cultural, Communication, Environment, Stability) L4: Cross-Dimensional Synergy L5: Contextual Reasoning (dual-source: AI transcripts + caseworker notes) L6: Bias Audit & Fairness L7: Stability Prediction & Risk Classification Vector pre-filtering uses pgvector with custom 25-dimensional embeddings (cosine similarity) to narrow candidates before full reasoning. How We Built It Frontend: Flutter web, Provider state management, 10 screens AI Counselor: Claude Sonnet 4.5 (7 conversation phases) + HeyGen Streaming Avatar (LiveKit/WebRTC) + speech-to-text/TTS with auto-listen Backend: Supabase — PostgreSQL + pgvector, 6 tables, 3 Edge Functions (Deno) Matching: 7-layer engine running server-side via Edge Function, local Dart fallback Embeddings: 25-dim vectors encoding energy, attachment, trust, culture, environment, and 20 other factors Challenges We Ran Into Ordering problem: Transcripts saved before the child existed in DB, violating FK constraints. Had to restructure the entire UUID/save flow. Client vs. server matching: Built the engine client-side first, then migrated to Edge Functions for database access while keeping local fallback. Avatar + speech coordination: HeyGen video, STT, and TTS had to not step on each other — solved with state guards and auto-listen callbacks. Trauma-informed prompting: 300+ line system prompt to make Claude genuinely warm with a 5-year-old while extracting clinical-grade metrics. Algorithmic bias: Built Layer 6 specifically to correct for systematic biases against older children, cross-cultural matches, and special needs. Accomplishments We're Proud Of Sunny feels real — kids don't know they're being assessed Every match has a full transparent audit trail — no black boxes Dual-source validation between AI counselor and caseworker notes Custom 25-dimensional embedding space purpose-built for adoption matching 6 real-time safety flags monitored during every conversation What We Learned The foster care matching problem isn't lack of caring — it's lack of tools Children reveal far more in natural conversation than in checkbox forms Bias correction is core infrastructure, not a nice-to-have — it's literally Layer 6 LLMs can be trauma-informed with the right prompting architecture What's Next for The First Match Longitudinal outcome tracking to improve matching with real-world feedback Multi-language support for Sunny Agency partnerships to pilot against expert caseworker decisions Mobile app for tablets in caseworker offices and group homes Sibling group optimization — keeping siblings together

## README (from the GitHub repository)

# treehacks

A new Flutter project.

## Getting Started

This project is a starting point for a Flutter application.

A few resources to get you started if this is your first Flutter project:

- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)

For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.


## Detected evidence (automated analysis)

Indexed codebase: 78 recognized source files, 525 KB.
- C (language) — detected in the code
- C++ (language) — detected in the code
- Dart (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Kotlin (language) — detected in the code
- Swift (language) — detected in the code
- TypeScript (language) — detected in the code
- Supabase (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (120 of 137)

```
.gitignore
.metadata
analysis_options.yaml
android/.gitignore
android/app/build.gradle
android/app/src/debug/AndroidManifest.xml
android/app/src/main/AndroidManifest.xml
android/app/src/main/kotlin/com/example/treehacks/MainActivity.kt
android/app/src/main/res/drawable-v21/launch_background.xml
android/app/src/main/res/drawable/launch_background.xml
android/app/src/main/res/values-night/styles.xml
android/app/src/main/res/values/styles.xml
android/app/src/profile/AndroidManifest.xml
android/build.gradle
android/gradle.properties
android/gradle/wrapper/gradle-wrapper.properties
android/settings.gradle
DEVPOST.md
ios/.gitignore
ios/Flutter/AppFrameworkInfo.plist
ios/Flutter/Debug.xcconfig
ios/Flutter/Release.xcconfig
ios/Podfile
ios/Runner.xcodeproj/project.pbxproj
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
ios/Runner.xcworkspace/contents.xcworkspacedata
ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
ios/Runner/AppDelegate.swift
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
ios/Runner/Base.lproj/LaunchScreen.storyboard
ios/Runner/Base.lproj/Main.storyboard
ios/Runner/Info.plist
ios/Runner/Runner-Bridging-Header.h
ios/RunnerTests/RunnerTests.swift
lib/config/secrets.example.dart
lib/main.dart
lib/models/caseworker_notes.dart
lib/models/child_profile.dart
lib/models/conversation_transcript.dart
lib/models/family_profile.dart
lib/models/match_result.dart
lib/screens/caseworker_notes_screen.dart
lib/screens/child_onboarding_screen.dart
lib/screens/children_screen.dart
lib/screens/counselor_chat_screen.dart
lib/screens/dashboard_screen.dart
lib/screens/families_screen.dart
lib/screens/family_onboarding_screen.dart
lib/screens/home_screen.dart
lib/screens/match_detail_screen.dart
lib/screens/matches_screen.dart
lib/services/ai_counselor_service.dart
lib/services/app_state.dart
lib/services/caseworker_notes_service.dart
lib/services/did_service.dart
lib/services/elevenlabs_service.dart
lib/services/embedding_service.dart
lib/services/heygen_service.dart
lib/services/matching_engine.dart
lib/services/mock_data_service.dart
lib/services/speech_service.dart
lib/services/supabase_service.dart
lib/services/virtual_counselor.dart
lib/utils/theme.dart
lib/widgets/did_avatar_widget.dart
lib/widgets/heygen_avatar_widget.dart
lib/widgets/profile_avatar.dart
lib/widgets/score_indicator.dart
lib/widgets/section_card.dart
lib/widgets/sunny_avatar.dart
linux/.gitignore
linux/CMakeLists.txt
linux/flutter/CMakeLists.txt
linux/flutter/generated_plugin_registrant.cc
linux/flutter/generated_plugin_registrant.h
linux/flutter/generated_plugins.cmake
linux/runner/CMakeLists.txt
linux/runner/main.cc
linux/runner/my_application.cc
linux/runner/my_application.h
macos/.gitignore
macos/Flutter/Flutter-Debug.xcconfig
macos/Flutter/Flutter-Release.xcconfig
macos/Flutter/GeneratedPluginRegistrant.swift
macos/Podfile
macos/Runner.xcodeproj/project.pbxproj
macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
macos/Runner.xcworkspace/contents.xcworkspacedata
macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
macos/Runner/AppDelegate.swift
macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
macos/Runner/Base.lproj/MainMenu.xib
macos/Runner/Configs/AppInfo.xcconfig
macos/Runner/Configs/Debug.xcconfig
macos/Runner/Configs/Release.xcconfig
macos/Runner/Configs/Warnings.xcconfig
macos/Runner/DebugProfile.entitlements
macos/Runner/Info.plist
macos/Runner/MainFlutterWindow.swift
macos/Runner/Release.entitlements
macos/RunnerTests/RunnerTests.swift
pubspec.lock
pubspec.yaml
README.md
run_dev.sh
supabase/config.toml
supabase/functions/compute-matches/index.ts
supabase/functions/extract-caseworker-metrics/index.ts
supabase/functions/generate-embedding/index.ts
web/did_webrtc_bridge.js
web/heygen_livekit_bridge.js
web/index.html
web/manifest.json
[17 more files omitted for size]
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Add files via upload
- Merge remote-tracking branch 'origin/master'
- "devpost"
- Delete lib/config/secrets.dart
- "v1"

## Key source files (fetched from GitHub, selected and truncated for size)

### DEVPOST.md

```markdown
# The First Match

## Inspiration

Every year, over 400,000 children sit in the U.S. foster care system — and nearly **50% of placements disrupt**. Caseworkers juggling 20+ cases rely on gut instinct and spreadsheets to make life-altering matches.

Niki has extensive experience in developmental therapy through his startup SportsMind, working intensively with high-performing athletes on psychological resilience. Julio has worked in Ed and now EdTech, specifically with students from marginalized contexts. In a conversation about one of Julio's previous students, the foster care system came up — how flawed the matching process is. Julio saw firsthand the severity that a familial mismatch could lead to: behavioral regression, broken trust, and a child who becomes harder to place with every failed attempt. They decided to build the solution themselves.

## What It Does

**Sunny, the AI Counselor** — Children talk to a warm, live video avatar (HeyGen + Claude) instead of filling out clinical forms. Sunny adapts to the child's age, extracts 30+ psychosocial dimensions and 6 safety flags from a natural 5-minute conversation. No clipboards.

**Family Profiling** — Families complete guided onboarding capturing parenting style, emotional capacity, trauma training, cultural practices, and environment details.

**Caseworker Note Intelligence** — Caseworkers paste free-text notes; Claude extracts structured clinical metrics and cross-references them with the AI counselor's findings.

**7-Layer Matching** — Every child-family pair runs through a 1,142-line matching engine:

$$\text{MatchScore} = \sum_{i=1}^{7} w_i \cdot L_i(\text{child}, \text{family}, \text{context})$$

L1: Profile Normalization | L2: Hard Constraints | L3: 7 Weighted Dimensions (Personality, Emotional, Attachment, Cultural, Communication, Environment, Stability) | L4: Cross-Dimensional Synergy | L5: Contextual Reasoning (dual-source: AI transcripts + caseworker notes) | L6: Bias Audit & Fairness | L7: Stability Prediction & Risk Classification

Vector pre-filtering uses **pgvector** with custom 25-dimensional embeddings (cosine similarity) to narrow candidates before full reasoning.

## How We Built It

- **Frontend**: Flutter web, Provider state management, 10 screens
- **AI Counselor**: Claude Sonnet 4.5 (7 conversation phases) + HeyGen Streaming Avatar (LiveKit/WebRTC) + speech-to-text/TTS with auto-listen
- **Backend**: Supabase — PostgreSQL + pgvector, 6 tables, 3 Edge Functions (Deno)
- **Matching**: 7-layer engine running server-side via Edge Function, local Dart fallback
- **Embeddings**: 25-dim vectors encoding energy, attachment, trust, culture, environment, and 20 other factors

## Challenges We Ran Into

- **Ordering problem**: Transcripts saved before the child existed in DB, violating FK constraints. Had to restructure the entire UUID/save flow.
- **Client vs. server matching**: Built the engine client-side first, then migrated to Edge Functions for database access while keeping
[truncated — 1491 more characters]
```

### supabase/functions/generate-embedding/index.ts

```typescript
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const SUPABASE_SERVICE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;

// ---------- 25-Dimension Embedding Helpers ----------

function attachmentToNumeric(style: string): number {
  const map: Record<string, number> = {
    secure: 0.9,
    anxious: 0.5,
    avoidant: 0.3,
    disorganized: 0.1,
  };
  return map[style?.toLowerCase()] ?? 0.5;
}

function approachToNumeric(approach: string): number {
  const map: Record<string, number> = {
    nurturing: 0.8,
    "structured-nurturing": 0.6,
    therapeutic: 0.4,
  };
  return map[approach?.toLowerCase()] ?? 0.5;
}

function commStyleToWarmth(style: string): number {
  const map: Record<string, number> = {
    warm: 0.9,
    open: 0.8,
    direct: 0.5,
    structured: 0.4,
    verbal: 0.7,
    "non-verbal": 0.3,
    mixed: 0.5,
  };
  return map[style?.toLowerCase()] ?? 0.5;
}

function envToOneHot(env: string): [number, number, number] {
  switch (env?.toLowerCase()) {
    case "urban":
      return [1, 0, 0];
    case "suburban":
      return [0, 1, 0];
    case "rural":
      return [0, 0, 1];
    default:
      return [0.33, 0.34, 0.33];
  }
}

function hasKeyword(items: string[], keywords: string[]): number {
  if (!items?.length) return 0;
  const lower = items.map((i) => i.toLowerCase());
  return lower.some((i) => keywords.some((k) => i.includes(k))) ? 1 : 0;
}

// ---------- Vector Generators ----------

function childToVector(c: Record<string, any>): number[] {
  const v = new Array(25).fill(0);
  v[0] = c.energy_level ?? 0.5;
  v[1] = (c.social_comfort ?? 0.5) * 0.5 + commStyleToWarmth(c.communication_style) * 0.5;
  v[2] = c.structure_need ?? 0.5;
  v[3] =
    (c.emotional_awareness ?? 0.5) * 0.4 +
    (1 - (c.trauma_severity ?? 0)) * 0.3 +
    (c.trust_level ?? 0.5) * 0.3;
  v[4] = attachmentToNumeric(c.attachment_style);
  v[5] = 1 - (c.trauma_severity ?? 0);
  v[6] = c.trust_level ?? 0.5;
  const env = envToOneHot(c.environment_preference);
  v[7] = env[0];
  v[8] = env[1];
  v[9] = env[2];
  v[10] = c.comfortable_with_pets ? 1 : 0;
  v[11] = c.needs_sibling_placement ? 1 : 0;
  const langs: string[] = [c.primary_language, ...(c.additional_languages ?? [])].filter(Boolean);
  v[12] = langs.length > 1 ? 0.8 : 0.5;
  v[13] = c.cultural_background ? 0.7 : 0.3;
  v[14] = (c.cultural_practices?.length ?? 0) > 0 ? 0.7 : 0.3;
  v[15] = c.adaptability ?? 0.5;
  v[16] = 0.5;
  v[17] = 0.5;
  v[18] = c.therapy_ongoing ? 0.7 : 0.3;
  v[19] = c.verbal_ability ?? 0.5;
  v[20] = c.expressiveness ?? 0.5;
  v[21] = (c.special_needs?.length ?? 0) > 0 ? 0.8 : 0.2;
  v[22] = (c.interests?.length ?? 0) / 10;
  v[23] = (c.coping_strategies?.length ?? 0) / 5;
  v[24] = c.adaptability ?? 0.5;
  return v;
}

function familyToVector(f: Record<string, any>): number[] {
  const v = new Array(25).fill(0);
  let familyEnergy = 0.5;
  const activities: string[] = f.family_activities ?? [];
  if (hasKeyword(activities, ["sport", "outdoor", "hiking"])) familyEnergy += 0.2;
  if ((f.structure_level ?? 0.5) > 0.7) familyEnergy -= 0.1;
  v[0] = Math.max(0, Math.min(1, familyEnergy));
  v[1] =
    (f.emotional_availability ?? 0.5) * 0.4 + commStyleToWarmth(f.communication_style) * 0.6;
  v[2] = f.structure_level ?? 0.5;
  v[3] =
    (f.emotional_regulation_capacity ?? 0.5) * 0.4 +
    (f.patience_level ?? 0.5) * 0.3 +
    (f.resilience_level ?? 0.5) * 0.3;
  v[4] = approachToNumeric(f.attachment_approach);
  v[5] = (f.resilience_level ?? 0.5) * 0.5 + (f.has_trauma_training ? 0.5 : 0);
  v[6] = (f.patience_level ?? 0.5) * 0.6 + (f.flexibility_level ?? 0.5) * 0.4;
  const env = envToOneHot(f.home_environment);
  v[7] = env[0];
  v[8] = env[1];
  v[9] = env[2];
  v[10] = (f.pets?.length ?? 0) > 0 ? 1 : 0;
  v[11] = f.open_to_sibling_group ? 1 : 0;
  const langs: string[] = [f.primary_language, ...(f.additional_languages ?? [])].filter(Boolean);
  v[12] = langs.length > 1 ? 0.8 : 0.5;
  v[13] = f.cultural_background ? 0.7 : 0.3;
  v[14] = (f.cultural_practices?.length ?? 0) > 0 ? 0.7 : 0.3;
  v[15] = f.resilience_level ?? 0.5;
  v[16] = f.support_network_strength ?? 0.5;
  v[17] =
    (f.has_parenting_experience ? 0.3 : 0) +
    (f.has_foster_experience ? 0.3 : 0) +
    (f.has_adoption_experience ? 0.2 : 0) +
    (f.has_trauma_training ? 0.2 : 0);
  v[18] = f.available_time ?? 0.5;
  v[19] = f.conflict_resolution_skill ?? 0.5;
  v[20] = f.emotional_availability ?? 0.5;
  v[21] = f.open_to_special_needs ? 0.8 : 0.2;
  v[22] = activities.length / 10;
  v[23] = (f.completed_trainings?.length ?? 0) / 5;
  v[24] = f.flexibility_level ?? 0.5;
  return v;
}

// ---------- Main Handler ----------

serve(async (req: Request) => {
  try {
    const { type, id } = await req.json();
    if (!type || !id) {
      return new Response(JSON.stringify({ error: "type and id required" }), { status: 400 });
    }

    const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY);
    const table = type === "child" ? "children" : "families";

    const { data: profile, error: fetchErr } = await supabase
      .from(table)
      .select("*")
      .eq("id", id)
      .single();

    if (fetchErr || !profile) {
      return new Response(JSON.stringify({ error: fetchErr?.message ?? "Not found" }), {
        status: 404,
      });
    }

    const embedding = type === "child" ? childToVector(profile) : familyToVector(profile);

    const { error: updateErr } = await supabase
      .from(table)
      .update({ embedding: `[${embedding.join(",")}]` })
      .eq("id", id);

    if (updateErr) {
      return new Response(JSON.stringify({ error: updateErr.message }), { status: 500 });
    }

    return new Response(JSON.stringify({ ok: true, embedding }), {
      headers: { "Content-Type": "application/json" },
    });
  } catch (e) {
    return new Response(JSON.stringify({ error: (e as Error
[truncated — 40 more characters]
```

### supabase/functions/extract-caseworker-metrics/index.ts

```typescript
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const SUPABASE_SERVICE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const ANTHROPIC_API_KEY = Deno.env.get("ANTHROPIC_API_KEY")!;

const SYSTEM_PROMPT = `You are a structured data extraction system for a child adoption matching platform.

You will receive free-text caseworker/social worker notes about a child in the foster care or adoption system. Your job is to extract structured metrics from these notes that will be used to match the child with a compatible family.

IMPORTANT RULES:
- Only extract what the notes explicitly state or strongly imply. Do NOT guess.
- If the notes do not mention a field, omit it entirely.
- Never store identifying info (last names, addresses, school names, phone numbers).
- Use the exact field names and value formats below.

EXTRACTED DATA SCHEMA:
You MAY include any of these fields when confident:

Core fields:
- "interests": ["..."]
- "energyLevel": "high" | "moderate" | "low"
- "socialComfort": "high" | "moderate" | "low"
- "emotionalTriggers": ["..."]
- "copingStrategies": ["..."]
- "trustLevel": "low" | "low-moderate" | "moderate-high" | "high"
- "expressiveness": "high" | "moderate" | "low"
- "structureNeed": "high" | "moderate" | "low"
- "environmentPreference": "urban" | "suburban" | "rural" | "no_preference"
- "petComfort": true | false
- "siblingNeed": true | false
- "communicationStyle": "verbal" | "non-verbal" | "mixed"
- "adaptability": "high" | "moderate" | "low"

Holistic matching signals:
- "strengths": ["..."]
- "calmingSupports": ["..."]
- "comfortBoundaries": {
    "touch": "likes_hugs" | "ask_first" | "no_touch" | "depends",
    "privacy": "needs_space" | "ok_close" | "mixed"
  }
- "routinePreferences": {
    "morning": "slow" | "fast" | "mixed",
    "bedtime": "structured" | "flexible" | "mixed"
  }
- "sensoryPreferences": {
    "noise": "quiet" | "ok_noise" | "depends",
    "crowds": "avoid" | "ok" | "depends"
  }
- "relationshipPriorities": {
    "importantPeopleRoles": ["sibling", "grandparent", "mentor", "foster_parent", "friend", "teacher"],
    "contactImportance": "low" | "medium" | "high"
  }
- "conflictRepairPreference": "space_then_talk" | "talk_now" | "write_text" | "adult_apology_first" | "not_sure"
- "transitionSupports": ["preview_photos", "clear_rules", "slow_introductions", "keep_items", "goodbye_rituals"]
- "identityNeeds": ["language", "hair_skin_care", "faith", "food_traditions", "community_connection"]
- "serviceComfort": {
    "talking_to_helper": "comfortable" | "maybe" | "not_now" | "depends",
    "what_helped_before": ["..."]
  }

Needs snapshot (0-3) - ONLY if notes provide clear evidence:
- "needsScores": {
    "emotionalRegulation": 0|1|2|3,
    "traumaSymptomLoad": 0|1|2|3,
    "behaviorFunctioning": 0|1|2|3,
    "developmentalComplexity": 0|1|2|3,
    "attachmentTransitionSupport": 0|1|2|3,
    "transitionLossLoad": 0|1|2|3,
    "identityContinuityNeeds": 0|1|2|3,
    "serviceIntensityNeeds": 0|1|2|3
  }
Scoring: 0 = no signal; 1 = mild; 2 = moderate; 3 = intense/safety-relevant.

Safety flags (TRUE ONLY if notes explicitly mention):
- "safetyFlags": {
    "currentDanger": true|false,
    "abuseDisclosure": true|false,
    "selfHarmIdeation": true|false,
    "harmToOthersIdeation": true|false,
    "runningAwayRisk": true|false,
    "exploitationRisk": true|false
  }

Caseworker-specific fields (new - only from professional observation):
- "attachmentStyle": "secure" | "anxious" | "avoidant" | "disorganized"
- "traumaSeverity": 0.0 to 1.0
- "emotionalAwareness": 0.0 to 1.0
- "verbalAbility": 0.0 to 1.0
- "therapeuticNeeds": ["..."]
- "placementHistory": { "count": N, "longestStay": "duration", "disruptions": N }
- "diagnosisNotes": ["..."]

OUTPUT:
Respond with ONLY valid JSON (no markdown, no code fences):
{
  "extractedMetrics": { ... },
  "confidence": "high" | "medium" | "low",
  "summary": "1-2 sentence summary of what you extracted"
}`;

serve(async (req: Request) => {
  try {
    const { childName, childAge, notes, childId } = await req.json();

    if (!childName || !notes) {
      return new Response(
        JSON.stringify({ error: "childName and notes are required" }),
        { status: 400 }
      );
    }

    // Call Claude API
    const apiResponse = await fetch("https://api.anthropic.com/v1/messages", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-key": ANTHROPIC_API_KEY,
        "anthropic-version": "2023-06-01",
      },
      body: JSON.stringify({
        model: "claude-sonnet-4-5-20250929",
        max_tokens: 2048,
        system: SYSTEM_PROMPT,
        messages: [
          {
            role: "user",
            content: `Extract structured metrics from the following caseworker notes about ${childName} (age ${childAge ?? "unknown"}):\n\n${notes}`,
          },
        ],
      }),
    });

    if (!apiResponse.ok) {
      const errBody = await apiResponse.json();
      return new Response(
        JSON.stringify({
          extractedMetrics: {},
          confidence: "low",
          summary: "Failed to process notes.",
          error: `API Error ${apiResponse.status}: ${errBody?.error?.message ?? "Unknown"}`,
        }),
        { status: 200, headers: { "Content-Type": "application/json" } }
      );
    }

    const body = await apiResponse.json();
    const content = body.content?.[0]?.text ?? "{}";

    // Parse response - strip markdown fences if present
    let cleaned = content.trim();
    if (cleaned.startsWith("```json")) cleaned = cleaned.slice(7);
    else if (cleaned.startsWith("```")) cleaned = cleaned.slice(3);
    if (cleaned.endsWith("```")) cleaned = cleaned.slice(0, -3);
    cleaned = cleaned.trim();

    let parsed: Record<string, any>;
    try {
      parsed = JSON.parse(cleaned);
    } catch {
      parsed = { extra
[truncated — 1287 more characters]
```

### supabase/functions/compute-matches/index.ts

```typescript
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
import { createClient, SupabaseClient } from "https://esm.sh/@supabase/supabase-js@2";

const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const SUPABASE_SERVICE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;

const ALGORITHM_VERSION = "2.0.0-multilayer";

const DIMENSION_WEIGHTS: Record<string, number> = {
  personality: 0.15,
  emotional: 0.2,
  attachment: 0.2,
  cultural: 0.1,
  communication: 0.1,
  environment: 0.1,
  stability: 0.15,
};

// ── Types ──

interface MatchDimension {
  name: string;
  score: number;
  weight: number;
  explanation: string;
  child_factors: string[];
  family_factors: string[];
}

interface MatchFactor {
  factor_name: string;
  impact: number;
  explanation: string;
  category: string;
}

interface ReasoningLayer {
  layer_number: number;
  name: string;
  description: string;
  score_adjustment?: number;
  score_after?: number;
  findings: string[];
  reasoning: string[];
  data: Record<string, any>;
  passed: boolean;
}

interface MatchResult {
  id: string;
  child_id: string;
  family_id: string;
  child_name: string;
  family_name: string;
  overall_score: number;
  personality_score: number;
  emotional_score: number;
  attachment_score: number;
  cultural_score: number;
  communication_score: number;
  environment_score: number;
  stability_score: number;
  dimensions: Record<string, MatchDimension>;
  strengths: string[];
  challenges: string[];
  recommendations: string[];
  bias_flags: string[];
  placement_stability_prediction: number;
  risk_level: string;
  risk_factors: string[];
  protective_factors: string[];
  scoring_factors: MatchFactor[];
  algorithm_version: string;
  generated_date: string;
  reasoning_layers: ReasoningLayer[];
  status: string;
}

type Child = Record<string, any>;
type Family = Record<string, any>;
type Transcript = Record<string, any>;
type CWNotes = Record<string, any>;

// ── Utility ──

function clamp(v: number, lo: number, hi: number): number {
  return Math.max(lo, Math.min(hi, v));
}

function levelLabel(v: number): string {
  if (v < 0.25) return "Low";
  if (v < 0.5) return "Moderate";
  if (v < 0.75) return "High";
  return "Very High";
}

function calculateOverlap(a: string[], b: string[]): number {
  if (!a?.length || !b?.length) return 0.3;
  const s1 = new Set(a.map((x) => x.toLowerCase()));
  const s2 = new Set(b.map((x) => x.toLowerCase()));
  let inter = 0;
  s1.forEach((x) => { if (s2.has(x)) inter++; });
  const union = new Set([...s1, ...s2]).size;
  return union > 0 ? inter / union : 0;
}

// ── Layer 1: Profile Normalization ──

function layer1Normalize(child: Child, family: Family): ReasoningLayer {
  const findings: string[] = [];
  const reasoning: string[] = [];
  const data: Record<string, any> = {};

  const childNeedLevel =
    (child.trauma_severity ?? 0) * 0.3 +
    (1 - (child.trust_level ?? 0.5)) * 0.25 +
    (1 - (child.emotional_awareness ?? 0.5)) * 0.2 +
    (child.structure_need ?? 0.5) * 0.15 +
    ((child.special_needs?.length ?? 0) > 0 ? 0.1 : 0);
  data.childNeedLevel = childNeedLevel;

  const familyCapacity =
    (family.patience_level ?? 0.5) * 0.2 +
    (family.emotional_regulation_capacity ?? 0.5) * 0.2 +
    (family.resilience_level ?? 0.5) * 0.15 +
    (family.support_network_strength ?? 0.5) * 0.15 +
    (family.available_time ?? 0.5) * 0.15 +
    (family.has_trauma_training ? 0.15 : 0);
  data.familyCapacity = familyCapacity;

  const capacityGap = familyCapacity - childNeedLevel;
  data.capacityGap = capacityGap;

  let familyEnergy = 0.5;
  const acts: string[] = family.family_activities ?? [];
  if (acts.some((a: string) => /sport|outdoor|hiking/i.test(a))) familyEnergy += 0.2;
  if ((family.structure_level ?? 0.5) > 0.7) familyEnergy -= 0.1;
  data.familyEnergy = clamp(familyEnergy, 0, 1);

  findings.push(`Child need level: ${(childNeedLevel * 100).toFixed(0)}%`);
  findings.push(`Family capacity: ${(familyCapacity * 100).toFixed(0)}%`);
  findings.push(`Capacity gap: ${capacityGap > 0 ? "+" : ""}${(capacityGap * 100).toFixed(0)}%`);

  reasoning.push(
    `Extracted ${(child.interests?.length ?? 0) + (child.coping_strategies?.length ?? 0) + (child.emotional_triggers?.length ?? 0)} child feature signals`
  );
  reasoning.push(
    `Extracted ${(family.family_activities?.length ?? 0) + (family.completed_trainings?.length ?? 0) + (family.family_values?.length ?? 0)} family feature signals`
  );

  if (capacityGap > 0.1) reasoning.push("Family capacity exceeds child needs - positive indicator");
  else if (capacityGap < -0.1) reasoning.push("Child needs may exceed family capacity - requires careful evaluation");
  else reasoning.push("Family capacity and child needs are closely matched");

  return {
    layer_number: 1,
    name: "Profile Normalization",
    description: "Extracts composite features from raw profile data and normalizes them for cross-comparison.",
    findings,
    reasoning,
    data,
    passed: true,
  };
}

// ── Layer 2: Hard Constraints ──

function layer2HardConstraints(child: Child, family: Family): ReasoningLayer {
  const findings: string[] = [];
  const reasoning: string[] = [];
  let penalty = 0;
  let passed = true;

  if (child.age < (family.min_child_age ?? 0) || child.age > (family.max_child_age ?? 18)) {
    findings.push(`CONSTRAINT: Child age ${child.age} outside family range ${family.min_child_age}-${family.max_child_age}`);
    reasoning.push("Age mismatch is a soft constraint - penalizes score by 10 points");
    penalty += 10;
  } else {
    findings.push(`Age check passed: ${child.age} within ${family.min_child_age}-${family.max_child_age}`);
  }

  if (child.needs_sibling_placement && !family.open_to_sibling_group) {
    findings.push("CONSTRAINT: Child requires sibling placement, family not open to sibling groups");
    reasoning.push("Sibling separation is a high-risk factor - penalizes score by 15 points");
    penalty += 15;
  } else if (chil
[truncated — 38244 more characters]
```

### analysis_options.yaml

```yaml
include: package:flutter_lints/flutter.yaml

linter:
  rules:
    prefer_const_constructors: false
    prefer_const_literals_to_create_immutables: false
    avoid_print: false

```

### run_dev.sh

```shell
#!/bin/bash
# Launch Flutter Web with CORS disabled for development.
# HeyGen's Streaming Avatar REST API doesn't allow browser-origin requests,
# so we need to disable web security in Chrome for the API calls to work.
#
# Usage: ./run_dev.sh

flutter run -d chrome \
  --web-browser-flag="--disable-web-security" \
  --web-browser-flag="--user-data-dir=/tmp/flutter_chrome_dev"

```

### pubspec.yaml

```yaml
name: adoption_matching
description: AI-powered adoption matching system for improving orphan-family pairing.
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.6
  fl_chart: ^0.68.0
  google_fonts: ^6.1.0
  percent_indicator: ^4.2.3
  uuid: ^4.2.1
  provider: ^6.1.1
  http: ^1.2.0
  supabase_flutter: ^2.3.0
  flutter_tts: ^4.2.0
  speech_to_text: ^7.0.0
  web: ^1.1.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.1

flutter:
  uses-material-design: true

```

### web/index.html

```html
<!DOCTYPE html>
<html>
<head>
  <base href="$FLUTTER_BASE_HREF">
  <meta charset="UTF-8">
  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
  <meta name="description" content="AI-Powered Adoption Matching System">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="The First Match">
  <link rel="icon" type="image/png" href="favicon.png"/>
  <title>The First Match</title>
  <link rel="manifest" href="manifest.json">

  <!-- LiveKit client SDK (required for HeyGen Streaming Avatar WebRTC) -->
  <script src="https://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script>
  <!-- HeyGen LiveKit bridge (connects avatar video to Flutter platform view) -->
  <script src="heygen_livekit_bridge.js"></script>

  <script src="flutter_bootstrap.js" async></script>
</head>
<body>
</body>
</html>

```

### lib/main.dart

```dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'config/secrets.dart';
import 'services/app_state.dart';
import 'utils/theme.dart';
import 'screens/home_screen.dart';
import 'screens/dashboard_screen.dart';
import 'screens/child_onboarding_screen.dart';
import 'screens/family_onboarding_screen.dart';
import 'screens/matches_screen.dart';
import 'screens/match_detail_screen.dart';
import 'screens/children_screen.dart';
import 'screens/families_screen.dart';
import 'screens/counselor_chat_screen.dart';
import 'screens/caseworker_notes_screen.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Supabase.initialize(
    url: Secrets.supabaseUrl,
    anonKey: Secrets.supabaseAnonKey,
  );

  runApp(
    ChangeNotifierProvider(
      create: (_) => AppState(Supabase.instance.client),
      child: const AdoptMatchApp(),
    ),
  );
}

class AdoptMatchApp extends StatelessWidget {
  const AdoptMatchApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'The First Match',
      debugShowCheckedModeBanner: false,
      theme: AppTheme.theme,
      initialRoute: '/',
      routes: {
        '/': (context) => const HomeScreen(),
        '/dashboard': (context) => const DashboardScreen(),
        '/child-onboarding': (context) => const ChildOnboardingScreen(),
        '/family-onboarding': (context) => const FamilyOnboardingScreen(),
        '/matches': (context) => const MatchesScreen(),
        '/match-detail': (context) => const MatchDetailScreen(),
        '/children': (context) => const ChildrenScreen(),
        '/families': (context) => const FamiliesScreen(),
        '/counselor-chat': (context) => const CounselorChatScreen(),
        '/caseworker-notes': (context) => const CaseworkerNotesScreen(),
      },
    );
  }
}

```

### web/heygen_livekit_bridge.js

```javascript
/**
 * HeyGen LiveKit Bridge for Flutter Web
 *
 * This script connects to a HeyGen Streaming Avatar session via LiveKit WebRTC
 * and attaches the remote video track to a <video> element that Flutter renders
 * via HtmlElementView.
 *
 * Loaded in index.html before Flutter bootstrap.
 */

// Store active connections for cleanup
window._heygenConnections = {};

/**
 * Connect to a HeyGen LiveKit room and display the avatar video.
 *
 * @param {string} url - LiveKit WebSocket URL from HeyGen session
 * @param {string} token - LiveKit access token from HeyGen session
 * @param {string} elementId - ID of the <video> element to attach to
 */
async function connectHeyGenLiveKit(url, token, elementId) {
  try {
    // Use LiveKit client SDK (loaded from CDN)
    const room = new LivekitClient.Room({
      adaptiveStream: true,
      dynacast: true,
      videoCaptureDefaults: { resolution: LivekitClient.VideoPresets.h720.resolution },
    });

    // Handle remote track subscribed - this is the avatar video/audio
    room.on(LivekitClient.RoomEvent.TrackSubscribed, (track, publication, participant) => {
      if (track.kind === LivekitClient.Track.Kind.Video) {
        // Find or create the video element
        let videoEl = document.getElementById(elementId);
        if (!videoEl) {
          videoEl = document.createElement('video');
          videoEl.id = elementId;
          videoEl.autoplay = true;
          videoEl.playsInline = true;
          videoEl.muted = false;
          videoEl.style.width = '100%';
          videoEl.style.height = '100%';
          videoEl.style.objectFit = 'cover';
          videoEl.style.borderRadius = '12px';
        }
        // Attach the track to the video element
        track.attach(videoEl);
      }

      if (track.kind === LivekitClient.Track.Kind.Audio) {
        // Create a hidden audio element for the avatar's voice
        const audioEl = document.createElement('audio');
        audioEl.autoplay = true;
        audioEl.id = elementId + '-audio';
        document.body.appendChild(audioEl);
        track.attach(audioEl);
      }
    });

    // Handle disconnection
    room.on(LivekitClient.RoomEvent.Disconnected, () => {
      console.log('[HeyGen] LiveKit room disconnected');
      cleanupConnection(elementId);
    });

    // Connect to the room
    await room.connect(url, token);
    console.log('[HeyGen] Connected to LiveKit room');

    // Store for cleanup
    window._heygenConnections[elementId] = room;

  } catch (error) {
    console.error('[HeyGen] LiveKit connection error:', error);
  }
}

/**
 * Disconnect and clean up a HeyGen session.
 * @param {string} elementId - ID of the video element
 */
function cleanupConnection(elementId) {
  const room = window._heygenConnections[elementId];
  if (room) {
    room.disconnect();
    delete window._heygenConnections[elementId];
  }
  // Remove audio element
  const audioEl = document.getElementById(elementId + '-audio');
  if (audioEl) audioEl.remove();
}

/**
 * Register a video element as a Flutter platform view.
 * Called by Flutter's HtmlElementView factory.
 * @param {string} viewId - The platform view ID
 */
function registerHeyGenVideoView(viewId) {
  const video = document.createElement('video');
  video.id = viewId;
  video.autoplay = true;
  video.playsInline = true;
  video.muted = false;
  video.style.width = '100%';
  video.style.height = '100%';
  video.style.objectFit = 'cover';
  video.style.backgroundColor = '#1a1a2e';
  video.style.borderRadius = '12px';
  // Poster/placeholder styling
  video.poster = '';
  return video;
}

```

[65 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]