From bb517011be9ea2b21937113cfde3ca2d79356a01 Mon Sep 17 00:00:00 2001 From: Omkar Tripathi <38633467+triggeredcode@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:25:54 +0530 Subject: [PATCH 1/4] docs: plan durable execution rollout and migration prerequisite --- docs/execution-plane-implementation.md | 47 ++++++++++++++++++++++++++ docs/production-readiness.md | 3 ++ 2 files changed, 50 insertions(+) create mode 100644 docs/execution-plane-implementation.md diff --git a/docs/execution-plane-implementation.md b/docs/execution-plane-implementation.md new file mode 100644 index 0000000..d1e7e27 --- /dev/null +++ b/docs/execution-plane-implementation.md @@ -0,0 +1,47 @@ +# Execution plane implementation + +This is the next production-readiness project after the learning UI. It is not +permission to accept untrusted public submissions. The current request handler +still runs work inside the web process with a local Docker or synthetic backend. + +## Goal and invariants + +One accepted submission must create one durable execution job. A worker may +retry an attempt, but only one terminal result may be published. A web restart +must not lose queued work, and a worker crash must not leave a lease stuck +forever. A run must be reproducible from its recorded source hash, challenge +version, workload seed and parameters, judge policy, runtime image, and backend. + +The worker, not participant code, owns resource limits and sandbox cleanup. +Only a verified, immutable arena bundle may be used for an official run. Stub +results remain development-only and must never be presented as official scores. + +## Change order + +1. **Repair the database migration baseline.** The only committed migration + predates the current `Difficulty` and `Language` enums, `Reply`, and several + `Challenge` and `Submission` fields. Prove an empty database and an upgraded + database reach the same schema before adding a job table. Preserve existing + submissions and results; do not reset a user's database to hide drift. +2. Define versioned job input and result-provenance contracts. Inputs snapshot + the challenge settings and workload variant at acceptance time. Arena jobs + pin an arena version and manifest digest. Hash the source without embedding + duplicate source code in the job payload. +3. Commit a submission and its unique job in one database transaction. Keep + the old in-process executor behind an explicit development-only switch + during transition; no request may return success before the job commits. +4. Build a worker that atomically claims a due job with a lease and fencing + token, heartbeats while running, and reconciles expired leases. Retried + attempts cannot overwrite a newer or terminal result. +5. Resolve verified arena bundles by digest, then add a production sandbox + adapter with enforced CPU, memory, process, wall-clock, output and network + policy. Reconcile and destroy execution environments after every outcome. +6. Exercise restarts, concurrent workers, lease expiry, duplicate delivery, + malformed bundles, and sandbox cleanup in integration tests. Add queue and + failure telemetry before enabling public traffic. + +## Current slice + +The first implementation slice is the versioned input/provenance contract and +tests. It is intentionally not wired into submission acceptance until the +migration baseline is verified. No existing run changes behavior in this slice. diff --git a/docs/production-readiness.md b/docs/production-readiness.md index 2e69de5..3f5486a 100644 --- a/docs/production-readiness.md +++ b/docs/production-readiness.md @@ -173,6 +173,9 @@ This ordering borrows proven patterns without copying another product's surface: ## Recommended implementation order +The [execution-plane implementation plan](execution-plane-implementation.md) +tracks the first production-readiness project and its migration prerequisite. + 1. Define the immutable execution job and result provenance schema. 2. Publish and verify arena bundles independently of the platform image. 3. Implement the queue, worker lease, production sandbox adapter, and reconciler. From f964e9ce43965942b0657d0bf218b68f245bbddc Mon Sep 17 00:00:00 2001 From: Omkar Tripathi <38633467+triggeredcode@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:25:56 +0530 Subject: [PATCH 2/4] feat(execution): define versioned run provenance contract --- src/lib/execution/job-contract.ts | 107 ++++++++++++++++++ tests/unit/lib/execution/job-contract.test.ts | 87 ++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 src/lib/execution/job-contract.ts create mode 100644 tests/unit/lib/execution/job-contract.test.ts diff --git a/src/lib/execution/job-contract.ts b/src/lib/execution/job-contract.ts new file mode 100644 index 0000000..a91ce21 --- /dev/null +++ b/src/lib/execution/job-contract.ts @@ -0,0 +1,107 @@ +import { createHash } from "node:crypto"; +import { z } from "zod"; + +const digest = z.string().regex(/^[a-f0-9]{64}$/); +const uint32 = z.number().int().min(0).max(0xffff_ffff); +const identifier = z.string().trim().min(1).max(200); + +const variantSchema = z.object({ + blocks: z.number().int().positive(), + pagesPerBlock: z.number().int().positive(), + logicalPages: z.number().int().positive(), + hotFraction: z.number().min(0).max(1), + hotProbability: z.number().min(0).max(1), +}).strict(); + +const commonInput = z.object({ + schemaVersion: z.literal(1), + submissionId: identifier, + challengeId: identifier, + language: z.enum(["GO", "PYTHON", "C", "CPP", "JAVA", "RUST"]), + sourceSha256: digest, + judgePolicyVersion: identifier, + limits: z.object({ + timeSec: z.number().int().positive(), + memoryMiB: z.number().int().positive(), + }).strict(), + workload: z.object({ + task: identifier, + seed: uint32, + variant: variantSchema, + params: z.json(), + }).strict(), +}).strict(); + +/** A job snapshots the run settings; participant source stays in Submission. */ +export const executionJobInputV1Schema = z.discriminatedUnion("kind", [ + commonInput.extend({ kind: z.literal("legacy") }), + commonInput.extend({ + kind: z.literal("arena"), + arena: z.object({ + id: identifier, + version: identifier, + manifestDigest: digest, + }).strict(), + }), +]); + +export type ExecutionJobInputV1 = z.infer; + +/** Written by a future worker, never inferred from the browser or a stub run. */ +export const executionProvenanceV1Schema = z.object({ + schemaVersion: z.literal(1), + jobInputSha256: digest, + sourceSha256: digest, + judgePolicyVersion: identifier, + runtimeImageDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/), + backend: identifier, + workloadSeed: uint32, + arena: z.object({ + id: identifier, + version: identifier, + manifestDigest: digest, + }).strict().nullable(), +}).strict(); + +export type ExecutionProvenanceV1 = z.infer; + +export function sha256(value: string): string { + return createHash("sha256").update(value, "utf8").digest("hex"); +} + +/** Stable across JSON object key order, including nested workload parameters. */ +export function executionJobInputDigest(input: ExecutionJobInputV1): string { + const parsed = executionJobInputV1Schema.parse(input); + return sha256(canonicalJson(parsed)); +} + +/** Rejects a result that claims to belong to a different immutable run. */ +export function verifyExecutionProvenance( + input: ExecutionJobInputV1, + candidate: unknown, +): ExecutionProvenanceV1 { + const job = executionJobInputV1Schema.parse(input); + const provenance = executionProvenanceV1Schema.parse(candidate); + const expectedArena = job.kind === "arena" ? job.arena : null; + if ( + provenance.jobInputSha256 !== executionJobInputDigest(job) || + provenance.sourceSha256 !== job.sourceSha256 || + provenance.judgePolicyVersion !== job.judgePolicyVersion || + provenance.workloadSeed !== job.workload.seed || + canonicalJson(provenance.arena) !== canonicalJson(expectedArena) + ) { + throw new Error("Execution provenance does not match the accepted job"); + } + return provenance; +} + +function canonicalJson(value: unknown): string { + if (Array.isArray(value)) { + return `[${value.map(canonicalJson).join(",")}]`; + } + if (value !== null && typeof value === "object") { + const object = value as Record; + return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`; + } + return JSON.stringify(value); +} diff --git a/tests/unit/lib/execution/job-contract.test.ts b/tests/unit/lib/execution/job-contract.test.ts new file mode 100644 index 0000000..df1f17d --- /dev/null +++ b/tests/unit/lib/execution/job-contract.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "vitest"; + +import { + executionJobInputDigest, + executionJobInputV1Schema, + executionProvenanceV1Schema, + sha256, + verifyExecutionProvenance, +} from "@/lib/execution/job-contract"; + +const input = { + schemaVersion: 1, + kind: "arena", + submissionId: "submission-1", + challengeId: "challenge-1", + language: "GO", + sourceSha256: sha256("package main\n"), + judgePolicyVersion: "practice-v1", + limits: { timeSec: 30, memoryMiB: 512 }, + workload: { + task: "arena:cache-pressure-easy", + seed: 42, + variant: { + blocks: 16, + pagesPerBlock: 64, + logicalPages: 768, + hotFraction: 0.2, + hotProbability: 0.8, + }, + params: { nested: { b: 2, a: 1 }, operations: 100 }, + }, + arena: { + id: "cache-pressure-easy", + version: "1.0.0", + manifestDigest: sha256("manifest"), + }, +} as const; + +describe("versioned execution contracts", () => { + it("pins an arena and hashes the source without copying source into the job", () => { + expect(executionJobInputV1Schema.parse(input)).toEqual(input); + expect(JSON.stringify(input)).not.toContain("package main"); + expect(input.sourceSha256).toBe(sha256("package main\n")); + }); + + it("rejects unpinned arenas, unexpected fields, and invalid workload seeds", () => { + const unpinned = Object.fromEntries(Object.entries(input).filter(([key]) => key !== "arena")); + expect(() => executionJobInputV1Schema.parse(unpinned)).toThrow(); + expect(() => executionJobInputV1Schema.parse({ ...input, sessionToken: "private" })).toThrow(); + expect(() => executionJobInputV1Schema.parse({ + ...input, workload: { ...input.workload, seed: -1 }, + })).toThrow(); + }); + + it("produces a stable digest for reordered nested JSON keys", () => { + const reordered = { + ...input, + workload: { + ...input.workload, + params: { operations: 100, nested: { a: 1, b: 2 } }, + }, + }; + expect(executionJobInputDigest(reordered)).toBe(executionJobInputDigest(input)); + expect(executionJobInputDigest({ ...input, sourceSha256: sha256("different") })) + .not.toBe(executionJobInputDigest(input)); + }); + + it("requires execution provenance to identify the backend and immutable image", () => { + const provenance = { + schemaVersion: 1, + jobInputSha256: executionJobInputDigest(input), + sourceSha256: input.sourceSha256, + judgePolicyVersion: input.judgePolicyVersion, + runtimeImageDigest: `sha256:${sha256("image")}`, + backend: "isolated-worker", + workloadSeed: input.workload.seed, + arena: input.arena, + }; + expect(executionProvenanceV1Schema.parse(provenance)).toEqual(provenance); + expect(verifyExecutionProvenance(input, provenance)).toEqual(provenance); + expect(() => executionProvenanceV1Schema.parse({ ...provenance, runtimeImageDigest: "latest" })).toThrow(); + expect(() => verifyExecutionProvenance(input, { ...provenance, workloadSeed: 43 })).toThrow(/does not match/); + expect(() => verifyExecutionProvenance(input, { + ...provenance, arena: { ...provenance.arena, version: "2.0.0" }, + })).toThrow(/does not match/); + }); +}); From a215bce6075768f4e18d57199f442daac33db9c0 Mon Sep 17 00:00:00 2001 From: Omkar Tripathi <38633467+triggeredcode@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:32:09 +0530 Subject: [PATCH 3/4] fix(db): align migration history with current schema --- .github/workflows/ci.yml | 28 ++++++ docs/execution-plane-implementation.md | 22 +++-- prisma.config.ts | 3 + .../migration.sql | 98 +++++++++++++++++++ 4 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 prisma/migrations/20261016183000_align_current_schema/migration.sql diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e314ad8..efde928 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,34 @@ jobs: fetch-depth: 0 - run: sh scripts/check-repository-hygiene.sh + schema-migrations: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: roughstack + POSTGRES_PASSWORD: migration_test_only + POSTGRES_DB: roughstack_migration_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U roughstack -d roughstack_migration_test" + --health-interval 3s --health-timeout 3s --health-retries 20 + env: + DATABASE_URL: postgresql://roughstack:migration_test_only@127.0.0.1:5432/roughstack_migration_test + SHADOW_DATABASE_URL: postgresql://roughstack:migration_test_only@127.0.0.1:5432/postgres + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npx prisma migrate deploy + - run: npx prisma migrate diff --from-migrations prisma/migrations --to-schema prisma/schema.prisma --exit-code + - run: npx prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma --exit-code + web: runs-on: ubuntu-latest steps: diff --git a/docs/execution-plane-implementation.md b/docs/execution-plane-implementation.md index d1e7e27..5096963 100644 --- a/docs/execution-plane-implementation.md +++ b/docs/execution-plane-implementation.md @@ -18,11 +18,15 @@ results remain development-only and must never be presented as official scores. ## Change order -1. **Repair the database migration baseline.** The only committed migration +1. **Repair the database migration baseline.** The original migration predates the current `Difficulty` and `Language` enums, `Reply`, and several - `Challenge` and `Submission` fields. Prove an empty database and an upgraded - database reach the same schema before adding a job table. Preserve existing - submissions and results; do not reset a user's database to hide drift. + `Challenge` and `Submission` fields. The follow-up migration maps old + difficulty values (`BEGINNER` to `EASY`, `INTERMEDIATE` to `MEDIUM`, and + `ADVANCED`/`EXPERT` to `HARD`), converts legacy description and interface + text to blocks, and preserves submissions. Disposable Postgres tests passed + for both populated legacy rows and a database already at the `db push` + schema; CI now replays and compares migrations on every PR. Never reset a + user's database to hide drift. 2. Define versioned job input and result-provenance contracts. Inputs snapshot the challenge settings and workload variant at acceptance time. Arena jobs pin an arena version and manifest digest. Hash the source without embedding @@ -42,6 +46,10 @@ results remain development-only and must never be presented as official scores. ## Current slice -The first implementation slice is the versioned input/provenance contract and -tests. It is intentionally not wired into submission acceptance until the -migration baseline is verified. No existing run changes behavior in this slice. +The first implementation slice repairs migration parity and defines the +versioned input/provenance contract with tests. It is intentionally not wired +into submission acceptance yet. No existing run changes behavior in this slice. + +Databases created earlier with `prisma db push` but without entries in +`_prisma_migrations` need an operator-reviewed baseline procedure before +`prisma migrate deploy`; do not run the initial migration against those tables. diff --git a/prisma.config.ts b/prisma.config.ts index f4be70a..986cd4c 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -16,5 +16,8 @@ export default defineConfig({ // do not touch the database, such as `prisma generate` during a type-check in CI, // do not fail merely because DATABASE_URL is absent. url: process.env.DATABASE_URL ?? "", + // Migration parity checks replay committed SQL into a disposable database. + // Never point this at a database that contains user or development data. + shadowDatabaseUrl: process.env.SHADOW_DATABASE_URL, }, }); diff --git a/prisma/migrations/20261016183000_align_current_schema/migration.sql b/prisma/migrations/20261016183000_align_current_schema/migration.sql new file mode 100644 index 0000000..1038c43 --- /dev/null +++ b/prisma/migrations/20261016183000_align_current_schema/migration.sql @@ -0,0 +1,98 @@ +-- Bring the original migration baseline to the current application schema. +-- The data conversions are deliberately explicit: existing challenges and +-- submissions must survive an upgrade from the original public schema. + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON t.oid = e.enumtypid + WHERE t.typname = 'Difficulty' AND e.enumlabel = 'BEGINNER' + ) THEN + CREATE TYPE "Difficulty_next" AS ENUM ('EASY', 'MEDIUM', 'HARD'); + ALTER TABLE "Challenge" ALTER COLUMN "difficulty" TYPE "Difficulty_next" + USING (CASE "difficulty"::text + WHEN 'BEGINNER' THEN 'EASY' + WHEN 'INTERMEDIATE' THEN 'MEDIUM' + ELSE 'HARD' + END)::"Difficulty_next"; + ALTER TYPE "Difficulty" RENAME TO "Difficulty_old"; + ALTER TYPE "Difficulty_next" RENAME TO "Difficulty"; + DROP TYPE "Difficulty_old"; + END IF; +END $$; + +ALTER TYPE "Language" ADD VALUE IF NOT EXISTS 'C'; +ALTER TYPE "Language" ADD VALUE IF NOT EXISTS 'CPP'; +ALTER TYPE "Language" ADD VALUE IF NOT EXISTS 'JAVA'; +ALTER TYPE "Language" ADD VALUE IF NOT EXISTS 'RUST'; + +ALTER TABLE "Challenge" + ADD COLUMN IF NOT EXISTS "blocks" JSONB, + ADD COLUMN IF NOT EXISTS "ladder" TEXT, + ADD COLUMN IF NOT EXISTS "pack" TEXT NOT NULL DEFAULT 'ssd', + ADD COLUMN IF NOT EXISTS "task" TEXT, + ADD COLUMN IF NOT EXISTS "taskParams" JSONB, + ADD COLUMN IF NOT EXISTS "tier" INTEGER NOT NULL DEFAULT 1; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'Challenge' + AND column_name = 'description' + ) THEN + EXECUTE 'UPDATE "Challenge" SET "blocks" = jsonb_build_array(' + || 'jsonb_build_object(''kind'', ''prose'', ''md'', "description")) ' + || 'WHERE "blocks" IS NULL'; + END IF; + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'Challenge' + AND column_name = 'interfaceDoc' + ) THEN + EXECUTE 'UPDATE "Challenge" SET "blocks" = "blocks" || ' + || 'jsonb_build_array(jsonb_build_object(''kind'', ''callout'', ' + || '''tone'', ''note'', ''title'', ''Interface'', ''md'', "interfaceDoc")) ' + || 'WHERE "interfaceDoc" IS NOT NULL AND "blocks" IS NOT NULL'; + END IF; +END $$; + +-- Earlier challenges used their slug as the execution task. Keep the slug as +-- a conservative ladder key until an editor assigns a richer progression. +UPDATE "Challenge" SET "task" = "slug" WHERE "task" IS NULL; +UPDATE "Challenge" SET "ladder" = "slug" WHERE "ladder" IS NULL; +ALTER TABLE "Challenge" + ALTER COLUMN "blocks" SET NOT NULL, + ALTER COLUMN "task" SET NOT NULL, + ALTER COLUMN "ladder" SET NOT NULL, + DROP COLUMN IF EXISTS "description", + DROP COLUMN IF EXISTS "interfaceDoc"; + +ALTER TABLE "Submission" ADD COLUMN IF NOT EXISTS "sessionId" TEXT; + +CREATE TABLE IF NOT EXISTS "Reply" ( + "id" TEXT NOT NULL, + "challengeId" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "authorLabel" TEXT NOT NULL, + "body" TEXT NOT NULL, + "isSpoiler" BOOLEAN NOT NULL DEFAULT false, + "isHidden" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Reply_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX IF NOT EXISTS "Challenge_ladder_tier_idx" ON "Challenge"("ladder", "tier"); +CREATE INDEX IF NOT EXISTS "Submission_sessionId_createdAt_idx" ON "Submission"("sessionId", "createdAt"); +CREATE INDEX IF NOT EXISTS "Reply_challengeId_createdAt_idx" ON "Reply"("challengeId", "createdAt"); +CREATE INDEX IF NOT EXISTS "Reply_sessionId_createdAt_idx" ON "Reply"("sessionId", "createdAt"); + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'Reply_challengeId_fkey') THEN + ALTER TABLE "Reply" ADD CONSTRAINT "Reply_challengeId_fkey" + FOREIGN KEY ("challengeId") REFERENCES "Challenge"("id") + ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END $$; From b1096c868ce73e09bf1f2e50d37d8bde064db936 Mon Sep 17 00:00:00 2001 From: Omkar Tripathi <38633467+triggeredcode@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:02:15 +0530 Subject: [PATCH 4/4] docs: separate project priorities from hosted readiness --- docs/execution-plane-implementation.md | 12 ++++-- docs/production-readiness.md | 54 +++++++++++++++++++------- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/docs/execution-plane-implementation.md b/docs/execution-plane-implementation.md index 5096963..17395ac 100644 --- a/docs/execution-plane-implementation.md +++ b/docs/execution-plane-implementation.md @@ -1,8 +1,14 @@ # Execution plane implementation -This is the next production-readiness project after the learning UI. It is not -permission to accept untrusted public submissions. The current request handler -still runs work inside the web process with a local Docker or synthetic backend. +This is an optional implementation plan for an instance that accepts untrusted +public submissions, not a prerequisite for releasing the open-source project. +The migration repair and versioned execution contracts in the current slice +also improve reproducibility for local and self-hosted use. Do not start the +queue, worker, or hosted sandbox work without a concrete deployment need and a +written estimate of operational time, recurring cost, and simpler alternatives. +This plan is not permission to accept untrusted public submissions. The current +request handler still runs work inside the web process with a local Docker or +synthetic backend. ## Goal and invariants diff --git a/docs/production-readiness.md b/docs/production-readiness.md index 3f5486a..2ade2ec 100644 --- a/docs/production-readiness.md +++ b/docs/production-readiness.md @@ -1,10 +1,30 @@ -# Production readiness roadmap - -Roughstack has a coherent learning experience, a deterministic arena contract, -and a strong local development path. It is not ready to accept untrusted public -traffic yet. The work below is ordered by dependency and launch risk. - -## Current product boundary +# Project roadmap and optional hosted readiness + +Roughstack is an open-source project, not a hosted product by default. Its first +promise is that contributors can understand, run, test, and extend the learning +experience without buying infrastructure or maintaining a public service. +Hosting an instance that accepts untrusted public submissions is a separate +decision. The hosted-readiness work below is not a prerequisite for releasing +the project or for trusted local development. + +## Default project priorities + +1. Make a clean checkout reproducible: documented setup, versioned migrations, + challenge packages that do not require an undocumented sibling checkout, + and CI that checks the same path contributors use. +2. Keep challenge statements, reference solutions, test cases, and local run + results correct and reviewable. Label synthetic results as development-only. +3. Document the trust boundary of local code execution. Never claim that a + local Docker runner safely accepts code from arbitrary internet users. +4. Make contributions sustainable with clear repository ownership, small + reviewable changes, tests, and release notes. + +Before adding a paid service or a substantial workstream, record the concrete +problem, the cheapest workable alternative, expected recurring cost and +maintainer time, and a measurable criterion for adopting it. Prefer local or +self-hosted components until an actual deployment need justifies more. + +## Current implementation boundary The browser experience is functional: visitors can browse challenges, read the educational material, edit code, submit a solution, inspect results, discuss an @@ -25,7 +45,7 @@ The current server remains a development deployment: - The application has no production observability contract, incident signals, or service-level objectives. -## P0: required before a public launch +## Optional hosted track: required before accepting untrusted public submissions ### 1. Build the durable execution plane @@ -115,7 +135,7 @@ Exit criteria: - Dependency, image, and secret scanning run in CI. - A release has a software bill of materials and signed artifacts. -## P1: required for a strong learning product +## Optional learning-experience improvements ### Guided progression @@ -145,16 +165,18 @@ and explicit completion state. Complete keyboard and screen-reader audits for the editor, split panes, diagrams, tables, and result visualizations. Test reduced motion, zoom, narrow screens, and high-contrast modes. -## P2: growth and ecosystem +## Optional ecosystem ideas - Public profiles with opt-in visibility and verified achievements. - Organization classrooms, private cohorts, and assignment progress. - A documented arena authoring kit with local preview and contract validation. - Challenge versioning, deprecation, moderation, and an editorial review queue. -- Product analytics based on explicit events and a documented retention policy. -- Billing only after execution cost, abuse, refund, and quota models are known. +- Privacy-conscious usage measurement only if it answers a demonstrated project + question; document retention before collecting events. +- Billing is out of scope for the open-source project. Revisit only if a + separately operated hosted service is deliberately proposed. -## Product patterns reviewed +## Learning-platform patterns reviewed This ordering borrows proven patterns without copying another product's surface: @@ -171,10 +193,12 @@ This ordering borrows proven patterns without copying another product's surface: show the value of grouped test cases, submission analysis, code replay, progress views, filters, profiles, security settings, and focus modes at scale. -## Recommended implementation order +## Order if the optional hosted track is chosen The [execution-plane implementation plan](execution-plane-implementation.md) -tracks the first production-readiness project and its migration prerequisite. +tracks one possible hosted-readiness path and its migration prerequisite. The +migration repair and versioned execution contracts are useful independently; +the queue, worker, and public sandbox are not the default next milestone. 1. Define the immutable execution job and result provenance schema. 2. Publish and verify arena bundles independently of the platform image.