Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4843734
fix: add single-function Classroom dispatcher
MajorIncident Oct 7, 2026
ccc83e1
fix: add consolidated Classroom Vercel entrypoint
MajorIncident Oct 7, 2026
295dc21
test: cover consolidated Classroom dispatcher
MajorIncident Oct 7, 2026
b5512ea
fix: route Classroom APIs through one Vercel function
MajorIncident Oct 7, 2026
622ed52
fix: remove redundant Classroom route wrapper
MajorIncident Oct 7, 2026
d4dda3c
fix: remove redundant Classroom route wrapper
MajorIncident Oct 7, 2026
04c8ebf
fix: remove redundant Classroom route wrapper
MajorIncident Oct 7, 2026
cf4ad8d
fix: remove redundant Classroom route wrapper
MajorIncident Oct 7, 2026
b699e38
fix: remove redundant Classroom route wrapper
MajorIncident Oct 7, 2026
4e40925
fix: remove redundant Classroom route wrapper
MajorIncident Oct 7, 2026
55c5b88
fix: remove redundant Classroom route wrapper
MajorIncident Oct 7, 2026
32a9efe
fix: remove redundant Classroom route wrapper
MajorIncident Oct 7, 2026
c3cbe56
fix: remove redundant Classroom route wrapper
MajorIncident Oct 7, 2026
2174cab
fix: remove redundant Classroom route wrapper
MajorIncident Oct 7, 2026
cb3393b
fix: remove redundant Classroom route wrapper
MajorIncident Oct 7, 2026
48417f5
fix: remove redundant Classroom route wrapper
MajorIncident Oct 7, 2026
4449175
fix: remove redundant Classroom route wrapper
MajorIncident Oct 7, 2026
ec1281c
fix: remove redundant Classroom route wrapper
MajorIncident Oct 7, 2026
624defd
fix: remove redundant Classroom route wrapper
MajorIncident Oct 7, 2026
f8a8512
fix: remove redundant Classroom route wrapper
MajorIncident Oct 7, 2026
64b2be3
build: guard Vercel function budget
MajorIncident Oct 7, 2026
51b1b16
test: lock Classroom rewrites and function budget
MajorIncident Oct 7, 2026
7546f92
build: enforce Vercel function budget in quality
MajorIncident Oct 7, 2026
c0eb724
build: verify function budget before Vercel deploy
MajorIncident Oct 7, 2026
34294de
docs: document consolidated Classroom deployment
MajorIncident Oct 7, 2026
3cce1a9
docs: govern Vercel function budget
MajorIncident Oct 7, 2026
d470126
docs: guard single-function Classroom routing
MajorIncident Oct 7, 2026
876b85a
docs: define Classroom deployment routing invariant
MajorIncident Oct 7, 2026
7cd33fd
docs: add Classroom production publish gate
MajorIncident Oct 7, 2026
0a19960
docs: checkpoint Vercel publish recovery
MajorIncident Oct 7, 2026
a8ec112
docs: gate production case work on publish recovery
MajorIncident Oct 7, 2026
da73f6f
fix: remove unused budget-script import
MajorIncident Oct 7, 2026
9094a3f
build: align protected boundary with function preflight
MajorIncident Oct 7, 2026
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ AI contributors should run the following commands (or manual preview) whenever t
| `npm run verify:persistence` | When adding or editing inputs/captions that should survive reloads. | Confirms new controls tie into `src/appState.js` and `src/storage.js`, prompting template/state updates so saves/loads remain lossless. |
| `npm test` | Before committing or when adding new suites. | Runs the full test matrix (DOM + unit) so CI sees the same state you validated locally. |
| `npm run verify:protected-cases` | After any template/classroom/deployment boundary change. | Proves protected Case Study IDs/names are absent from public browser runtime assets and that authored `templates/*.json` remains excluded from Vercel deployment. |
| `npm run verify:vercel-functions` | After adding or moving anything under `api/`, or changing Vercel routing. | Conservatively counts deployable `api/*.js` entrypoints and fails above the Hobby-plan budget of 12. Classroom public URLs are intentionally multiplexed through one function. |
| `npm run quality` | Before marking any pull request ready. | Canonical repository gate: lockfile, repo doctor, domain guards, lint, generated-file freshness, protected-case boundary, storage docs, and the full test suite. See [`docs/REPOSITORY-OPERATIONS.md`](docs/REPOSITORY-OPERATIONS.md). |
| `npm run update:storage-docs` / `npm run check:storage-docs` | Run `update` whenever you alter persisted schema, then `check` before pushing. | Keeps [`docs/storage-schema.md`](docs/storage-schema.md) and [`docs/storage-schema.appendix.md`](docs/storage-schema.appendix.md) synced with new keys or shapes. |

Expand Down Expand Up @@ -127,6 +128,8 @@ The database stores only SHA-256 capability hashes. `CLASS_EXPIRY_DAYS` optional

See `docs/classroom-api.md` for the endpoint/capability matrix and `SECURITY.md` for the security boundary. Instructor observation is implemented through the separate GET-only `/api/classes/observe` path, authorized by the Instructor class capability after class/workspace ownership is proven. It never resolves through the editable workspace-alias path.

The public `/api/classes/**` URL contract is preserved while Vercel packaging is consolidated: `vercel.json` rewrites those paths into the single `api/classroom.js` Serverless Function, which delegates through `api/_classroomRouter.js` to the same class/coaching/protected-case/exercise handlers. The rewrite marker is routing only and grants no authority. Do not restore one Vercel function file per Classroom endpoint; `npm run verify:vercel-functions` protects the deployment budget.

#313 staged simulation is implemented end-to-end on the active classroom branch. Protected Case Studies may carry an explicit server-only `simulation` definition; additive Neon tables persist class exercise lifecycle, optional releases, workspace readiness, and immutable debrief checkpoints. Running exercises pin the staged-definition version plus a definition fingerprint and inherit the owning class expiry. Instructor APIs/UI support staged selection, Start/Pause/Resume, controlled evidence release, debrief, explicit edit freeze/unfreeze, checkpoint inspection, Advance, and Complete. Student staged reads use the stable class-session capability, expose only current/cumulative Student-safe material, support Ready/Resume Working, and project frozen debrief Intake read-only while the server enforces writes with HTTP 423. These tables and clients do **not** duplicate live Intake state: collaboration workspaces remain the only live snapshot/revision source of truth, checkpoint snapshots are historical read-only evidence, and Classroom exercise/session state stays outside Intake persistence/export/summary. Production official Case Study stage content remains intentionally unauthored until authoritative material is supplied.

### Conflict recovery and two-window testing
Expand Down Expand Up @@ -188,12 +191,13 @@ Need to know which module owns a given storage field? Jump to the [Storage-to-Mo
- `npm run dev` and local `npm run build` still regenerate both manifests for maintainers. Commit the authored JSON and both generated manifests together.

### Vercel deployment
- Production Vercel builds execute `npm run verify:protected-cases && npm run build:vercel-public` (see `vercel.json`). They **do not regenerate manifests**, because authored `templates/*.json` source files are deliberately excluded from the deployment upload.
- Production Vercel builds execute `npm run verify:vercel-functions && npm run verify:protected-cases && npm run build:vercel-public` (see `vercel.json`). They **do not regenerate manifests**, because authored `templates/*.json` source files are deliberately excluded from the deployment upload.
- `scripts/build-vercel-public.mjs` creates the only public static surface under `dist/`: `index.html`, `main.js`, `styles.css`, browser JavaScript under `src/` and `components/`, plus the explicitly public `docs/eula.md` linked from the footer. All other docs, tests, build scripts, authoring JSON, AGENTS files, and repository metadata are not copied.
- `vercel.json` must keep `outputDirectory: "dist"`. The former repository-root output exposed internal files such as `/docs/classroom-workstream.md` and `/scripts/build-templates-manifest.mjs`.
- Generated manifests are committed artifacts. GitHub CI remains responsible for running `npm run build:templates` / `check:templates` and proving they match authored JSON; `npm run quality` also builds/verifies the same minimal `dist/` bundle.
- `.vercelignore` prevents raw authored Case Study JSON from entering the Vercel source upload, while the `dist/` build prevents all non-runtime repository files from becoming public static assets.
- Vercel Git deployments are deny-by-default for ordinary branches. `main` deploys automatically; short-lived `verify/**` branches are the explicit preview path for security/E2E checks. `vercel.json` also owns the ignored-build decision so those two allowed branch classes proceed even if the project dashboard has an older Ignored Build Step configured. This prevents AI checkpoint commits from exhausting Vercel build capacity while preserving GitHub CI on every PR commit.
- The linked Vercel project currently runs on the Hobby plan, which permits at most 12 Serverless Functions per deployment. Classroom routes therefore share one deployment entrypoint. Server helpers stay in underscore-prefixed `api/_*.js` modules, and collaboration workspace endpoints remain separate.
- When modifying the manifest workflow, public bundle, or required quality gates, update both this README and `vercel.json` so the documented steps mirror the actual hosted build.
- Dependency changes must be installed through the normal npm registry so npm generates the complete lockfile. Run `npm run verify:lockfile` before committing and `npm ci` from a clean dependency tree; the offline guard catches missing resolved root-package entries before CI reaches its clean install.

Expand Down
2 changes: 2 additions & 0 deletions api/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ These rules apply to all server-only modules below `api/`.

- Never return or log database connection values, raw capability hashes, authorization headers, or production Intake snapshots.
- Keep route files thin and inject repositories into handlers for deterministic tests.
- Vercel Classroom deployment uses one `api/classroom.js` entrypoint plus `api/_classroomRouter.js`; preserve public `/api/classes/**` URLs through `vercel.json` rewrites instead of creating new `api/classes/**/*.js` function wrappers. Routing never replaces handler authorization.
- Keep the conservative deployable-function count at or below 12 and preserve `npm run verify:vercel-functions` in both `quality` and the Vercel build command.
- Schema changes must remain additive/idempotent and be documented in the README plus the owning architecture document.
- Validate method, authorization, identifiers, names, and payload size before expensive or mutating work.
- Responses containing capabilities or private Intake data use `Cache-Control: no-store` and `Referrer-Policy: no-referrer`.
Expand Down
100 changes: 100 additions & 0 deletions api/_classroomRouter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* @module api/classroomRouter
* @description
* Server-only dispatcher that lets Vercel expose the complete Classroom API
* through one Serverless Function while preserving the existing public URLs
* via vercel.json rewrites. Routing is not authorization: every delegated
* handler retains its existing capability, method, and payload checks.
*/

import {
classAdmitHandler,
classCoachingHandler,
classHandler,
classJoinHandler,
classObserveHandler,
classParticipantsHandler,
classStudentAccessHandler,
classStudentCoachingHandler,
classStudentHandler,
classWorkspacesHandler
} from './_classroom.js';
import {
instructorCaseStudiesHandler,
studentCaseStudiesHandler
} from './_protectedCaseStudies.js';
import {
classExerciseCheckpointHandler,
classExerciseHandler,
classStudentExerciseHandler,
classStudentExerciseReadyHandler
} from './_classroomExercise.js';

export const CLASSROOM_ROUTE_PARAM = '__classroomRoute';

export const CLASSROOM_ROUTE_IDS = Object.freeze([
'root',
'admit',
'join',
'participants',
'student',
'student-access',
'workspaces',
'observe',
'coaching',
'coaching-student',
'case-studies',
'case-studies-student',
'exercise',
'exercise-checkpoint',
'exercise-student',
'exercise-student-ready'
]);

function defaultHandlers() {
return {
root: classHandler(),
admit: classAdmitHandler(),
join: classJoinHandler(),
participants: classParticipantsHandler(),
student: classStudentHandler(),
'student-access': classStudentAccessHandler(),
workspaces: classWorkspacesHandler(),
observe: classObserveHandler(),
coaching: classCoachingHandler(),
'coaching-student': classStudentCoachingHandler(),
'case-studies': instructorCaseStudiesHandler(),
'case-studies-student': studentCaseStudiesHandler(),
exercise: classExerciseHandler(),
'exercise-checkpoint': classExerciseCheckpointHandler(),
'exercise-student': classStudentExerciseHandler(),
'exercise-student-ready': classStudentExerciseReadyHandler()
};
}

function requestedRoute(req) {
const value = req?.query?.[CLASSROOM_ROUTE_PARAM];
return typeof value === 'string' ? value.trim() : '';
}

function notFound(res) {
res.setHeader?.('Cache-Control', 'no-store');
res.setHeader?.('Referrer-Policy', 'no-referrer');
return res.status(404).json({ error: 'Classroom route not found.' });
}

/**
* Create the single-function Classroom route dispatcher.
*
* @param {object} [options] Dependency overrides.
* @param {Record<string, Function>} [options.handlers] Route handler map for deterministic tests.
* @returns {Function} Vercel request handler.
*/
export function createClassroomRouteDispatcher({ handlers = defaultHandlers() } = {}) {
return async (req, res) => {
const route = requestedRoute(req);
const handler = Object.hasOwn(handlers, route) ? handlers[route] : null;
if (typeof handler !== 'function') return notFound(res);
return handler(req, res);
};
}
7 changes: 0 additions & 7 deletions api/classes/admit.js

This file was deleted.

7 changes: 0 additions & 7 deletions api/classes/case-studies.js

This file was deleted.

7 changes: 0 additions & 7 deletions api/classes/case-studies/student.js

This file was deleted.

7 changes: 0 additions & 7 deletions api/classes/coaching.js

This file was deleted.

7 changes: 0 additions & 7 deletions api/classes/coaching/student.js

This file was deleted.

7 changes: 0 additions & 7 deletions api/classes/exercise.js

This file was deleted.

7 changes: 0 additions & 7 deletions api/classes/exercise/checkpoint.js

This file was deleted.

7 changes: 0 additions & 7 deletions api/classes/exercise/student.js

This file was deleted.

7 changes: 0 additions & 7 deletions api/classes/exercise/student/ready.js

This file was deleted.

7 changes: 0 additions & 7 deletions api/classes/index.js

This file was deleted.

7 changes: 0 additions & 7 deletions api/classes/join.js

This file was deleted.

7 changes: 0 additions & 7 deletions api/classes/observe.js

This file was deleted.

7 changes: 0 additions & 7 deletions api/classes/participants.js

This file was deleted.

7 changes: 0 additions & 7 deletions api/classes/student.js

This file was deleted.

7 changes: 0 additions & 7 deletions api/classes/student/access.js

This file was deleted.

7 changes: 0 additions & 7 deletions api/classes/workspaces.js

This file was deleted.

7 changes: 7 additions & 0 deletions api/classroom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @module api/classroom
* @description Single Vercel Serverless Function entrypoint for all Classroom routes.
*/
import { createClassroomRouteDispatcher } from './_classroomRouter.js';

export default createClassroomRouteDispatcher();
19 changes: 18 additions & 1 deletion docs/REPOSITORY-OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ After changing the runtime, update `.nvmrc` and `package.json#engines` together

- lockfile consistency;
- repository contract checks (`repo:doctor`);
- conservative Vercel Serverless Function budget verification (`verify:vercel-functions`);
- changed-runtime test coverage guard for `src/`, `components/`, and server `api/` code;
- summary integration guard;
- persistence integration guard;
Expand All @@ -36,11 +37,27 @@ Case Study authoring source is intentionally present in Git but intentionally ab
- `scripts/build-vercel-public.mjs` generates the **only** public static surface under `dist/`: `index.html`, `main.js`, `styles.css`, browser JavaScript under `src/` and `components/`, plus the explicitly public `docs/eula.md` linked from the app footer.
- Vercel `outputDirectory` must remain `dist`. Serving `.` is prohibited because production previously exposed internal files such as `docs/classroom-workstream.md` and `scripts/build-templates-manifest.mjs`.
- The public-bundle builder rejects all other Markdown plus JSON/MJS/AGENTS/internal files and scans all emitted text—including the EULA—for protected Case Study IDs/names.
- Production Vercel builds run `npm run verify:protected-cases && npm run build:vercel-public`; they do not regenerate manifests because authored template JSON is intentionally excluded from the upload.
- Production Vercel builds run `npm run verify:vercel-functions && npm run verify:protected-cases && npm run build:vercel-public`; they do not regenerate manifests because authored template JSON is intentionally excluded from the upload.
- GitHub CI/local authoring remains responsible for `npm run build:templates` / `npm run check:templates` freshness, and `npm run quality` also builds/verifies the same minimal `dist/` surface.

Any change to `vercel.json`, `.vercelignore`, template generation, static output layout, or protected-resource routing must re-run `npm run quality` and include a deployed HTTP check that raw `/templates/*.json` Case Study paths and internal `/docs/*` / `/scripts/*` paths are not served.

### Vercel Serverless Function budget

The linked `intake` project is intentionally compatible with Vercel Hobby's **12 Serverless Function** deployment limit. #312/#313 originally exceeded that limit when each new Classroom URL received its own thin `api/classes/**` wrapper, even though GitHub tests were green.

Repository contract after #321:

- every public `/api/classes/**` URL remains stable;
- `vercel.json` rewrites those URLs to the single `api/classroom.js` deployment function;
- `api/_classroomRouter.js` selects the existing handler; the route marker is not authentication and no handler authorization is bypassed;
- do not add standalone JavaScript route wrappers back under `api/classes/**`; add the handler to the dispatcher + rewrite map instead;
- underscore-prefixed `api/_*.js` files are server helper modules bundled into entrypoints;
- `npm run verify:vercel-functions` conservatively counts every non-underscore JavaScript file under `api/` and fails above 12;
- the same guard runs in `npm run quality` and before every Vercel production build.

A PR that changes API routing is not publish-complete until the exact merged `main` SHA has a **READY** production deployment, not merely green GitHub checks.


### Vercel Git deployment policy

Expand Down
7 changes: 7 additions & 0 deletions docs/classroom-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ Raw capabilities are returned only when created or rotated and are never persist

## Endpoints

### Deployment routing invariant

The public endpoint paths documented below are stable API contracts. To stay within the Vercel Hobby Serverless Function budget, `vercel.json` rewrites every `/api/classes/**` path to the single `api/classroom.js` deployment entrypoint. `api/_classroomRouter.js` then delegates to the existing handler factory for that route.

The internal `__classroomRoute` marker is **routing only**. It is not a capability, does not select a class/workspace, and does not weaken the method/authentication/authorization checks inside the delegated handler. Do not recreate one physical `api/classes/**/*.js` wrapper per public route; extend the rewrite/dispatcher mapping and keep `npm run verify:vercel-functions` green instead.


### `POST /api/classes`

Creates a class from a title and returns:
Expand Down
Loading
Loading