# Project export: Compass

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: OpenAI Build Week
- Tagline: Turn any school exercise into guided practice without taking the thinking away.
- Devpost: https://devpost.com/software/compass-tedvqs
- GitHub: https://github.com/VincentNinIA/Compass
- Demo: https://compass-geotutor-demo.vercel.app/
- Video: https://www.youtube.com/embed/kI-Dt8hJx00?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — VincentNinIA (46 commits)

## Devpost submission (written by the team)

### Inspiration

Compass started with two memories. The first was OpenAI’s GPT-4o launch demo. A child was working on a geometry problem and pointed to the wrong side of a triangle. Instead of simply giving the answer, the AI noticed what was happening and gently helped them correct it. The second was my father. He taught mathematics for more than 50 years and often spoke to me about GeoGebra. On paper, geometry can feel static. In GeoGebra, students can move a point, test an idea, and immediately see what changes—and what does not. When I saw the OpenAI demo, I imagined combining these two ideas: GeoGebra as the space where the student experiments. A realtime AI companion that understands enough of that space to offer the right help at the right moment. With the release of gpt-realtime-2.1, I felt that the time had come—and the hackathon was a good excuse to finally build it. I called my father and told him: “I’m going to bring your project to life.” That became Compass.

### What it does

Compass is a bilingual learning companion that helps students work through an exercise without doing the thinking for them. The main demonstration is a guided investigation of the Varignon theorem. A student opens the activity in one click and works directly inside GeoGebra. No account or classroom code is required for this public journey. The investigation contains nine missions. Students: Construct the four exact midpoints of a quadrilateral. Connect them. Explore convex, concave, and crossed configurations. Formulate a conjecture. Verify parallel relationships. Build a justification. Finish with a transfer question. Compass can speak with the student or respond through text. It can ask a short question, offer a progressive hint, or point to the exact GeoGebra tool or object it is discussing. For example, if a student does not know how to construct a midpoint, Compass can highlight the real Midpoint tool. It can explain which points to select and, when explicitly requested, perform one bounded construction using existing points. However, Compass cannot click anywhere it wants or send arbitrary commands to GeoGebra. The application controls the available actions, their targets, and their effects. The student also remains in control of the conversation. Speaking or manipulating the figure interrupts any response or guidance already in progress. The student is no longer alone in front of their screen. They have a companion that helps them discover, experiment, and think at their own pace. A clear difference between help and verification One of the most important ideas behind Compass is that the AI does not decide whether the mathematics is correct. Compass receives a limited description of the GeoGebra construction, but exact relationships are checked by the application. A point placed approximately in the middle of a segment is not accepted as an exact midpoint. Compass checks the actual geometric dependency. The application also verifies supported parallel relationships and identifies the convex, concave, and crossed configurations used during the investigation. This creates two distinct types of progress: A student can explain what they tried and mark a mission as completed. A compatible deterministic check can confirm that the mission has been verified. The AI cannot award verified XP, complete a mission, or turn its own construction into student evidence. Progressive help Compass uses several levels of support. It can begin with a simple question, continue with a more precise hint, and then provide visual guidance inside GeoGebra. A guided demonstration is only possible after the student has agreed to it. I spent a lot of time thinking about when Compass should intervene. I did not want it to react negatively to a first mistake or constantly interrupt the learner. At the same time, Compass should not simply repeat the same explanation again and again. That is why I created an agent harness that gives Compass the ability to help—but prevents it from helping too quickly. Compass can take limited initiative: When the activity begins. After the current mission changes. When a repeated difficulty has been detected. Its first response should always be the smallest useful hint, not the solution. Teacher preparation The repository also contains a teacher space where an activity can be prepared from: A topic. An existing worksheet. A manually written exercise. The generated draft is never published automatically. The teacher can edit the statement, learning objective, missions, common difficulties, and support instructions before reviewing the real student experience. For the Varignon investigation, the teacher can edit the wording and XP value of each mission while keeping the deterministic mathematical structure intact. The project also includes a general workflow for reading a photograph of a school exercise. The student reviews the extracted statement before beginning. If the image is incomplete or unclear, Compass asks for clarification instead of inventing the missing information. Conversational guidance can support different school subjects. Automatic verification, however, is only displayed when Compass has a compatible deterministic module. Today, the complete specialist experience is the GeoGebra Varignon investigation. Privacy and classroom reporting Compass does not produce a school grade. By default, the public prototype keeps media, progress, and reports in memory. Audio, transcripts, and uploaded images are not stored in the learning report. The teacher can receive factual information such as: Completed missions. Verified missions. Captured configurations. Earned XP. The highest support level used. The student’s free-text reasoning and transfer answer are not sent back to the teacher. I also built a limited classroom pilot with rotating joining codes, student pseudonyms, targeted assignments, and PostgreSQL. It is separate from the public one-click demonstration and is not intended to be a complete learning management system. How I built it I used Codex throughout the project as an engineering and review collaborator. It helped me turn each product idea into a small implementation slice, keep specifications and runtime contracts aligned, write tests, and challenge the project from technical, educational, accessibility, and product perspectives. Compass uses GeoGebra under its Non-Commercial License, so the GeoGebra code remains subject to GeoGebra’s license. I contacted the GeoGebra team, and they confirmed that I could use GeoGebra for this hackathon. Compass is a Next.js and TypeScript application built around three separate AI responsibilities: gpt-5.6-terra reads a student’s exercise image and converts it into a strict structured format. The image is processed in memory, without tools, using store: false. gpt-5.6-luna can create one editable teacher draft from a topic or learning objective. It does not publish anything and does not receive tools. gpt-realtime-2.1 provides low-latency voice and text tutoring over WebRTC. Everything else is controlled by the application. Challenges I ran into Defining what the AI should be allowed to do The hardest challenge was deciding what the AI should be allowed to do. Giving a model unrestricted access to GeoGebra would have made the demonstration easier to build, but it would also have made the result difficult to trust. Instead, I created a closed set of semantic actions. Compass can refer to a midpoint, a line, or an existing point, while the application chooses the actual command, validates it, and can roll it back. Separating conversation from proof Another challenge was separating conversation from proof. An encouraging AI response is useful, but it is not mathematical evidence. I had to ensure that the model could never: Validate its own answer. Award verified XP. Transform an assistant-created object into student work. Handling realtime interruptions Realtime interruption was also more difficult than I expected. If the student moves a point while Compass is speaking, the current response may already refer to an outdated figure. Compass therefore cancels the response and waits for the construction to become stable before continuing. Knowing when helping becomes interrupting Finally, I had to decide when helping becomes interrupting. Compass needed to be proactive enough to feel present, but patient enough to let the student try. This part can still be improved, but the current experience already feels surprisingly alive. Accomplishments that I’m proud of The moment I am most proud of was when my father called me and said: “It’s amazing—and you’re not even a mathematics teacher!” I told him: “Codex helps me. I’m the coach!” I am also proud that Compass does more than place a chatbot next to GeoGebra. The companion can: Understand a limited geometric world. Point to real controls. Follow the student’s progress. Act inside the exercise without taking ownership of the work. Discuss mathematical demonstrations. Adapt the level of the conversation to the student. The agent harness was an especially important achievement for me. It feels as though someone is actually beside the learner, reacting to what is happening instead of repeating generic instructions. I am equally proud that the project states its limits clearly. Compass does not claim to: Correct every exercise. Understand every subject deterministically. Replace a teacher. What I learned I learned that building useful educational AI is not only about making the model more capable. It is also about deciding where its authority should stop. The model can be excellent at listening, explaining, and encouraging. Verification, rewards, and irreversible actions require stronger guarantees. I also learned that good tutoring often means doing less. A short question at the right moment can be more helpful than a complete explanation. Finally, Codex helped me understand the value of keeping product decisions, implementation, and tests connected. When I changed an educational rule, I could follow that decision through the contracts, interface, and browser journeys instead of treating it as prompt wording alone.

### What's next

I joined this hackathon to: Turn a personal idea into something real. Help make AI useful in education. Honor my father’s 50 years as a mathematics teacher. Varignon is the first complete specialist investigation, not the final destination. The next step is to create a reusable system where teachers and contributors can add other validated geometry investigations without rebuilding the entire runtime. I would also like to test Compass in a real classroom with a teacher and a small group of students. That would help me understand: When students accept proactive guidance. When they prefer silence. What information is genuinely useful to the teacher. I intend to make the source code available to the GeoGebra community so that educators and developers can continue improving it. Compass currently uses GeoGebra under its Non-Commercial License and is intended for accessible, educational, and non-commercial use. If Compass wins first or second place, I will use 20% of the cash prize to fund API credits for a school and support a real educational pilot. Realtime AI has a cost—and the long testing sessions with my father definitely hurt my bank account! No student should lose access to foundational learning because they cannot afford individual help.

## README (from the GitHub repository)

# Compass

**A bilingual, realtime AI learning companion that helps students think—not
just get answers.**

Compass combines a live GeoGebra workspace with a voice and text tutor that can
observe a bounded geometric world, point to the exact tool or object it is
discussing, and provide the smallest useful hint while the application keeps
mathematical verification under deterministic control.

**[Open the public demo](https://compass-geotutor-demo.vercel.app/)**

![Compass points to the Midpoint tool in GeoGebra](output/playwright/T74-midpoint-guidance-1440x900.png)

## Inspiration

Compass started with two memories.

The first was OpenAI's GPT-4o launch demo. A child was working on a geometry
problem and pointed to the wrong side of a triangle. Instead of simply giving
the answer, the AI noticed what was happening and gently helped them correct it.

The second was my father.

He taught mathematics for more than 50 years and often spoke to me about
GeoGebra. On paper, geometry can feel static. In GeoGebra, students can move a
point, test an idea, and immediately see what changes—and what does not.

When I saw the OpenAI demo, I imagined combining these two ideas:

- **GeoGebra** as the space where the student experiments.
- **A realtime AI companion** that understands enough of that space to offer
  the right help at the right moment.

With the release of `gpt-realtime-2.1`, I felt that the time had come—and the
hackathon was a good excuse to finally build it.

I called my father and told him:

> “I'm going to bring your project to life.”

That became **Compass**.

## What it does

Compass is a bilingual learning companion that helps students work through an
exercise **without doing the thinking for them**.

The flagship experience is a guided investigation of the **Varignon theorem**.
A student opens the teacher-prepared activity in one click and works directly
inside GeoGebra. No account, classroom code, or pseudonym is required for the
public journey.

The investigation contains nine missions. Students:

1. Construct the four exact midpoints of a quadrilateral.
2. Connect the midpoints.
3. Explore a convex configuration.
4. Explore a concave configuration.
5. Explore a crossed configuration.
6. Formulate a conjecture.
7. Verify the relevant parallel relationships.
8. Build a justification.
9. Finish with a transfer question.

Compass can speak with the student or respond through text. It can ask a short
question, offer a progressive hint, or point to the exact GeoGebra tool or
object it is discussing.

For example, if a student does not know how to construct a midpoint, Compass can
highlight the real **Midpoint** tool and explain which points to select. When
explicitly requested, it can also perform one bounded construction using
existing points.

Compass cannot click anywhere it wants or send arbitrary commands to GeoGebra.
The application controls the available actions, their targets, permissions,
budgets, and effects.

The student also remains in control of the conversation. Speaking or
manipulating the figure interrupts any response or guidance already in
progress. The student is no longer alone in front of the screen: they have a
companion that helps them discover, experiment, and think at their own pace.

## Help is not verification

One of the most important ideas behind Compass is that **the AI does not decide
whether the mathematics is correct**.

Compass receives a limited description of the GeoGebra construction, but exact
relationships are checked by the application. A point placed approximately in
the middle of a segment is not accepted as an exact midpoint: the application
checks its actual geometric dependency. It also verifies supported parallel
relationships and identifies the convex, concave, and crossed configurations
used during the investigation.

This creates two distinct types of progress:

- A student can explain what they tried and mark a compatible mission as
  completed.
- A deterministic application check can confirm that a compatible mission has
  been verified.

The AI cannot award verified XP, complete a mission, or turn its own
construction into student evidence.

## Progressive help

Compass uses several levels of support:

1. Begin with a short question.
2. Continue with a more precise hint.
3. Provide temporary visual guidance inside GeoGebra.
4. Offer a bounded guided step only when the activity policy allows it and the
   student has requested enough help.

I spent a lot of time thinking about **when Compass should intervene**. I did
not want it to react negatively to a first mistake or constantly interrupt the
learner. At the same time, Compass should not simply repeat the same explanation
again and again.

That is why I created an agent harness that gives Compass the ability to help
while preventing it from helping too quickly.

Compass can take limited initiative when the activity begins, after the current
mission changes, or when a repeated difficulty has been detected. Its first
response should always be **the smallest useful hint**, not the solution.

## Teacher preparation

The repository also contains a teacher space where an activity can be prepared
from:

- A topic or learning objective.
- An existing worksheet image.
- A manually written exercise.

The generated draft is never published automatically. The teacher can edit the
statement, learning objective, missions, common difficulties, support
instructions, and XP rewards before reviewing the real student experience.

For the Varignon investigation, the teacher can edit the wording and XP value
of each mission while keeping the deterministic mathematical structure intact.

The project also includes a general workflow for reading a photograph of a
school exercise. The student reviews the extracted statement before beginning.
If the image is incomplete or unclear, Compass asks for clarification instead
of inventing the missing information.

Conversational guidance can support different school subjects. Automatic
verification, however, is only displayed when Compass has a compatible
deterministic module. Today, the complete specialist experience is the
**GeoGebra Varignon investigation**.

## Privacy and classroom reporting

Compass does not produce a school grade.

By default, the public prototype keeps media, progress, and reports in memory.
Audio, transcripts, and uploaded images are not stored in the learning report.

The teacher can receive factual information such as:

- Completed missions.
- Verified missions.
- Captured configurations.
- Earned XP.
- The highest support level used.

The student's free-text reasoning and transfer answer are not sent back to the
teacher.

The repository also contains a limited classroom pilot with rotating joining
codes, student pseudonyms, targeted assignments, and PostgreSQL 16. It is
separate from the public one-click demonstration and is not intended to be a
complete learning management system.

## How I built it

I used Codex throughout the project as an engineering and review collaborator.
It helped me turn each product idea into a small implementation slice, keep
specifications and runtime contracts aligned, write tests, and challenge the
project from technical, educational, accessibility, and product perspectives.

Compass is a Next.js and TypeScript application built around three separate AI
responsibilities:

| Responsibility | Model | Boundary |
|---|---|---|
| Read a student's exercise image | `gpt-5.6-terra` | One server call, in-memory image, `store:false`, no tools, strict structured output |
| Prepare an editable teacher draft | `gpt-5.6-luna` | At most one call, low effort, `store:false`, no tools, no automatic publication |
| Provide voice and text tutoring | `gpt-realtime-2.1` | Low-latency WebRTC session with closed functions in the specialist investigation |
| Check relations, evidence, and XP | No model | Deterministic TypeScript engine, versioned contracts, and a local ledger |

Everything

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 425 recognized source files, 3895 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- PostgreSQL (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- TypeScript (language) — detected in the code
- Vercel (technology) — claimed on Devpost, not found in the code
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (120 of 459)

```
.env.example
.gitignore
.nvmrc
AGENTS.md
agents/CONTRACT.md
agents/DECISIONS.md
agents/SPEC.md
agents/TODO_NEXT.md
apps/frontend/app/api/classroom/join/route.ts
apps/frontend/app/api/classroom/teacher/assignments/route.ts
apps/frontend/app/api/classroom/teacher/classes/route.ts
apps/frontend/app/api/classroom/teacher/session/route.ts
apps/frontend/app/api/demo/access/route.ts
apps/frontend/app/api/exercise/parse/route.ts
apps/frontend/app/api/realtime/session/route.ts
apps/frontend/app/api/teacher/draft/route.ts
apps/frontend/app/api/teacher/exercises/route.test.ts
apps/frontend/app/api/teacher/exercises/route.ts
apps/frontend/app/globals.css
apps/frontend/app/layout.tsx
apps/frontend/app/page.test.tsx
apps/frontend/app/page.tsx
apps/frontend/components/classroom-join.tsx
apps/frontend/components/classroom-teacher-panel.tsx
apps/frontend/components/compass-mascot.test.tsx
apps/frontend/components/compass-mascot.tsx
apps/frontend/components/demo-access-gate.test.tsx
apps/frontend/components/demo-access-gate.tsx
apps/frontend/components/exercise-photo/exercise-confirmation.test.tsx
apps/frontend/components/exercise-photo/exercise-confirmation.tsx
apps/frontend/components/exercise-photo/exercise-uploader.test.tsx
apps/frontend/components/exercise-photo/exercise-uploader.tsx
apps/frontend/components/general-exercise-workspace.test.tsx
apps/frontend/components/general-exercise-workspace.tsx
apps/frontend/components/geogebra-scratchpad.tsx
apps/frontend/components/geogebra-spike.test.tsx
apps/frontend/components/geogebra-spike.tsx
apps/frontend/components/geometry-evidence-gallery.test.tsx
apps/frontend/components/geometry-evidence-gallery.tsx
apps/frontend/components/geometry-guidance-overlay.test.tsx
apps/frontend/components/geometry-guidance-overlay.tsx
apps/frontend/components/geometry-investigation-panel.test.tsx
apps/frontend/components/geometry-investigation-panel.tsx
apps/frontend/components/geometry-published-workspace.test.tsx
apps/frontend/components/geometry-published-workspace.tsx
apps/frontend/components/geometry-teacher-studio.test.tsx
apps/frontend/components/geometry-teacher-studio.tsx
apps/frontend/components/invariance-experiment.test.tsx
apps/frontend/components/invariance-experiment.tsx
apps/frontend/components/language-provider.tsx
apps/frontend/components/progress-feedback.tsx
apps/frontend/components/realtime-spike.tsx
apps/frontend/components/reliability-panel.test.tsx
apps/frontend/components/reliability-panel.tsx
apps/frontend/components/teacher-exercise-library.test.tsx
apps/frontend/components/teacher-exercise-library.tsx
apps/frontend/components/teacher-workspace.test.tsx
apps/frontend/components/teacher-workspace.tsx
apps/frontend/components/tutor-workspace-exercise-data-minimization.test.tsx
apps/frontend/components/tutor-workspace.tsx
apps/frontend/e2e/t0.spec.ts
apps/frontend/e2e/t11-general-exercise.spec.ts
apps/frontend/e2e/t18-education-loop.spec.ts
apps/frontend/e2e/t22-actions.spec.ts
apps/frontend/e2e/t22-engine.spec.ts
apps/frontend/e2e/t22-evidence.spec.ts
apps/frontend/e2e/t22-golden-gate.spec.ts
apps/frontend/e2e/t22-learning.spec.ts
apps/frontend/e2e/t22-realtime-live.spec.ts
apps/frontend/e2e/t22-teacher-varignon.spec.ts
apps/frontend/e2e/t22-world-v2.spec.ts
apps/frontend/e2e/t24-demo-access.fixture.ts
apps/frontend/e2e/t24-demo-access.spec.ts
apps/frontend/e2e/t25-classroom.fixture.ts
apps/frontend/e2e/t25-classroom.spec.ts
apps/frontend/e2e/t25-demo-direct.spec.ts
apps/frontend/e2e/t3-photo.spec.ts
apps/frontend/e2e/t4-pedagogy.spec.ts
apps/frontend/e2e/t5-invariance.spec.ts
apps/frontend/e2e/t6-capability.spec.ts
apps/frontend/e2e/t6-evidence-log.spec.ts
apps/frontend/e2e/t6-live-gate.spec.ts
apps/frontend/e2e/t6-presentation.spec.ts
apps/frontend/e2e/t6-reliability.spec.ts
apps/frontend/e2e/t6-reset.spec.ts
apps/frontend/e2e/t9-mascot.spec.ts
apps/frontend/eslint.config.mjs
apps/frontend/evals/t3-exercise-live.eval.ts
apps/frontend/lib/classroom/access.ts
apps/frontend/lib/classroom/activity-catalog.test.ts
apps/frontend/lib/classroom/activity-catalog.ts
apps/frontend/lib/classroom/contracts.test.ts
apps/frontend/lib/classroom/contracts.ts
apps/frontend/lib/classroom/data-policy.ts
apps/frontend/lib/classroom/index.ts
apps/frontend/lib/classroom/join-code.test.ts
apps/frontend/lib/classroom/join-code.ts
apps/frontend/lib/classroom/lifecycle.ts
apps/frontend/lib/classroom/migration.ts
apps/frontend/lib/classroom/pilot-assignment-service.test.ts
apps/frontend/lib/classroom/pilot-http.test.ts
apps/frontend/lib/classroom/pilot-http.ts
apps/frontend/lib/classroom/pilot-runtime.ts
apps/frontend/lib/classroom/pilot-service.ts
apps/frontend/lib/classroom/pilot-session.test.ts
apps/frontend/lib/classroom/pilot-session.ts
apps/frontend/lib/classroom/pilot-store.test.ts
apps/frontend/lib/classroom/pilot-store.ts
apps/frontend/lib/classroom/postgres-migration.test.ts
apps/frontend/lib/classroom/postgres-pilot-migration.test.ts
apps/frontend/lib/classroom/postgres-pilot-store.test.ts
apps/frontend/lib/classroom/postgres-pilot-store.ts
apps/frontend/lib/classroom/store.test.ts
apps/frontend/lib/classroom/store.ts
apps/frontend/lib/classroom/test-fixtures.ts
apps/frontend/lib/demo-access/access-route.ts
apps/frontend/lib/demo-access/guard.ts
apps/frontend/lib/demo-access/server.test.ts
apps/frontend/lib/demo-access/server.ts
apps/frontend/lib/exercise/__fixtures__/image-normalization-fixtures.ts
[339 more files omitted for size]
```

### Dependencies

- apps/frontend/package.json: @axe-core/playwright@4.12.1, @playwright/test@1.61.1, @testing-library/jest-dom@6.9.1, @testing-library/react@16.3.2, @types/node@22.17.0, @types/pg@8.20.0, @types/react@19.2.17, @types/react-dom@19.2.3, eslint@9.39.3, eslint-config-next@16.2.10, jsdom@29.1.1, next@16.2.10, openai@6.46.0, pg@8.22.0, pg-mem@3.0.14, react@19.2.7, react-dom@19.2.7, sharp@0.35.3, typescript@5.9.3, vitest@4.1.10, zod@4.4.3

### Recent commits (newest first)

- Clarify Devpost submission status
- Add noncommercial licensing and refresh submission docs
- Enable bounded AI-guided geometry movement
- Merge pull request #4 from VincentNinIA/codex/realtime-geogebra-preemption
- fix realtime preemption on geometry input
- docs: publish the README in English
- docs: refresh README for the active Compass coach
- feat: make Compass an active GeoGebra coach
- docs: close the direct hackathon demo
- feat: simplify the public Varignon demo
- docs: close the production test classroom slice
- feat: launch assigned Varignon from a test class
- feat: deliver targeted Varignon assignments
- docs: record public demo access
- fix: open public demo without access code
- feat: deliver T25 classroom pilot
- feat: close T25 classroom data contracts
- docs: close protected T22 publication
- fix: restore teacher studio contrast before release
- feat: protect demo access and API budget

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

### AGENTS.md

```markdown
# GeoTutor - Agent entrypoint

Le dépôt suit le workflow Builder Nin-IA décrit dans `prompts/AGENTS.md`.

Avant toute modification, lire dans cet ordre :

1. `agents/SPEC.md`
2. `agents/CONTRACT.md`
3. `prompts/AGENTS.md`
4. `agents/DECISIONS.md`
5. `agents/TODO_NEXT.md`
6. `docs/ARCHITECTURE.md` si la tranche touche la structure ou le runtime

Ne travailler que sur la tranche contractualisée. Ne pas créer de `QA_REPORT.md` côté Builder et ne créer `HANDOFF.md` que pour une reprise réellement nécessaire.


```

### THIRD_PARTY_NOTICES.md

```markdown
# Third-party notices

The PolyForm Noncommercial License in [`LICENSE`](LICENSE) applies only to
source code and documentation authored for Compass. It does not replace or
modify the licenses of third-party software, services, assets, or trademarks.

## GeoGebra

> **Made with GeoGebra®**

Compass loads the official GeoGebra web application for its specialist geometry
workspace. GeoGebra is separate third-party software and is not distributed or
relicensed under the Compass license.

According to the official GeoGebra licensing page:

- GeoGebra installers, web services, materials, and the complete GeoGebra
  product are governed by the
  [GeoGebra Non-Commercial License](https://www.geogebra.org/license).
- GeoGebra source code is licensed under the
  [EUPL v1.2](https://interoperable-europe.ec.europa.eu/collection/eupl/eupl-text-eupl-12).
- GeoGebra language files, documentation, and user-interface image and style
  files are licensed under
  [CC BY-NC-SA 4.0 or later](https://creativecommons.org/licenses/by-nc-sa/4.0/).

The complete GeoGebra product may be used under its non-commercial terms only.
A commercial use requires a separate License and Collaboration Agreement from
GeoGebra GmbH; contact [office@geogebra.org](mailto:office@geogebra.org).

GeoGebra® and related materials remain the property of GeoGebra GmbH and their
respective licensors. No endorsement of Compass by GeoGebra is implied.

## Other dependencies

Compass also depends on third-party packages whose own license notices and
package metadata remain controlling for those packages. Nothing in the Compass
license grants additional rights to those dependencies.

```

### package.json

```
{
  "name": "geotutor",
  "version": "0.1.0",
  "private": true,
  "packageManager": "pnpm@10.6.3",
  "engines": {
    "node": ">=22.17.0 <23",
    "pnpm": "10.6.3"
  },
  "scripts": {
    "dev": "set -a; [ ! -f .env ] || . ./.env; set +a; pnpm --dir apps/frontend dev",
    "lint": "pnpm --dir apps/frontend lint",
    "typecheck": "pnpm --dir apps/frontend typecheck",
    "test": "pnpm --dir apps/frontend test --run",
    "build": "pnpm --dir apps/frontend build",
    "test:docs:t0": "node scripts/validate-t0-docs.mjs",
    "test:e2e:t0": "pnpm --dir apps/frontend test:e2e:t0",
    "test:e2e:t0:live": "pnpm --dir apps/frontend test:e2e:t0:live",
    "gate:t6:identity": "set -a; [ ! -f .env ] || . ./.env; set +a; pnpm --dir apps/frontend gate:t6:identity",
    "gate:t6:live": "set -a; [ ! -f .env ] || . ./.env; set +a; pnpm --dir apps/frontend gate:t6:live"
  }
}

```

### apps/frontend/package.json

```
{
  "name": "@geotutor/frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "start:https": "node scripts/serve-https.mjs",
    "lint": "eslint .",
    "typecheck": "tsc --noEmit",
    "test": "vitest",
    "test:e2e": "next build && playwright test",
    "test:e2e:https": "playwright test --config playwright.https.config.ts e2e/t6-presentation.spec.ts",
    "test:e2e:t0": "next build && playwright test --grep-invert @live",
    "test:e2e:t0:live": "set -a; [ ! -f ../../.env ] || . ../../.env; set +a; next build && T0_LIVE=1 playwright test --grep @live",
    "test:e2e:t24": "playwright test --config playwright.t24.config.ts",
    "test:e2e:t25": "playwright test --config playwright.t25.config.ts",
    "gate:t6:identity": "node scripts/t6-live-gate.mjs --identity",
    "gate:t6:live": "node scripts/t6-live-gate.mjs",
    "gate:t22:golden": "node scripts/t22-golden-gate.mjs",
    "gate:t22:live": "set -a; [ ! -f ../../.env ] || . ../../.env; set +a; T22_LIVE=1 playwright test --config playwright.t22-live.config.ts e2e/t22-realtime-live.spec.ts",
    "demo:access:generate": "node scripts/t24-generate-demo-access.mjs",
    "classroom:secrets:generate": "node scripts/t25-generate-classroom-secrets.mjs",
    "classroom:demo:seed": "node scripts/t25-seed-demo-class.mjs",
    "test:gate:t22": "vitest run scripts/t22-golden-gate.test.mjs",
    "test:eval:t3": "vitest run --config vitest.eval.config.ts"
  },
  "dependencies": {
    "next": "16.2.10",
    "openai": "6.46.0",
    "pg": "8.22.0",
    "react": "19.2.7",
    "react-dom": "19.2.7",
    "sharp": "0.35.3",
    "zod": "4.4.3"
  },
  "devDependencies": {
    "@axe-core/playwright": "4.12.1",
    "@playwright/test": "1.61.1",
    "@testing-library/jest-dom": "6.9.1",
    "@testing-library/react": "16.3.2",
    "@types/node": "22.17.0",
    "@types/pg": "8.20.0",
    "@types/react": "19.2.17",
    "@types/react-dom": "19.2.3",
    "eslint": "9.39.3",
    "eslint-config-next": "16.2.10",
    "jsdom": "29.1.1",
    "pg-mem": "3.0.14",
    "typescript": "5.9.3",
    "vitest": "4.1.10"
  }
}

```

### apps/frontend/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { cookies } from "next/headers";
import type { ReactNode } from "react";

import "./globals.css";
import {
  DemoAccessGate,
  DemoSessionControl,
} from "@/components/demo-access-gate";
import { LanguageProvider } from "@/components/language-provider";
import {
  DEMO_SESSION_COOKIE_NAME,
  readDemoProtectionConfig,
  verifyDemoSession,
} from "@/lib/demo-access/server";

export const metadata: Metadata = {
  title: "Compass — Explore Varignon in GeoGebra",
  description:
    "A guided bilingual geometry investigation: build, explore and prove Varignon’s theorem directly in GeoGebra.",
};

export default async function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
  const protection = readDemoProtectionConfig();
  let content = children;
  if (protection.status === "unavailable") {
    content = <DemoAccessGate unavailable />;
  } else if (protection.status === "enabled") {
    const session = verifyDemoSession(
      (await cookies()).get(DEMO_SESSION_COOKIE_NAME)?.value,
      protection,
    );
    content =
      session.status === "authorized" ? (
        <>
          {children}
          <DemoSessionControl />
        </>
      ) : (
        <DemoAccessGate />
      );
  }
  return (
    <html lang="en">
      <body>
        <LanguageProvider>{content}</LanguageProvider>
      </body>
    </html>
  );
}

```

### apps/frontend/lib/classroom/index.ts

```typescript
export * from "./access";
export * from "./activity-catalog";
export * from "./contracts";
export * from "./data-policy";
export * from "./lifecycle";
export * from "./migration";
export * from "./join-code";
export * from "./pilot-runtime";
export * from "./pilot-http";
export * from "./pilot-service";
export * from "./pilot-session";
export * from "./pilot-store";
export * from "./postgres-pilot-store";
export * from "./store";

```

### apps/frontend/app/page.tsx

```typescript
"use client";

import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";

import {
  TutorWorkspace,
  type TutorWorkspaceScreen,
} from "@/components/tutor-workspace";
import { useLanguage } from "@/components/language-provider";
import { CompassMascot, MascotProvider } from "@/components/compass-mascot";
import { TeacherExerciseLibrary } from "@/components/teacher-exercise-library";
import { TeacherWorkspace } from "@/components/teacher-workspace";
import { GeometryPublishedWorkspace } from "@/components/geometry-published-workspace";
import { ClassroomJoin } from "@/components/classroom-join";
import {
  parseTeacherExercisePublication,
  type TeacherExercisePublication,
} from "@/lib/teacher/exercise";
import type { LearningSessionReportV1 } from "@/lib/learning/session-report";
import {
  GeometryLearningSessionReportV1,
  type GeometryLearningSessionReportV1 as GeometryLearningReport,
} from "@/lib/geometry-investigation/contracts";
import { createDemoVarignonPublicationV2 } from "@/lib/teacher/geometry-exercise";

type AppScreen =
  | "landing"
  | "teacher"
  | "library"
  | "classroom_join"
  | TutorWorkspaceScreen;

function LearningPlayground({ french }: { french: boolean }) {
  return (
    <div className="hero-playground" aria-hidden="true">
      <div className="playground-note playground-note-top">
        {french ? "quadrilatère libre" : "free quadrilateral"}
      </div>
      <svg viewBox="0 0 520 520" role="presentation">
        <path className="playground-orbit" d="M82 362C138 104 390 78 452 302" />
        <path className="playground-line" d="M112 154L416 196L348 414L92 344Z" />
        <path className="playground-guide" d="M264 175L382 305L220 379L102 249Z" />
        {[
          [112, 154, "A"],
          [416, 196, "B"],
          [348, 414, "C"],
          [92, 344, "D"],
        ].map(([cx, cy, label]) => (
          <g key={label}>
            <circle className="playground-point" cx={cx} cy={cy} r="13" />
            <text x={Number(cx) + 14} y={Number(cy) - 12}>{label}</text>
          </g>
        ))}
        {[
          [264, 175, "E"],
          [382, 305, "F"],
          [220, 379, "G"],
          [102, 249, "H"],
        ].map(([cx, cy, label]) => (
          <g key={label}>
            <circle className="playground-midpoint" cx={cx} cy={cy} r="10" />
            <text x={Number(cx) + 12} y={Number(cy) - 10}>{label}</text>
          </g>
        ))}
      </svg>
      <div className="playground-equation">
        <span>{french ? "observer" : "observe"}</span>
        <strong>→</strong>
        <span>{french ? "conjecturer" : "conjecture"}</span>
      </div>
      <div className="playground-note playground-note-bottom">
        {french ? "construis · déplace · justifie" : "build · drag · justify"}
      </div>
    </div>
  );
}

export default function Home() {
  const { language, text, toggleLanguage } = useLanguage();
  const french = language === "fr";
  const specialistGeometryMode = useSyncExternalStore(
    () => () => undefined,
    () =>
      new URLSearchParams(window.location.search).get("specialist") ===
      "geometry",
    () => false,
  );
  const workspaceDemoMode = useSyncExternalStore(
    () => () => undefined,
    () => ["geogebra", "gamification"].includes(
      new URLSearchParams(window.location.search).get("demo") ?? "",
    ),
    () => false,
  );
  const entryScreen = useSyncExternalStore<AppScreen | undefined>(
    () => () => undefined,
    () => {
      const entry = new URLSearchParams(window.location.search).get("entry");
      if (entry === "upload") return "upload";
      if (entry === "classroom") return "classroom_join";
      return undefined;
    },
    () => undefined,
  );
  const [screen, setScreen] = useState<AppScreen>("landing");
  const [entryDismissed, setEntryDismissed] = useState(false);
  const [assignedExercise, setAssignedExercise] =
    useState<TeacherExercisePublication>();
  const [assignedFromClass, setAssignedFromClass] = useState(false);
  const [localPublications, setLocalPublications] = useState<
    readonly TeacherExercisePublication[]
  >([]);
  const [learningReports, setLearningReports] = useState<
    readonly LearningSessionReportV1[]
  >([]);
  const [geometryLearningReports, setGeometryLearningReports] = useState<
    readonly GeometryLearningReport[]
  >([]);
  const visibleScreen = workspaceDemoMode
    ? "work"
    : !entryDismissed && screen === "landing" && entryScreen
      ? entryScreen
      : screen;
  const mainRef = useRef<HTMLElement>(null);
  const hasMountedScreenRef = useRef(false);

  useEffect(() => {
    if (!hasMountedScreenRef.current) {
      hasMountedScreenRef.current = true;
      return;
    }
    const frame = window.requestAnimationFrame(() => {
      if (!window.navigator.userAgent.includes("jsdom")) {
        window.scrollTo({ top: 0, left: 0, behavior: "auto" });
      }
      mainRef.current
        ?.querySelector<HTMLElement>("[data-screen-title]")
        ?.focus();
    });
    return () => window.cancelAnimationFrame(frame);
  }, [visibleScreen]);

  const rememberPublication = useCallback(
    (publication: TeacherExercisePublication) => {
      setLocalPublications((current) => [
        publication,
        ...current.filter((exercise) => exercise.id !== publication.id),
      ]);
    },
    [],
  );

  const rememberGeometryLearningReport = useCallback(
    (report: GeometryLearningReport) => {
      setGeometryLearningReports((current) =>
        [
          report,
          ...current.filter((entry) => entry.exerciseId !== report.exerciseId),
        ]
          .sort((left, right) => right.updatedAt - left.updatedAt)
          .slice(0, 8),
      );
    },
    [],
  );

  const handleGeometryLearningReport = useCallback(
    (report: GeometryLearningReport) => {
      rememberGeometryLearningReport(report);
      if (typeof BroadcastChannel === "undefined") return;
      const channel = new BroadcastChannel(
  
[truncated — 14960 more characters]
```

### apps/frontend/lib/geometry-investigation/index.ts

```typescript
export * from "./classifier";
export * from "./checkpoint-v2";
export * from "./actions";
export * from "./action-gateway";
export * from "./action-runtime";
export * from "./authority";
export * from "./consent";
export * from "./ui-effects";
export * from "./visual-guidance";
export * from "./contracts";
export * from "./dependencies";
export * from "./engine";
export * from "./evidence-store";
export * from "./numeric";
export * from "./privileged-consent";
export * from "./runtime";
export * from "./replay";
export * from "./stabilizer";
export * from "./varignon";
export * from "./world";

```

### apps/frontend/lib/demo-access/server.ts

```typescript
import {
  createHash,
  createHmac,
  randomBytes,
  scrypt,
  timingSafeEqual,
} from "node:crypto";

export const DEMO_SESSION_COOKIE_NAME = "compass_demo_session";
export const DEMO_ACCESS_MAX_BODY_BYTES = 512;
export const DEMO_ACCESS_MAX_CODE_LENGTH = 128;
export const DEMO_SESSION_DEFAULT_TTL_SECONDS = 4 * 60 * 60;
export const DEMO_SESSION_MIN_TTL_SECONDS = 15 * 60;
export const DEMO_SESSION_MAX_TTL_SECONDS = 8 * 60 * 60;

const ACCESS_HASH_VERSION = "scrypt-v1";
const SESSION_TOKEN_VERSION = "v1";
const ACCESS_HASH_BYTES = 32;
const SCRYPT_OPTIONS = {
  N: 16_384,
  r: 8,
  p: 1,
  maxmem: 32 * 1024 * 1024,
} as const;

type Environment = Readonly<Record<string, string | undefined>>;

export type DemoProtectionConfig =
  | { status: "disabled" }
  | { status: "unavailable" }
  | {
      status: "enabled";
      accessHash: string;
      sessionSecret: string;
      sessionTtlSeconds: number;
    };

export type DemoSessionInspection =
  | { status: "disabled" }
  | { status: "unavailable" }
  | { status: "authorized"; sessionId: string; expiresAt: number }
  | { status: "required" };

function base64Url(bytes: Uint8Array): string {
  return Buffer.from(bytes).toString("base64url");
}

function decodeBase64Url(value: string): Buffer | null {
  if (!/^[A-Za-z0-9_-]+$/.test(value)) return null;
  try {
    return Buffer.from(value, "base64url");
  } catch {
    return null;
  }
}

function equalBytes(left: Uint8Array, right: Uint8Array): boolean {
  if (left.byteLength !== right.byteLength) return false;
  return timingSafeEqual(Buffer.from(left), Buffer.from(right));
}

function deriveAccessHash(code: string, salt: Uint8Array): Promise<Buffer> {
  return new Promise((resolve, reject) => {
    scrypt(
      code,
      salt,
      ACCESS_HASH_BYTES,
      SCRYPT_OPTIONS,
      (error, derivedKey) => {
        if (error) reject(error);
        else resolve(derivedKey);
      },
    );
  });
}

function parseAccessHash(
  accessHash: string,
): { salt: Buffer; digest: Buffer } | null {
  const [version, encodedSalt, encodedDigest, extra] = accessHash.split("$");
  if (version !== ACCESS_HASH_VERSION || extra !== undefined) return null;
  const salt = decodeBase64Url(encodedSalt ?? "");
  const digest = decodeBase64Url(encodedDigest ?? "");
  if (!salt || salt.byteLength < 16 || digest?.byteLength !== ACCESS_HASH_BYTES) {
    return null;
  }
  return { salt, digest };
}

function parseTtl(value: string | undefined): number {
  const parsed = Number(value);
  if (!Number.isSafeInteger(parsed)) return DEMO_SESSION_DEFAULT_TTL_SECONDS;
  return Math.min(
    DEMO_SESSION_MAX_TTL_SECONDS,
    Math.max(DEMO_SESSION_MIN_TTL_SECONDS, parsed),
  );
}

export function readDemoProtectionConfig(
  environment: Environment = process.env,
): DemoProtectionConfig {
  const required = environment.COMPASS_DEMO_PROTECTION_ENABLED === "1";
  if (!required) return { status: "disabled" };

  const accessHash = environment.COMPASS_DEMO_ACCESS_HASH?.trim() ?? "";
  const sessionSecret = environment.COMPASS_DEMO_SESSION_SECRET?.trim() ?? "";
  if (!parseAccessHash(accessHash) || sessionSecret.length < 32) {
    return { status: "unavailable" };
  }

  return {
    status: "enabled",
    accessHash,
    sessionSecret,
    sessionTtlSeconds: parseTtl(
      environment.COMPASS_DEMO_SESSION_TTL_SECONDS,
    ),
  };
}

export async function createDemoAccessHash(
  code: string,
  salt: Uint8Array = randomBytes(16),
): Promise<string> {
  if (code.length < 8 || code.length > DEMO_ACCESS_MAX_CODE_LENGTH) {
    throw new Error("demo_access_code_length_invalid");
  }
  const digest = await deriveAccessHash(code, salt);
  return `${ACCESS_HASH_VERSION}$${base64Url(salt)}$${base64Url(digest)}`;
}

export async function verifyDemoAccessCode(
  code: string,
  accessHash: string,
): Promise<boolean> {
  const parsed = parseAccessHash(accessHash);
  if (!parsed || code.length < 8 || code.length > DEMO_ACCESS_MAX_CODE_LENGTH) {
    return false;
  }
  const digest = await deriveAccessHash(code, parsed.salt);
  return equalBytes(digest, parsed.digest);
}

function accessFingerprint(accessHash: string): string {
  return createHash("sha256")
    .update(accessHash)
    .digest("base64url")
    .slice(0, 22);
}

function sessionSignature(payload: string, secret: string): string {
  return createHmac("sha256", secret).update(payload).digest("base64url");
}

export function issueDemoSession(
  config: Extract<DemoProtectionConfig, { status: "enabled" }>,
  options: { now?: number; sessionId?: string } = {},
): { token: string; sessionId: string; expiresAt: number } {
  const now = options.now ?? Date.now();
  const sessionId = options.sessionId ?? base64Url(randomBytes(18));
  const expiresAt = Math.floor(now / 1_000) + config.sessionTtlSeconds;
  const payload = [
    SESSION_TOKEN_VERSION,
    String(expiresAt),
    sessionId,
    accessFingerprint(config.accessHash),
  ].join(".");
  return {
    token: `${payload}.${sessionSignature(payload, config.sessionSecret)}`,
    sessionId,
    expiresAt,
  };
}

export function verifyDemoSession(
  token: string | undefined,
  config: Extract<DemoProtectionConfig, { status: "enabled" }>,
  now = Date.now(),
): Extract<DemoSessionInspection, { status: "authorized" | "required" }> {
  if (!token || token.length > 512) return { status: "required" };
  const [version, rawExpiresAt, sessionId, fingerprint, signature, extra] =
    token.split(".");
  const expiresAt = Number(rawExpiresAt);
  if (
    version !== SESSION_TOKEN_VERSION ||
    extra !== undefined ||
    !Number.isSafeInteger(expiresAt) ||
    expiresAt <= Math.floor(now / 1_000) ||
    !/^[A-Za-z0-9_-]{16,64}$/.test(sessionId ?? "") ||
    fingerprint !== accessFingerprint(config.accessHash) ||
    !signature
  ) {
    return { status: "required" };
  }

  const payload = [version, rawExpiresAt, sessionId, fingerprint].join(".");
  const expected = sessionSignature(payload, config.sessionSecret);
  if (!equalBytes(Buffer.
[truncated — 1725 more characters]
```

### apps/frontend/app/api/realtime/session/route.ts

```typescript
import { createRealtimeSessionHandler } from "@/lib/realtime/session-route";
import { withDemoAccessProtection } from "@/lib/demo-access/guard";

export const runtime = "nodejs";

export const POST = withDemoAccessProtection(createRealtimeSessionHandler());

```

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