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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jobs:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run architecture:check
- run: npm run lint
- run: npm run typecheck
- run: npm test
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ next-env.d.ts
/dist/
/.wrangler/
/outputs/
/output/
/audit/
/.playwright-cli/
/work/
9 changes: 6 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ WonderDrive is in an intentionally narrow hackathon build. Contributions should

## Before a pull request

1. Read `docs/phase-0.md` and the relevant section of `docs/architecture.md`.
1. Read `docs/architecture.md`, then use `docs/code-index.md` to locate the smallest responsible module.
2. Keep secrets and provider keys server-side. Never add a provider key to browser code, fixtures, logs, or screenshots.
3. Preserve the product invariants: one performer, bounded foreground research, honest evidence, exactly two options, and no invisible continuation.
4. Add or update tests for the behavior being changed.
5. Run:
5. If files or local imports changed, refresh the checked architecture index with `npm run architecture:update`.
6. Run:

```bash
npm run architecture:check
npm run lint
npm run typecheck
npm test
```

6. If the D1 schema changes, run `npm run db:generate`, inspect the SQL, and commit the migration.
7. If the D1 schema changes, run `npm run db:generate`, inspect the SQL, and commit the migration.

## Pull requests

Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@ cp .env.example .env.local
npm run dev
```

The local site runs at `http://localhost:3000`. Set `OPENAI_API_KEY` in `.env.local` to exercise live mode locally; the free demo and build tests require no provider key. `WONDERDRIVE_DAILY_BUDGET_USD` optionally changes the default $25 rolling project ceiling. Never expose either value through a `NEXT_PUBLIC_` variable.
The local site runs at `http://localhost:3000`. Set `OPENAI_API_KEY` in `.env.local` to exercise live mode locally; build tests require no provider request. `WONDERDRIVE_DAILY_BUDGET_USD` optionally changes the default $25 rolling project ceiling. Never expose either value through a `NEXT_PUBLIC_` variable.

Apply all SQL files in `drizzle/` to a fresh local D1 database before exercising the API. Sites applies the packaged migrations when a version is deployed.

## Validation

```bash
npm run architecture:check
npm run lint
npm run typecheck
npm test
Expand All @@ -59,8 +60,9 @@ npm run db:generate
app/ Product experience, identity helper, and server routes
db/ Canonical D1 schema
drizzle/ Generated, reviewed SQL migrations
lib/ Contracts, reviewed fixtures, identity, and D1 repository
docs/ Final blueprint, architecture, and phase gates
lib/ Contracts, domain boundaries, providers, fixtures, and D1 repositories
scripts/ Architecture-index maintenance tooling
docs/ Blueprint, architecture, generated code index, and phase gates
tests/ Rendered production and fixture checks
.openai/hosting.json Logical Sites-managed bindings
```
Expand All @@ -72,11 +74,12 @@ tests/ Rendered production and fixture checks
- [Phase 2 live research contract](docs/phase-2.md)
- [V3 implementation contract](docs/v3-implementation.md)
- [Final architecture decisions](docs/architecture.md)
- [Current code index](docs/code-index.md)
- [Final product and engineering blueprint](docs/WonderDrive_Final_Product_and_Engineering_Blueprint_v3_Research_First.docx)

## Status and scope

Live mode is one foreground OpenAI Responses request with built-in web search. Presets cap tool calls, output tokens, reasoning effort, and wall time; guest and signed-in identities also have rolling live-run and estimated-spend limits. Consulted URLs, cited relations, provider request ID, complete usage, price snapshot, prompt/performer/model versions, and research handoff are saved with the committed turn. The free demo remains available for zero-provider-cost judging and development.
Every journey uses a user-selected, compatible OpenAI model through one foreground Responses request with built-in text and optional image search. Presets cap tool calls, output tokens, reasoning effort, and wall time; guest and signed-in identities also have rolling live-run and estimated-spend limits. Consulted URLs, cited relations, sourced image results, provider request ID, complete usage, price snapshot, prompt/performer/model versions, and research handoff are saved with the committed turn.

Automatic journeys, scheduled/background continuation, Trigger.dev, provider fan-out, and live parallel comparison are outside the hackathon scope.

Expand Down
14 changes: 5 additions & 9 deletions app/api/bootstrap/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { BOOTSTRAP_CATALOG } from "../../../lib/catalog";
import { failure, success } from "../../../lib/api";
import { query } from "../../../lib/api";
import { getPreferences } from "../../../lib/product-repository";
import { resolveViewer } from "../../../lib/viewer";

export async function GET() {
try {
const viewer = await resolveViewer();
const preferences = await getPreferences(viewer);
return success({ catalog: BOOTSTRAP_CATALOG, preferences }, viewer);
} catch (error) {
return failure(error);
}
return query(async (viewer) => ({
catalog: BOOTSTRAP_CATALOG,
preferences: await getPreferences(viewer),
}));
}
15 changes: 6 additions & 9 deletions app/api/compare/route.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { failure, success } from "../../../lib/api";
import { compareJourneys, RepositoryError } from "../../../lib/repository";
import { resolveViewer } from "../../../lib/viewer";
import { query } from "../../../lib/api";
import { RepositoryError } from "../../../lib/errors";
import { compareJourneys } from "../../../lib/repository";

export async function GET(request: Request) {
try {
const viewer = await resolveViewer();
return query(async (viewer) => {
const url = new URL(request.url);
const left = url.searchParams.get("left");
const right = url.searchParams.get("right");
if (!left || !right) {
throw new RepositoryError("BAD_REQUEST", "Choose two journeys to compare.", 400);
}
return success(await compareJourneys(viewer, left, right), viewer);
} catch (error) {
return failure(error);
}
return compareJourneys(viewer, left, right);
});
}
33 changes: 22 additions & 11 deletions app/api/journeys/[journeyId]/advance/route.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import { assertMutationOrigin, failure, readJson, success } from "../../../../../lib/api";
import { mutation, readJson } from "../../../../../lib/api";
import type { AdvanceJourneyRequest } from "../../../../../lib/contracts";
import { advanceJourney } from "../../../../../lib/repository";
import { resolveViewer } from "../../../../../lib/viewer";
import { runLiveRedraw } from "../../../../../lib/live-redraw";
import {
advanceJourney,
listRejectedQuestions,
} from "../../../../../lib/repository";

type Context = { params: Promise<{ journeyId: string }> };

export async function POST(request: Request, context: Context) {
try {
assertMutationOrigin(request);
const viewer = await resolveViewer();
return mutation(request, async (viewer) => {
const { journeyId } = await context.params;
const body = (await readJson(request)) as AdvanceJourneyRequest;
return success(await advanceJourney(viewer, journeyId, body), viewer);
} catch (error) {
return failure(error);
}
const body = await readJson<AdvanceJourneyRequest>(request);
return advanceJourney(viewer, journeyId, body, async ({ journey, turn }) =>
runLiveRedraw({
turn,
performerId: journey.performerId,
modelId: journey.modelId,
rejectedQuestions: [
...await listRejectedQuestions(viewer, journeyId),
...turn.options.map((option) => option.question),
],
adventure: body.adventure ?? 50,
reason: body.reason?.trim() || undefined,
}),
);
});
}
32 changes: 10 additions & 22 deletions app/api/journeys/[journeyId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,26 @@
import { assertMutationOrigin, failure, readJson, success } from "../../../../lib/api";
import { mutation, query, readJson } from "../../../../lib/api";
import { updateJourneyManagement } from "../../../../lib/product-repository";
import { deleteJourney, getJourney } from "../../../../lib/repository";
import { resolveViewer } from "../../../../lib/viewer";

type Context = { params: Promise<{ journeyId: string }> };

export async function GET(_request: Request, context: Context) {
try {
const viewer = await resolveViewer();
return query(async (viewer) => {
const { journeyId } = await context.params;
return success(await getJourney(viewer, journeyId), viewer);
} catch (error) {
return failure(error);
}
return getJourney(viewer, journeyId);
});
}

export async function PATCH(request: Request, context: Context) {
try {
assertMutationOrigin(request);
const viewer = await resolveViewer();
return mutation(request, async (viewer) => {
const { journeyId } = await context.params;
return success(await updateJourneyManagement(viewer, journeyId, await readJson(request)), viewer);
} catch (error) {
return failure(error);
}
return updateJourneyManagement(viewer, journeyId, await readJson(request));
});
}

export async function DELETE(request: Request, context: Context) {
try {
assertMutationOrigin(request);
const viewer = await resolveViewer();
return mutation(request, async (viewer) => {
const { journeyId } = await context.params;
return success(await deleteJourney(viewer, journeyId), viewer);
} catch (error) {
return failure(error);
}
return deleteJourney(viewer, journeyId);
});
}
24 changes: 8 additions & 16 deletions app/api/journeys/[journeyId]/snapshots/route.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
import { assertMutationOrigin, failure, readJson, success } from "../../../../../lib/api";
import { mutation, query, readJson } from "../../../../../lib/api";
import { createSnapshot, listSnapshots } from "../../../../../lib/product-repository";
import { resolveViewer } from "../../../../../lib/viewer";

type Context = { params: Promise<{ journeyId: string }> };

export async function GET(_request: Request, context: Context) {
try {
const viewer = await resolveViewer();
return query(async (viewer) => {
const { journeyId } = await context.params;
return success(await listSnapshots(viewer, journeyId), viewer);
} catch (error) {
return failure(error);
}
return listSnapshots(viewer, journeyId);
});
}

export async function POST(request: Request, context: Context) {
try {
assertMutationOrigin(request);
const viewer = await resolveViewer();
return mutation(request, async (viewer) => {
const { journeyId } = await context.params;
const body = (await readJson(request)) as { label?: unknown };
return success(await createSnapshot(viewer, journeyId, body.label), viewer, 201);
} catch (error) {
return failure(error);
}
const body = await readJson<{ label?: unknown }>(request);
return createSnapshot(viewer, journeyId, body.label);
}, 201);
}
23 changes: 7 additions & 16 deletions app/api/journeys/route.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
import { assertMutationOrigin, failure, readJson, success } from "../../../lib/api";
import { mutation, query, readJson } from "../../../lib/api";
import type { CreateJourneyRequest } from "../../../lib/contracts";
import { createJourney, listJourneys } from "../../../lib/repository";
import { resolveViewer } from "../../../lib/viewer";

export async function GET() {
try {
const viewer = await resolveViewer();
return success(await listJourneys(viewer), viewer);
} catch (error) {
return failure(error);
}
return query(listJourneys);
}

export async function POST(request: Request) {
try {
assertMutationOrigin(request);
const viewer = await resolveViewer();
const body = (await readJson(request)) as CreateJourneyRequest;
return success(await createJourney(viewer, body), viewer, 201);
} catch (error) {
return failure(error);
}
return mutation(
request,
async (viewer) => createJourney(viewer, await readJson<CreateJourneyRequest>(request)),
201,
);
}
18 changes: 3 additions & 15 deletions app/api/preferences/route.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,10 @@
import { assertMutationOrigin, failure, readJson, success } from "../../../lib/api";
import { mutation, query, readJson } from "../../../lib/api";
import { getPreferences, updatePreferences } from "../../../lib/product-repository";
import { resolveViewer } from "../../../lib/viewer";

export async function GET() {
try {
const viewer = await resolveViewer();
return success(await getPreferences(viewer), viewer);
} catch (error) {
return failure(error);
}
return query(getPreferences);
}

export async function PUT(request: Request) {
try {
assertMutationOrigin(request);
const viewer = await resolveViewer();
return success(await updatePreferences(viewer, await readJson(request)), viewer);
} catch (error) {
return failure(error);
}
return mutation(request, async (viewer) => updatePreferences(viewer, await readJson(request)));
}
12 changes: 4 additions & 8 deletions app/api/research/[runId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { failure, success } from "../../../../lib/api";
import { query } from "../../../../lib/api";
import { getResearchStatus } from "../../../../lib/product-repository";
import { resolveViewer } from "../../../../lib/viewer";

type Context = { params: Promise<{ runId: string }> };

export async function GET(_request: Request, context: Context) {
try {
const viewer = await resolveViewer();
return query(async (viewer) => {
const { runId } = await context.params;
return success(await getResearchStatus(viewer, runId), viewer);
} catch (error) {
return failure(error);
}
return getResearchStatus(viewer, runId);
});
}
Loading
Loading