From 89f6e98a6c0dd56134c9e078fbd4d05bd554b9a6 Mon Sep 17 00:00:00 2001 From: Joey Spiker Date: Sat, 9 May 2026 15:21:12 -0400 Subject: [PATCH] updated readme --- README.md | 224 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 191 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index cbd2526..e229a36 100644 --- a/README.md +++ b/README.md @@ -276,8 +276,9 @@ The same Blockly and Rete editors available at `/tools/(tools:blockly)` and `/to SpikerSoft's flagship coding-education tools run three parallel tracks: - **C# Playground** — 95-lesson curriculum graded by **Roslyn** server-side and **.NET WebAssembly** in the browser. -- **Python Playground** — ~75-lesson curriculum graded by **CPython subprocess** server-side and **Pyodide** in the browser. -- **JavaScript Playground** — **134-lesson** curriculum (8 orientation tutorials + 126 graded challenges across 17 tiers) graded by **Node** server-side and **QuickJS-WASM** in the browser. Same harness contract as Python (`runTests()` returning `PASS:`/`FAIL:`/`ERROR:` strings); offline + Run-locally + batch re-verification all work the same way. Every challenge ships with a reference solution that's executed end-to-end through Node by `JavaScriptChallenge_ReferenceSolutionPasses`. +- **Python Playground** — ~101-lesson curriculum graded by **CPython subprocess** server-side and **Pyodide** in the browser. +- **JavaScript Playground** — **138-lesson** curriculum (10 orientation tutorials + 128 graded challenges across 17 tiers) graded by **Node** server-side and **QuickJS-WASM** in the browser. Same harness contract as Python (`runTests()` returning `PASS:`/`FAIL:`/`ERROR:` strings); offline + Run-locally + batch re-verification all work the same way. Every challenge ships with a reference solution that's executed end-to-end through Node by `JavaScriptChallenge_ReferenceSolutionPasses`. +- **Regex Playground** — 12-chapter curriculum graded **fully in-browser** against a pre-computed `RegexLessonPlan` and re-verified server-side through Node (not .NET) to preserve JavaScript regex flavor. See [Regex Playground & Curriculum](#regex-playground--curriculum) below. All three tracks share the same lesson UI (the language-agnostic `LanguageRunner` Angular component), the same attempt/progress APIs, the same PASS/FAIL line protocol, and the same offline-first PWA model. A student can start a lesson at home, finish it on a campus bus with no signal, and see their unlocks update the moment connectivity returns. The JavaScript Playground is reachable at `/tools/(tools:javascript-playground)`, Python at `/tools/(tools:python-playground)`, and C# at `/tools/(tools:csharp-playground)`. @@ -289,14 +290,14 @@ Python lessons live under [`SpikerSoft.Business/Domain/Lessons/Curriculum/Python | Tier | Numbers | Topic | |------|---------|-------| -| `Tier00_Welcome` | 20001-20006 | Welcome tutorials (intro, modules, functions, print, first submit) | +| `Tier00_Welcome` | 20001-20007 | Welcome tutorials (intro, modules, functions, print, first submit, **how the test harness works**) | | `Tier01_Foundations` | 20100-20107 | Hello World, variables (int, float, bool, str, None) | | `Tier02_Operators` | 20200-20206 | Arithmetic, comparison, logical, conditional expression, f-strings | -| `Tier03_ControlFlow` | 20300-20307 | if / elif / else, `match`/`case`, dict dispatch, for / while / iteration | +| `Tier03_ControlFlow` | 20300-20307 | if / elif / else, dict dispatch, for / while / iteration (`match`/`case` is now introduced in Tier 13 patterns) | | `Tier04_Functions` | 20400-20405 | return, multi-param, keyword args, defaults, lambda | | `Tier05_Collections` | 20500-20506 | list, append, iteration, dict, set, nested lists, tuple | -| `Tier06_Strings` | 20600-20604 | slicing, split/join, replace/case, `''.join` accumulation, try/except `int()` | -| `Tier07_Classes` | 20700-20707 | class, `__init__`, methods, `@property`, `@staticmethod`/`@classmethod`, `__repr__`, `_private`, `self` | +| `Tier06_Strings` | 20600-20605 | slicing, split/join, replace/case, `''.join` accumulation, `isdigit()` validation, **f-strings (canonical)** | +| `Tier07_Classes` | 20699-20707 | **decorator-syntax recipe**, class, `__init__`, methods, `@property`, `@staticmethod`/`@classmethod`, `__repr__`, `_private`, `self` | | `Tier08_Inheritance` | 20800-20805 | base/derived, override, `abc.ABC`, duck typing, multiple inheritance, `super()` | | `Tier09_Generics` | 20900-20902 | `TypeVar`, generic class (`Stack[T]`), bounded generics | | `Tier10_Exceptions` | 21000-21003 | try/except, finally, custom exception, context manager (`with`) | @@ -314,24 +315,44 @@ JavaScript lessons live under [`SpikerSoft.Business/Domain/Lessons/Curriculum/Ja | Tier | Numbers | Topic | |------|---------|-------| -| `Tier00_Welcome` | 30001-30008 | Welcome tutorials (history of JS, engines, browser vs Node, console basics, code style, reading errors, first submit) | -| `Tier01_Foundations` | 30100-30109 | console.log, numbers, strings, booleans, null/undefined, typeof, let vs const, template literals, multi-line | +| `Tier00_Welcome` | 30001-30010 | Welcome tutorials (history of JS, engines, browser vs Node, console basics, code style, reading errors, first submit, **how the test harness works**, **function recipe**) | +| `Tier01_Foundations` | 30100-30109 | console.log, numbers, strings, booleans, null/undefined, typeof, let vs const, template literals (preview), multi-line | | `Tier02_Operators` | 30200-30207 | Arithmetic, `===`/`!==`, logical, ternary, modulo, ++/--, compound assignment, concat-vs-template | | `Tier03_ControlFlow` | 30300-30308 | if/else, switch, for, for-of, for-in, while, do-while, break/continue | | `Tier04_Functions` | 30400-30409 | Declaration vs expression, arrow, defaults, rest, spread, early return, purity, closures, hoisting, higher-order | | `Tier05_Arrays` | 30500-30508 | literal/index, push/pop, length, slice/splice, concat/spread, indexOf/includes, sort, join/split, Array.from/of | -| `Tier06_Strings` | 30600-30607 | length/index, case, slice, includes, replaceAll, split, trim, padStart | +| `Tier06_Strings` | 30600-30608 | length/index, case, slice, includes, replaceAll, split, trim, padStart, **template literals (canonical)** | | `Tier07_Objects` | 30700-30708 | literal, dot/bracket, mutation, keys/values/entries, destructuring, shorthand, spread/assign, JSON, nested | | `Tier08_Iteration` | 30800-30808 | forEach, map, filter, reduce, find, some/every, flat/flatMap, chaining, sort comparator | | `Tier09_Classes` | 30900-30908 | class, ctor, methods, getters/setters, static, this rules, arrow this, bind/call/apply, #private | | `Tier10_Inheritance` | 31000-31005 | extends/super, override, super in methods, instanceof, mixins, Object.create | -| `Tier11_Modules` | 31100-31104 | IIFE module, Symbol.iterator, Symbol keys, Object.freeze, namespace export | -| `Tier12_Errors` | 31200-31205 | throw/catch, finally, custom Error, name+message, re-throw, async errors | -| `Tier13_Async` | 31300-31309 | callbacks, Promise basics, .then chains, resolve/reject, async, await, all/race/allSettled, sequential vs parallel | +| `Tier11_Modules` | 31099-31104 | **ESM export/import recipe**, IIFE module, Symbol.iterator, Symbol keys, Object.freeze, namespace export | +| `Tier12_Errors` | 31200-31204 | throw/catch, finally, custom Error, name+message, re-throw (errors-in-async moved to Tier 13) | +| `Tier13_Async` | 31300-31310 | callbacks, Promise basics, .then chains, resolve/reject, async, await, all/race/allSettled, sequential vs parallel, **errors in async functions** | | `Tier14_Modern` | 31400-31407 | optional chaining, nullish coalescing, Map, Set, WeakMap, generators, for-of generator, generator iterator | | `Tier15_Patterns` | 31500-31504 | revealing module, observer (event bus), factory, strategy, singleton | | `Tier16_Performance` | 31600-31604 | memoization, debounce, throttle, avoiding O(n²), lazy generators | +### Pedagogical contract: no concept used before introduction + +Every challenge lesson is reachable from "Hello, World" using only language features that were formally introduced (and graded) in an earlier lesson. A student who works the curriculum top-to-bottom never has to leave the platform to look up a syntactic form they have not been taught — if the reference solution uses an `f-string`, decorator, optional chain, or generator expression, an earlier lesson exists that puts that exact construct in front of them as a "recipe to copy". Reference solutions for every gradable challenge are checked in alongside the lesson and executed end-to-end on every CI run (`ReferenceSolutionPasses` in `SpikerSoft.Tests.Unit`), so a contract violation surfaces as a red test, not as a confused student. + +The audit findings that drove the most recent Python and JavaScript reorderings are living documents in [`docs/curriculum/python-curriculum-audit.md`](docs/curriculum/python-curriculum-audit.md) and [`docs/curriculum/javascript-curriculum-audit.md`](docs/curriculum/javascript-curriculum-audit.md). Re-run the same audit when adding a new tier; the verification gate is `dotnet test SpikerSoft.Tests.Unit --filter "ReferenceSolutionPasses"`. + +### Regex Playground & Curriculum + +The fourth language track is a 12-chapter regular-expressions curriculum under [`SpikerSoft.Business/Domain/Lessons/Curriculum/Regex/`](spikersoft-backend/SpikerSoft.Business/Domain/Lessons/Curriculum/Regex/) (`Chapter01_Literals` → `Chapter12_Recipes`, covering literals, special characters, character classes, quantifiers, anchors, alternation, capturing groups, back-references, replace, lookarounds, named groups + Unicode property escapes, and idiomatic recipes). Lessons are written as `RegexLessonStrategyBase` subclasses that hand the SPA a serialized `RegexLessonPlan` — test text plus expected matches/replacements row-by-row. + +Grading mechanics differ deliberately from the C# / Python / JavaScript tracks: + +- **Live grading runs entirely in the browser.** [`RegexLessonRunnerService`](spikersoft-angular/libraries/features/dev-tools-reg-ex/src/lib/regex-lesson-runner.service.ts) and [`RegexLessonGraderService`](spikersoft-angular/libraries/features/dev-tools-reg-ex/src/lib/regex-lesson-grader.service.ts) (in `libraries/features/dev-tools-reg-ex`) feed the student's pattern through the browser's native `RegExp` against the plan's test text and emit row-level pass/fail just like the JS / Python harnesses do. +- **Server re-verification routes through Node, not .NET.** When an offline-graded regex submission flushes through `POST /api/Lessons/progress/batch`, [`RegexLessonGradingExecutor`](spikersoft-backend/SpikerSoft.Business/Domain/CodeExecution/Execution/RegexLessonGradingExecutor.cs) builds a JS harness via [`RegexLessonHarnessBuilder`](spikersoft-backend/SpikerSoft.Business/Domain/CodeExecution/Execution/RegexLessonHarnessBuilder.cs) and runs it on the worker's Node subprocess (the same `JavaScriptLessonExecutor` plumbing the JS Playground uses). This is **not** a casual choice — JavaScript regex differs from .NET's `System.Text.RegularExpressions` in ways that matter for the curriculum: the `v`-flag set operations `[[a-z]--[aeiou]]` / `&&`, JS-only replacement patterns `` $` `` / `$'`, and the supported `\p{...}` Unicode property names all diverge between engines. Re-grading through Node guarantees the server records the same pass/fail the student saw locally. +- **No second runtime in the browser.** Regex lessons need no Pyodide, no QuickJS, no .NET WASM — the SPA already has `RegExp`. Offline support is automatic. + +Server-side regex re-grading rides the [Offline Lesson Re-Grading via Worker RPC](#offline-lesson-re-grading-via-worker-rpc) pipeline (below), so a code-runner crash does not take the API down. SPA-side details and the in-app "Lesson" pane / cheatsheet UI live in [`libraries/features/dev-tools-reg-ex/README.md`](spikersoft-angular/libraries/features/dev-tools-reg-ex/README.md). + +> **WASM exclude.** [`SpikerSoft.Wasm.csproj`](spikersoft-backend/SpikerSoft.Wasm/SpikerSoft.Wasm.csproj) excludes `Regex*.cs` from the link-included executor sources — regex re-grading is server-only by design. + ### Dual execution | Path | Where | When | Latency | @@ -361,7 +382,46 @@ ENV CodeExecution__NodeExecutable=/usr/bin/node Two startup probes (`PythonInterpreterProbeHostedService` and `NodeInterpreterProbeHostedService`) each run `--version` once and log the resolved path so a misconfigured deploy fails loudly. **Pin worker images away from `:latest`** so Swarm picks up new layers reliably (the Python + Node installs live in the runtime stage of the Dockerfile). -> **WASM coupling note.** [`SpikerSoft.Wasm.csproj`](spikersoft-backend/SpikerSoft.Wasm/SpikerSoft.Wasm.csproj) link-includes `Domain/CodeExecution/Execution/**/*.cs` from `SpikerSoft.Business`, but excludes server-only files via a list (Python*, JavaScript*, executor/factory abstractions). When you add a new server-only executor, add its file pattern to that exclude list — otherwise the WASM build pulls in `IOptions` / `ILogger` / `Microsoft.Extensions.Services` that the WASM project deliberately doesn't reference, and you'll see a wall of "type or namespace not found" errors. +> **WASM coupling note.** [`SpikerSoft.Wasm.csproj`](spikersoft-backend/SpikerSoft.Wasm/SpikerSoft.Wasm.csproj) link-includes `Domain/CodeExecution/Execution/**/*.cs` from `SpikerSoft.Business`, but excludes server-only files via a list (`Python*`, `JavaScript*`, `Regex*`, executor/factory abstractions, `RoslynLessonGradingExecutor`). When you add a new server-only executor, add its file pattern to that exclude list — otherwise the WASM build pulls in `IOptions` / `ILogger` / `Microsoft.Extensions.Services` that the WASM project deliberately doesn't reference, and you'll see a wall of "type or namespace not found" errors. + +### Offline Lesson Re-Grading via Worker RPC + +Browser graders (`SpikerSoft.Wasm` for C#, Pyodide for Python, QuickJS for JavaScript, the SPA's native `RegExp` for regex) are convenient and work entirely offline, but they run on the student's machine and are tamperable. Before the platform credits a skill unlock or marks a lesson complete for-real, the *server* has to re-execute the same lesson against the same grader and confirm it actually passed. That's the trust boundary the offline IndexedDB queue crosses every time it flushes through `POST /api/Lessons/progress/batch`. + +**The architectural rule:** code execution NEVER runs in the API process. If the code-runner container crashes mid-grade, the API stays up; a fresh client retry round-trips through Rabbit on the next batch flush. This is enforced by the API's DI graph — only [`ILessonRegradeClient`](spikersoft-backend/SpikerSoft.Business/Domain/Lessons/Services/ILessonRegradeClient.cs) is registered there; no `ILessonGradingExecutor` implementation is reachable from `SpikerSoft.Api`. + +**The flow:** [`LessonsController.BatchProgress`](spikersoft-backend/SpikerSoft.Api/Domain/Lessons/LessonsController.cs) builds a [`LessonRegradeRequest`](spikersoft-backend/SpikerSoft.Business/Domain/Lessons/Messaging/LessonRegradeRequest.cs) for each queued submission and hands it to [`LessonRegradeClient`](spikersoft-backend/SpikerSoft.Business/Domain/Lessons/Services/LessonRegradeClient.cs), which publishes it to the `lesson.regrade.requests` exchange and blocks on the worker's reply via RabbitMQ **Direct-Reply-To** (`amq.rabbitmq.reply-to`). The worker's [`LessonRegradeWorkerHostedService`](spikersoft-backend/SpikerSoft.EventHandlers.CodeExecution/Services/LessonRegradeWorkerHostedService.cs) consumes the request, dispatches through the same [`ILessonGradingExecutorFactory`](spikersoft-backend/SpikerSoft.Business/Domain/CodeExecution/Execution/ILessonGradingExecutorFactory.cs) that backs the live "Submit" path (Roslyn / CPython / Node / Regex-via-Node — no code duplication between live-submit and regrade), and publishes a [`LessonRegradeResponse`](spikersoft-backend/SpikerSoft.Business/Domain/Lessons/Messaging/LessonRegradeResponse.cs) back to the API's reply queue. + +```mermaid +sequenceDiagram + autonumber + participant SPA as Angular SPA + participant API as SpikerSoft.Api + participant MQ as RabbitMQ + participant Worker as SpikerSoft.EventHandlers.CodeExecution + Note over SPA: Student grades offline (in-browser)
Submission queued in IndexedDB + SPA->>API: POST /api/Lessons/progress/batch + Note over API: For each queued attempt + API->>API: Build LessonRegradeRequest
(lessonNumber, attemptToken,
studentCode, testCode, runtime) + API->>MQ: publish to lesson.regrade.requests
reply-to = amq.rabbitmq.reply-to + MQ->>Worker: deliver request + Note over Worker: ILessonGradingExecutorFactory
picks Roslyn / CPython / Node / Regex + Worker->>Worker: Execute & grade + Worker->>MQ: publish LessonRegradeResponse
to API's Direct-Reply queue + MQ->>API: deliver response + alt Response received within timeout + API->>API: Persist progress if AllTestsPassed + API->>SPA: 200 OK with per-attempt status + else Timeout (worker down / overloaded) + API->>SPA: 5xx server_error
(IndexedDB keeps the attempt; SPA retries) + end +``` + +**Idempotency.** Each request carries the SPA-generated `attemptToken`. If the worker grades a token, the API records it; if a network blip causes the SPA to retry, the controller short-circuits on the duplicate token rather than re-publishing — so the worker never regrades the same attempt twice. + +**Timeout handling.** `LessonRegradeClient` enforces a per-call timeout (default 30 s). On expiry, the call throws `TimeoutException`, the controller maps it to HTTP `server_error`, and the SPA leaves the offline attempt in IndexedDB so the next batch flush picks it up. The same attempt token is re-used so the worker still de-dupes correctly when it eventually catches up. + +**Same factory, same code.** Both worker consumer paths (live-submit `code.execution.requests` and offline-regrade `lesson.regrade.requests`) dispatch through `ILessonGradingExecutorFactory`. There is no separate "regrade" code path that could drift from the live grader — fixing a hint in `RoslynLessonGradingExecutor` fixes both modes simultaneously. --- @@ -533,11 +593,12 @@ In CI the bundle is published by `spikersoft-backend/.gitea/workflows/spikersoft | **WASM project** | `SpikerSoft.Wasm/SpikerSoft.Wasm.csproj` | .NET 10 WebAssembly app; publishes to `assets/dotnet/` | | **JS interop** | `SpikerSoft.Wasm/WasmCompilerEntry.cs` | `[JSExport]` shim for the browser worker | | **Worker boot** | `spikersoft-angular/projects/spikersoft/src/assets/dotnet/main.js` | Pre-fetches managed assemblies, feeds PE bytes to Roslyn via `AddReferenceImage` | -| **Web Worker** | `libraries/tools/src/services/csharp-runner/wasm-runtime.worker.ts` | Hosts the .NET runtime on a worker thread | -| **Path-routing service** | `libraries/tools/src/services/csharp-runner/csharp-runner.service.ts` | Server↔WASM routing with auto-failover | -| **Runner component** | `libraries/tools/src/components/csharp-runner/csharp-runner.ts` | Lesson UI, sidebar, refresh orchestration | -| **Lesson sidebar** | `libraries/tools/src/components/csharp-runner/lesson-sidebar/` | Tier-grouped lesson list with prereq-driven lock state | -| **Offline queue** | `libraries/tools/src/services/csharp-runner/progress-sync.service.ts` | IndexedDB enqueue + flush-on-reconnect | +| **Web Worker** | `libraries/features/dev-tools-csharp-runner/src/lib/wasm-runtime.worker.ts` | Hosts the .NET runtime on a worker thread | +| **Path-routing service** | `libraries/features/dev-tools-csharp-runner/src/lib/csharp-runner.service.ts` | Server↔WASM routing with auto-failover | +| **Language-agnostic shell** | `libraries/platform/language-runner/src/lib/language-runner.ts` | Shared playground SHELL composed by all 3 wrapping shells (C#/Python/JS) via `LANGUAGE_RUNNER_CONFIG` + `LANGUAGE_RUNTIME_ADAPTER` | +| **C# wrapping shell** | `libraries/features/dev-tools-csharp-runner/src/lib/csharp-runner.ts` | Wraps the language-runner with the C#-specific `LANGUAGE_RUNNER_CONFIG` and `csharp-runtime.adapter.ts` | +| **Lesson sidebar** | `libraries/shared/lesson-panes/src/lib/lesson-sidebar/` | Tier-grouped lesson list with prereq-driven lock state (shared across all 3 playgrounds) | +| **Offline queue** | `libraries/platform/progress-sync/src/lib/progress-sync.service.ts` | IndexedDB enqueue + flush-on-reconnect (consumed via `PROGRESS_SYNC_PORT` so the platform shell stays decoupled) | | **SW config** | `projects/spikersoft/ngsw-config.json` | `freshness` for `/api/Lessons/progress`, lazy for `/assets/dotnet/` | ### API Endpoints @@ -556,6 +617,83 @@ All endpoints require authentication. **`POST /api/CSharpCodeRunner/*`** and **` --- +## Frontend Architecture: Feature-Oriented Boundaries + +The Angular workspace was migrated through a multi-phase refactor (Phases 4–6, completed 2026-05-09 — see [`docs/architecture/inventory.md`](docs/architecture/inventory.md) and [`docs/architecture/boundaries.md`](docs/architecture/boundaries.md)) from a monolithic `libraries/tools/` lib into ~65 buildable libraries grouped by **layer**. Each library has its own `ng-package.json`, `vitest.config.ts`, `project.json`, and tests run isolated. Lazy-loaded routes per feature → smaller initial bundle (the Phase 6 sub-phases collectively brought the gzipped `main.js` to ~302 kB). + +### Layers and clusters + +Every library carries at least one `layer:*` tag in its `project.json`. The boundary categories on disk are: + +| Cluster | Layer tag | Count | Purpose | +|---|---|---|---| +| [`libraries/domain/`](spikersoft-angular/libraries/domain/) | `layer:domain` | 9 | Pure data models + thin services bound to backend resources, **no UI**: `blog`, `book`, `child-account`, `fundraiser`, `geo`, `mrz`, `pre-registration`, `profile`, `sponsor` | +| [`libraries/features/`](spikersoft-angular/libraries/features/) | `layer:feature` | 24 | Top-level user-facing features that own routes/components/services: the entire `dev-tools-*` suite (21 tools — C#/Python/JavaScript/regex/x86 playgrounds, decompiler, encoding/conversion, image-to-avif/ico, qr-code, voronoi, diff, diagram, blockly, duckdb, ipv4, quick-type, common-commands), `blog`, `child-account-dialog`, `fundraiser`, `games-clue-for-sql`, `parent-dashboard`, `sponsor`, `trellis-3d-generator` | +| [`libraries/platform/`](spikersoft-angular/libraries/platform/) | `layer:platform` | 13 | Cross-cutting infrastructure consumed by features: `language-runner`, `lesson-catalog`, `progress-sync`, `pyodide-runtime`, `monaco-editor`, `tool-storage`, `tool-file-menu`, `intro-tour`, `intro-launching`, `js-step-debugger`, `activity-tracking`, `loading`, `avif-encoder` | +| [`libraries/shared/`](spikersoft-angular/libraries/shared/) | `layer:shared` | 6 | Small reusable units that don't belong to a single feature/domain: `api-config`, `js-formatting-options`, `lesson-panes`, `lesson-platform`, `save-load-dialogs`, `utils/{crc32,file-hash}` | +| [`libraries/ui/`](spikersoft-angular/libraries/ui/) | `layer:ui` | 2 | Pure presentational atoms: `confirm-dialog`, `mrz-crop` | +| [`libraries/game/`](spikersoft-angular/libraries/game/) | `layer:game` | 1 | Game-runtime libs that pair a feature with heavy runtime deps: `wasm-voxel` | +| Top-level legacy | various | 5 | Pre-Phase-4 libs that haven't been re-clustered into the new directories yet but already carry the right layer tag: `keycloak-admin` (`layer:feature`), `marks-site-models` + `spikersoft-models` (`layer:domain`), `spikersoft-environment` (`layer:platform`), `spikersoft-theme` (`layer:ui`) | + +### Allowed dependency arrows + +Enforced by `@nx/enforce-module-boundaries` (severity `error` since Phase 5) on every PR. The full constraint set lives in [`spikersoft-angular/eslint.config.cjs`](spikersoft-angular/eslint.config.cjs); the abridged version a feature author cares about: + +| Source layer | May depend on | +|---|---| +| `app` | `feature`, `ui`, `platform`, `domain`, `runtime`, `game`, `editor`, `shared`, `generated` | +| `feature` | `feature`, `domain`, `ui`, `runtime`, `game`, `editor`, `platform`, `shared`, `generated` | +| `ui` | `ui`, `platform`, `shared` | +| `domain` | `domain`, `shared`, `generated` | +| `platform` | `platform`, `shared` | +| `game` | `game`, `runtime`, `shared` | +| `shared` | `shared` | + +The shape of the rule, in one diagram: + +```mermaid +flowchart TB + app[app: projects/spikersoft] + feature[feature: libraries/features, libraries/keycloak-admin] + domain[domain: libraries/domain, libraries/marks-site-models, libraries/spikersoft-models] + platform[platform: libraries/platform, libraries/spikersoft-environment] + ui[ui: libraries/ui, libraries/spikersoft-theme] + game[game: libraries/game] + shared[shared: libraries/shared] + + app --> feature + app --> domain + app --> platform + app --> ui + app --> game + app --> shared + + feature --> domain + feature --> platform + feature --> ui + feature --> game + feature --> shared + + ui --> platform + ui --> shared + domain --> shared + platform --> shared + game --> shared +``` + +The arrows go **only downward**. A `domain` lib that tries to import from `feature` or `platform` fails CI; `platform` cannot import `feature`; `shared` cannot import anything but `shared`. This is what the boundary refactor bought: the dep graph is now mechanically prevented from collapsing back into the old "everything imports `tools`" shape. + +### Cross-cluster ports + +Where a feature genuinely needs to consume a service from another cluster without violating the arrows, the lib pair uses a **port** (an injection token) declared in `shared/lesson-platform` and provided at the wrapping shell. Examples: + +- [`PROGRESS_SYNC_PORT`](spikersoft-angular/libraries/shared/lesson-platform/src/lib/progress-sync-port.ts) — consumed by `platform/language-runner` so the shared playground shell never hard-imports the Monaco-coupled `ProgressSyncService` concrete (the `feature/dev-tools-csharp-runner` shell wires it up at provider time). +- `AVIF_ENCODER` port — `domain/blog` and `feature/dev-tools-image-to-avif` consume the encoder through a port declared in `shared`, so the domain lib never depends directly on `platform/avif-encoder`. + +If a new feature seems to require an arrow the layering forbids, the right answer is almost always to extract a port or a contract type into `shared/lesson-platform` (or its sibling `shared/api-config`) rather than to widen the rule. See [`docs/architecture/boundaries.md`](docs/architecture/boundaries.md) for the canonical write-up of the rule, baseline counts, and the burn-down protocol that closed the last ~22 violations. + +--- + ## Project Structure ### Frontend (spikersoft-angular/) @@ -587,20 +725,40 @@ spikersoft-angular/ │ ├── location/ # LocationService (API-driven country list) │ ├── position/ # PositionService (open positions + auto ambassadors) │ └── team/ # TeamService (staff member profiles) -├── libraries/ -│ ├── keycloak-admin/ # Keycloak administration UI -│ ├── marks-site-models/ # Shared models for field-service customer management -│ ├── spikersoft-environment/ # Environment configuration library -│ ├── spikersoft-models/ # Shared TypeScript models -│ ├── spikersoft-theme/ # Glassmorphic theme and styling utilities -│ ├── tools/ # Developer & visual programming utilities -│ │ ├── blockly/ # Blockly editor + robot blocks -│ │ ├── diagram/ # Rete.js node editor + robot nodes -│ │ ├── ram-calculator.ts # Program RAM cost calculation -│ │ └── src/ -│ │ ├── components/csharp-runner/ # C# Playground UI (lessons, sidebar, free-play) -│ │ └── services/csharp-runner/ # Server↔WASM router, Web Worker, offline progress queue -│ └── wasm-voxel/ # TypeScript Minecraft port (voxel engine) +├── libraries/ # Buildable Nx libs grouped by layer (see Frontend Architecture section) +│ ├── domain/ # layer:domain — pure data + thin services bound to backend resources (9 libs) +│ │ ├── blog/ book/ child-account/ fundraiser/ geo/ +│ │ └── mrz/ pre-registration/ profile/ sponsor/ +│ ├── features/ # layer:feature — top-level user features w/ routes (24 libs) +│ │ ├── dev-tools-csharp-runner/ # C# Playground wrapping shell (config + adapter) +│ │ ├── dev-tools-python-runner/ # Python Playground wrapping shell +│ │ ├── dev-tools-javascript-runner/# JS Playground wrapping shell +│ │ ├── dev-tools-reg-ex/ # Regex Playground (chapters + cheatsheet) +│ │ ├── dev-tools-x86-playground/ ...# 21 dev-tools-* + blog, sponsor, fundraiser, ... +│ │ └── … # see libraries/features/ for full list +│ ├── platform/ # layer:platform — cross-cutting infra (13 libs) +│ │ ├── language-runner/ # Language-agnostic playground SHELL (consumed by 3 wrapping shells) +│ │ ├── lesson-catalog/ # Lesson catalog + prerequisites +│ │ ├── progress-sync/ # Server↔in-browser router + offline IndexedDB queue +│ │ ├── pyodide-runtime/ # Pyodide loader (Python in-browser) +│ │ ├── monaco-editor/ tool-storage/ tool-file-menu/ intro-tour/ intro-launching/ +│ │ └── js-step-debugger/ activity-tracking/ loading/ avif-encoder/ +│ ├── shared/ # layer:shared — small reusable units (6 libs) +│ │ ├── lesson-platform/ # Cross-cluster contracts (PROGRESS_SYNC_PORT, SourceFile, ...) +│ │ ├── lesson-panes/ # Lesson + tutorial + sidebar panes +│ │ ├── api-config/ js-formatting-options/ save-load-dialogs/ +│ │ └── utils/ # crc32, file-hash +│ ├── ui/ # layer:ui — presentational atoms (2 libs) +│ │ └── confirm-dialog/ mrz-crop/ +│ ├── game/ # layer:game — game runtime libs (1 lib) +│ │ └── wasm-voxel/ # TypeScript Minecraft port (voxel engine) +│ │ +│ │ # Legacy top-level libs (pre-Phase-4 boundary refactor; tagged but not re-clustered) +│ ├── keycloak-admin/ # layer:feature — Keycloak administration UI +│ ├── marks-site-models/ # layer:domain — Shared models for field-service customer management +│ ├── spikersoft-environment/ # layer:platform — Environment configuration +│ ├── spikersoft-models/ # layer:domain — Shared TypeScript models +│ └── spikersoft-theme/ # layer:ui — Glassmorphic theme + styling utilities ├── nx.json # Nx workspace config └── package.json ``` @@ -1012,7 +1170,7 @@ docker build -t spikersoft-angular -f spikersoft-angular/Dockerfile . | Game Server | `spikersoft-backend/SpikerSoft.GameServer/README.md` | | Integrated Game Ecosystem | See main README — [Integrated Game Ecosystem](#integrated-game-ecosystem) | | Robot & Visual Programming | See main README — [Robot & Visual Programming](#robot--visual-programming) and `ARCHITECTURE-ROBOT-VISUAL-PROGRAMMING.md` | -| C# Coding Curriculum & Playground | See main README — [C# Coding Curriculum & Playground](#c-coding-curriculum--playground); WASM bundle: `spikersoft-angular/projects/spikersoft/src/assets/dotnet/README.md`; Tools library: `spikersoft-angular/libraries/tools/README.md` | +| C# Coding Curriculum & Playground | See main README — [C# Coding Curriculum & Playground](#c-coding-curriculum--playground); WASM bundle: `spikersoft-angular/projects/spikersoft/src/assets/dotnet/README.md`; Wrapping shell: `spikersoft-angular/libraries/features/dev-tools-csharp-runner/README.md`; Shared playground SHELL: `spikersoft-angular/libraries/platform/language-runner/README.md` | | Event Handlers | See main README — [Event Handlers](#event-handlers); shared infra: `spikersoft-backend/SpikerSoft.EventHandlers.Infrastructure/README.md`; code worker: `spikersoft-backend/SpikerSoft.EventHandlers.CodeExecution/README.md` | | Chess Game | See main README — [Chess Game](#chess-game) section | | Dungeon Crawler | `spikersoft-angular/projects/spikersoft/src/app/_components/_games/dungeon-crawler/README.md` |