Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 30 additions & 11 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
# Supabase Auth public configuration. The application exposes these only through its
# Auth-only browser wrapper; migration 006 denies browser roles access to application data.
# Browser-visible Supabase Auth configuration. This wrapper exposes Auth only; migrations revoke
# browser Data API access to application tables.
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_placeholder

# Administrative connection used only by db:migrate — server-only.
DATABASE_ADMIN_URL=postgresql://postgres:[ADMIN-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres
# Administrative connection used only by db:migrate and synthetic seed/reset operators.
# Keep this out of Vercel's web runtime environment.
DATABASE_ADMIN_URL=postgresql://postgres:[ADMIN-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres

# Restricted application runtime credential — server-only. Provision its secret after migrations.
# Never prefix either database credential with NEXT_PUBLIC.
DATABASE_URL=postgresql://huddle_app:[RUNTIME-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres
# Restricted huddle_app runtime connection. Server-only; never prefix it with NEXT_PUBLIC.
DATABASE_URL=postgresql://huddle_app:[RUNTIME-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres

# Optional only for narration; server-only.
ANTHROPIC_API_KEY=
# Public Server root CA from Supabase Database SSL Configuration, base64 encoded. The server-side
# data layer uses it for certificate and hostname verification; never expose this variable to JS.
SUPABASE_DB_CA_BASE64=base64-encoded-pem-certificate

# Operator-only UUID copied from the one synthetic reviewer account in Supabase Auth.
HUDDLE_DEMO_AUTH_USER_ID=00000000-0000-4000-8000-000000000000

# Fixed reviewed CSV/board anchor. This private portfolio corpus is intentionally time-pinned.
HUDDLE_DEMO_BOARD_DATE=2026-07-27

# Server-only HMAC keys for one-use evidence reveal/acknowledgment grants. Generate at least 32 bytes.
HUDDLE_ACKNOWLEDGMENT_KEY_CURRENT=replace-with-a-long-random-secret
HUDDLE_ACKNOWLEDGMENT_KEY_PREVIOUS=

# Protected nightly and per-request refresh dispatch; server-only.
INTERNAL_REFRESH_SECRET=replace-with-a-random-secret
# Protected nightly dispatcher. Manual reviewer refresh does not require a scheduler.
INTERNAL_REFRESH_SECRET=replace-with-a-different-random-secret
INTERNAL_REFRESH_SECRET_PREVIOUS=
INTERNAL_REFRESH_URL=http://127.0.0.1:3000/internal/refresh

# Operator-only secret for smoke-checking the required Vercel Deployment Protection wall. Leave
# blank for local development and never add this bypass value to the deployed application
# environment.
VERCEL_PROTECTION_BYPASS=

# Optional generated narration only. Leave unset for the reviewed deterministic-fallback demo/eval.
ANTHROPIC_API_KEY=

NODE_ENV=development
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,10 @@ jobs:
- run: npm run lint
- run: npm run format:check
- run: npm run check:determinism
- run: npm run verify:quickstart
- uses: actions/upload-artifact@v4
with:
name: portfolio-eval-report
path: artifacts/portfolio-eval-report.json
- run: npm test
- run: npm run build
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dist/
# Test and coverage
coverage/
.vitest/
artifacts/

# Local tooling indexes
.codegraph/
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ student data is synthetic only. The enforced package/data boundaries and validat
live in `scripts/check-determinism.ts`, `eslint.config.js`, and `.github/workflows/ci.yml`.
<!-- SPECKIT END -->

For credentialed local DB tests, preserve dotenv values exactly:
`RUN_DB_TESTS=1 node --env-file=.env node_modules/vitest/vitest.mjs run`. Do not shell-source `.env`.

## Maintaining this file

Keep this file for knowledge useful to almost every future agent session in this project.
Expand Down
156 changes: 156 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Huddle

Huddle is a private, synthetic-only morning triage board built as a focused work sample for Texas
Sports Academy. It helps a guide or coach decide **who to see first, why, and what to say first** when
adaptive academic software carries practice and adults carry motivation and intervention.

The portfolio demo is deliberately narrow: sign in, upload one fixed CSV, validate and commit it,
refresh, inspect the ranked Evidence Desk, open exact evidence, and see the report acknowledged. It is
not a diagnosis product and makes **no accuracy claim**.

## Why this is useful at TSA

- **Guide utility:** one deterministic queue turns attempt-level activity into a short morning plan.
- **Coach-like intervention:** every report includes a concrete opener, not another analytics chart.
- **Responsible student data:** the roster, activity, names, and evaluation corpus are visibly
synthetic; real student data is rejected and remains out of scope.
- **Trustworthy AI boundary:** rules classify and rank without a model. Optional AI may select only
reviewed language-catalog IDs and evidence slots; deterministic fallback always keeps the board
usable.

## Architecture and trust boundaries

```text
Browser (Supabase Auth only)
-> Next.js Server Components / narrow server actions
-> @huddle/application use cases
-> @huddle/db scoped SQL
-> Supabase PostgreSQL

CSV -> strict synthetic-csv-v1 adapter -> immutable import receipt
-> deterministic signal engine -> immutable board run/head
-> Evidence Desk -> signed one-use reveal -> PostgreSQL acknowledgment ledger

Optional narration -> closed catalog + exact bundle slots
Deterministic ranking/evaluation -X-> model client
```

`packages/db` is the only package that owns `pg`; service/database credentials never enter browser
code. Supabase Auth sessions are verified server-side and resolve exactly one synthetic guide/studio
scope. Browser Data API privileges are revoked by migration. RLS, retention policy, and multi-guide
access are mandatory before real data or a pilot and are intentionally not implemented here.

Authoritative deeper contracts: [plan](specs/001-huddle-triage-board/plan.md),
[application boundaries](specs/001-huddle-triage-board/contracts/application-interfaces.md), and
[evaluation posture](specs/001-huddle-triage-board/contracts/eval-harness.md).

## Clean local quickstart

Prerequisites: Node 22, npm, and one clean Supabase project with Postgres 16 + Auth. In Supabase Auth,
create **one private reviewer email/password account** and copy its user UUID. Do not create any real
student records.

```bash
npm ci
cp .env.example .env
# Fill the two public Auth values, DATABASE_ADMIN_URL, DATABASE_URL,
# SUPABASE_DB_CA_BASE64, HUDDLE_DEMO_AUTH_USER_ID, and
# HUDDLE_ACKNOWLEDGMENT_KEY_CURRENT.
npm run db:migrate
npm run demo:seed
npm run dev
```

Open `http://localhost:3000/login`. The reviewed demo uses
`HUDDLE_DEMO_BOARD_DATE=2026-07-27`, so the fixed CSV remains reproducible rather than drifting with
the wall clock.

Reset is explicit and scoped to the fixed synthetic demo identity:

```bash
npm run demo:reset -- --confirm-synthetic-only
```

The reset command refuses non-synthetic guide/student identities. Use it only in the dedicated demo
project; it clears that synthetic scope's imports, board runs, evidence opens, and acknowledgments,
then restores the four-person synthetic roster.

## Fixed portfolio evaluation

```bash
unset ANTHROPIC_API_KEY
npm run eval:portfolio
npm run verify:quickstart
```

`eval:portfolio` regenerates eight deterministic fallback cases in memory and byte-compares them with
[`fallback-corpus.json`](packages/eval/fixtures/portfolio/fallback-corpus.json). Unknown catalog IDs
and unauthorized evidence slots must hard-fail for every case. Output records `modelCalls: 0` and
writes `artifacts/portfolio-eval-report.json`.

This is evidence for deterministic fallback, grounding, and model-free execution only. It reports no
precision, recall, F1, or diagnosis accuracy. The full simulator/holdout/permutation accuracy gate in
the evaluation contract remains deferred before any pilot or accuracy-backed claim.

## Vercel + Supabase reviewer deployment

`vercel.json` builds the npm workspace and packages `apps/web`. Configure these Vercel variables:

- `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`
- server-only `DATABASE_URL`
- server-only `SUPABASE_DB_CA_BASE64` (the public Supabase Server root CA, base64 encoded)
- server-only `HUDDLE_ACKNOWLEDGMENT_KEY_CURRENT`
- `HUDDLE_DEMO_BOARD_DATE=2026-07-27`
- `INTERNAL_REFRESH_SECRET` only if the protected nightly dispatcher will be used

Do **not** put `DATABASE_ADMIN_URL`, `HUDDLE_DEMO_AUTH_USER_ID`, a Supabase service-role key, or any
credential in browser-visible variables. Run migrations and `demo:seed` from a trusted operator
terminal before deployment. The reviewer-facing `/board` and `/import` operations require the
Supabase account; unauthenticated requests expose no roster, evidence, or freshness.

Private hosting is required for this demo. In Vercel **Project Settings → Deployment Protection**,
select **All Deployments** with **Vercel Authentication** before release, and add the reviewer to the
Vercel project or team. Do not ship from a plan or project configuration that leaves the production
domain public. Send the reviewer the separate Supabase guide credentials through a private channel.

Deploy from the repository root with the pinned CLI version so the committed `vercel.json` and
workspace build are used consistently:

```bash
npx --yes vercel@48.8.0 deploy --prod
```

Create a Vercel Protection Bypass for Automation secret for the hosted smoke only, set it as
`VERCEL_PROTECTION_BYPASS` in the operator's gitignored `.env`, and run:

```bash
npm run smoke:hosted -- --base-url https://YOUR-PRIVATE-DEMO.vercel.app
```

That command checks health/policy output, the sign-in page, the fixed synthetic CSV, and fail-closed
anonymous board access through the required protection wall. Never add the bypass value to Vercel's
application environment or share it with the reviewer. The captain still performs the authenticated
walkthrough below; the smoke command does not pretend to replace it.

## Two-minute reviewer walkthrough

1. **0:00–0:15 — Sign in.** Note the private Supabase Auth boundary and synthetic-only label.
2. **0:15–0:40 — Import.** Open **Import synthetic CSV**, download/select the fixed file, then
validate. Call out the separate received/accepted/duplicate/unmapped/rejected counts and that
validation stores no activity.
3. **0:40–0:55 — Commit and refresh.** Commit the same bytes, then choose **Refresh board now**. The
deterministic engine publishes one immutable ranked run; no model key is needed.
4. **0:55–1:30 — Use the Evidence Desk.** Open the top report. Read the cause-specific opener, compare
it with the student's own baseline, and expand exact attempts/sessions. Severity determines rank;
evidence confidence is shown separately.
5. **1:30–1:45 — Show trust behavior.** Point to “Deterministic fallback · degraded” and the visible
“Seen” acknowledgment. Refresh/back navigation keeps the report and seen state.
6. **1:45–2:00 — Show engineering evidence.** Run `npm run eval:portfolio`: eight fixed cases, hard-fail
grounding injections, zero model calls, and an explicit no-accuracy-claim posture.

## AI-assisted development workflow

The work was shaped spec-first, split into enforceable package boundaries, reviewed in independent
agent passes, and checked with deterministic negative dependency tests. AI accelerated exploration,
implementation, and review; committed contracts, tests, fixed artifacts, SQL constraints, and human
walkthrough evidence remain the acceptance authority.
53 changes: 21 additions & 32 deletions apps/web/app/board/acknowledge-visible-open.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,30 @@
import 'server-only';
'use server';

import type { EvidenceReader, GuideAccess } from '@huddle/application';
import type {
VisibleAcknowledgmentAction,
VisibleOpenAuthorizationAction,
} from './visible-open-acknowledgment';
import { resolveGuideAccess } from '../../lib/guide-access';
import { evidenceReaderForRequest } from '../../lib/evidence-desk-operations';
import {
runAcknowledgeVisibleOpen,
runAuthorizeVisibleOpen,
} from './lib/visible-open-action-handler';

/**
* The composition root binds the request's server-resolved reader. Browser input contains only the
* two opaque credentials; scope, report IDs, and finding fingerprints are never action parameters.
*/
export function createAcknowledgeVisibleOpenAction(dependencies: {
resolveAccess(): Promise<GuideAccess | null>;
evidenceReader: EvidenceReader;
}): VisibleAcknowledgmentAction {
return async ({ acknowledgmentGrant, openingRenewalToken }) => {
'use server';
const access = await dependencies.resolveAccess();
if (!access) return { kind: 'not-found' };
return dependencies.evidenceReader.acknowledgeVisibleOpen(access, {
acknowledgmentGrant,
openingRenewalToken,
});
};
/** Module-level action captures no request dependencies in the React Flight payload. */
export async function acknowledgeVisibleOpenAction(
input: Parameters<VisibleAcknowledgmentAction>[0]
): ReturnType<VisibleAcknowledgmentAction> {
const evidenceReader = evidenceReaderForRequest();
if (!evidenceReader) return { kind: 'not-found' };
return runAcknowledgeVisibleOpen({ resolveAccess: resolveGuideAccess, evidenceReader }, input);
}

export function createAuthorizeVisibleOpenAction(dependencies: {
resolveAccess(): Promise<GuideAccess | null>;
evidenceReader: EvidenceReader;
}): VisibleOpenAuthorizationAction {
return async ({ acknowledgmentGrant, openingRenewalToken }) => {
'use server';
const access = await dependencies.resolveAccess();
if (!access) return { kind: 'not-found' };
return dependencies.evidenceReader.authorizeVisibleOpen(access, {
acknowledgmentGrant,
openingRenewalToken,
});
};
/** Module-level action resolves auth and reconstructs the fail-closed reader on every POST. */
export async function authorizeVisibleOpenAction(
input: Parameters<VisibleOpenAuthorizationAction>[0]
): ReturnType<VisibleOpenAuthorizationAction> {
const evidenceReader = evidenceReaderForRequest();
if (!evidenceReader) return { kind: 'not-found' };
return runAuthorizeVisibleOpen({ resolveAccess: resolveGuideAccess, evidenceReader }, input);
}
Loading
Loading