# WSC-MVC — Architecture (as implemented through M2) ## Request flow ``` GET /hello -> IIS URL Rewrite rule "WscMvc-Hello" (^hello/?$) -> /Default.asp?route=/hello -> Server.CreateObject("WscMvc.RequestContext"); ctx.Initialize path, httpMethod, logDir -> Server.CreateObject("WscMvc.Application") -> Application.Run(ctx, statusLine, contentType, body) [Framework/Application.wsc] -> CreateObject("WscMvc.HomeController") -> HomeController.Hello(body) [Controllers/HomeController.wsc] -> LogOutcome ctx, statusLine (best-effort append to logs/app.log) -> Default.asp sets Response.Status/ContentType, writes body ``` ## Component boundary contract - `Default.asp` is the only file that touches ASP intrinsic objects (`Request`, `Response`, `Server`). It contains no business logic — only reading the `route` query parameter and `REQUEST_METHOD`, resolving `logs/`'s physical path via `Server.MapPath`, invoking `WscMvc.RequestContext` and `WscMvc.Application`, and writing the response. - `Framework/RequestContext.wsc`, `Framework/Application.wsc`, and `Controllers/HomeController.wsc` never reference `Request`/`Response`/`Server`/`Session`. All data crosses the ASP-to-WSC and WSC-to-WSC boundaries as either VBScript scalars (strings) or our own `RequestContext` COM object (not an ASP intrinsic) — never an ASP host object. See `docs/DECISIONS.md` for why the original SPEC §15 open question about passing ASP intrinsics into a WSC never needed a direct experiment: the architecture never crosses that boundary by design. - WSC public members are exposed as plain `` entries backed by ordinary `Sub`/`Function` procedures, never ``/`Property Get`. VBScript's `Property Get/Let/Set` requires a `Class...End Class` block and cannot appear at a WSC's top-level script scope — confirmed experimentally (see `docs/DECISIONS.md`), not assumed from general WSC documentation. - Routing in M1/M2 is still a single hardcoded `If path = "/hello"` check inside `Application.Run`. This is intentionally minimal and will be replaced by the explicit allowlisted route table in M3 (`Router.wsc`); do not extend it ad hoc before that milestone. - `Application.Run` is the single central point that decides expected (404, ordinary control flow) vs. unexpected (COM/method failure, 500) outcomes, and the single point that logs every outcome. `Default.asp` still independently guards its own three sequential calls (`RequestContext` creation, `Initialize`, `Application` creation, `Run`) since a WSC failing to even instantiate happens outside `Application.Run`'s reach. ## Per-request lifetime and diagnostics - `RequestContext` and `Application` are both created fresh per request via `Server.CreateObject` in `Default.asp` and dropped (`Set ... = Nothing`) at the end of the page; neither is ever cached in ASP `Session`/`Application` scope, per SPEC §10. - `RequestContext.Initialize` generates a correlation id from `Fix(Timer)` plus `Scripting.FileSystemObject.GetTempName()` — not `Randomize`/`Rnd()`, which was tried first and shown experimentally to collide when two contexts are created within the same clock tick (see `docs/DECISIONS.md`). - `Application.LogOutcome` appends one line per request (timestamp, correlation id, method, path, status line, elapsed ms) to `logs/app.log`, guarded end-to-end by `On Error Resume Next` so a logging failure can never affect the HTTP response. Concurrent writers are serialized with a manual lock-file mutex (`logs/app.log.lock`, via `CreateTextFile(..., OverwriteExisting:=False)`) since no ASP intrinsic locking primitive (`Application.Lock`) is available to code that must not reference ASP intrinsics. The retry budget is deliberately short (see `docs/DECISIONS.md`): logging is explicitly best-effort and may drop lines under heavy concurrency without affecting correctness of the response. ## Test harness: GET /self-test (JSON, frontend-agnostic) `Controllers/SelfTestController.wsc` (`WscMvc.SelfTestController`) exposes the same checks as `tests/Test-Components.vbs`, but callable over plain HTTP and returning JSON — no PowerShell, cscript, or SSH access to the VM required. `Application.Run` routes `path = "/self-test"` to it. The HTTP status is always `200` if the self-test mechanism itself ran (a broken `SelfTestController`/`Application` still degrades to the normal `500` path); the JSON body's top-level `"ok"` field and each check's `"pass"` field carry the actual test results — this is conventional health-check design (5xx is reserved for the diagnostics mechanism being broken, not for a failed assertion inside it). ``` curl http://100.127.62.31:8090/self-test {"ok":true,"checks":[{"name":"request_context_contract","pass":true,"detail":""}, ...]} ``` `tests/run-self-test.sh` wraps this in `curl` + `python3 -m json.tool`, runnable from any CLI (this was verified working directly from the Linux OpenClaw host, not just from the VM). `tests/Test-Http.ps1` also calls `/self-test` and folds each check into its own PASS/FAIL output, complementing (not duplicating) its own direct `/hello` and denied-`.wsc`-access checks, which `/self-test` cannot observe about itself since those are IIS-layer/`web.config` concerns, not component-layer ones. `SelfTestController`'s error details intentionally include raw `Err.Description` text — unlike every other route, which must never leak internals to an arbitrary caller, this route's entire purpose is diagnostics. It is a dev/test-milestone tool; reconsider whether it should be gated or removed before any production deployment during the M6 hardening pass. ## COM identity | Component | ProgID | CLSID | |---|---|---| | `Framework/RequestContext.wsc` | `WscMvc.RequestContext` | `{1C36FA55-34DF-4974-94B9-D657389362B2}` | | `Framework/Application.wsc` | `WscMvc.Application` | `{851C7763-1638-42FE-A166-BF3DD3A96A88}` | | `Controllers/HomeController.wsc` | `WscMvc.HomeController` | `{87488446-60BE-4068-8368-0B709BB68F3F}` | | `Controllers/SelfTestController.wsc` | `WscMvc.SelfTestController` | `{D2634944-4646-4C55-956E-4C05E7E10904}` | CLSIDs are fixed at creation and must never be recycled for a different component (AGENTS.md). `Application`'s public `Run` signature changed between M1 and M2 (added a leading `ctx` parameter) under the same CLSID; acceptable because this is active pre-release (v0.1) development with exactly one caller (`Default.asp`, updated in lockstep) — not a claim that live interface changes are safe for a published/external client. ## IIS site (test host: win2025test, 100.127.62.31) - Site: `WscMvc`, binding `*:8090`, physical path `C:\Projects\wsc-mvc\public` — **not** the project root. `public/` contains only `Default.asp` and `web.config`; every other directory (`Framework/`, `Controllers/`, `tests/`, `tools/`, `docs/`, `logs/`) is a sibling of `public/`, outside IIS's site physical path entirely. Changed 2026-09-19 at Daniel's explicit direction (see `docs/DECISIONS.md`); previously the site root was the whole project directory with those folders hidden via `hiddenSegments`. - App pool: `WscMvc`, 64-bit, no managed code. Anonymous authentication identity: `IUSR`. - `public/web.config` keeps only a `.wsc/.vbs/.ps1/.md` extension denylist as defense-in-depth (in case a stray file ever lands directly in `public/`); no `hiddenSegments` are needed since the directories they used to hide no longer exist under the served root at all. - `enableParentPaths=true` is set for this site (scoped via `appcmd ... /commit:apphost`, a `` block in `applicationHost.config` — not a machine-wide unlock of the locked `system.webServer/asp` section) so `Default.asp` can `Server.MapPath("../logs")` to reach `logs/`. This setting affects only server-side script `MapPath`/`#include` resolution, not client-supplied URL paths — IIS's own URL normalization independently rejects `..`-traversal in an incoming request regardless of this setting; verified with a direct request attempt, see `docs/DECISIONS.md`. - `logs/` (`C:\Projects\wsc-mvc\logs`, unchanged physical location from before this restructure) has an explicit, scoped `icacls ... /grant "IIS_IUSRS:(OI)(CI)M"` so classic ASP (impersonating `IUSR` for anonymous requests) can write `app.log`. No other project directory grants `IUSR`/`IIS_IUSRS` write access — verified with a recursive `icacls /T` scan, see `docs/DECISIONS.md`. - Default document is `Default.asp`; `/hello` and `/self-test` are served via rewrite rules, not the default document. ## Deferred to later milestones (do not implement early) Per SPEC §3 non-goals and IMPLEMENTATION_PLAN M3+: explicit route table, HTML views/templates, ADODB, auth. A more robust logging mechanism (if complete coverage under concurrent load is ever required) is deferred to M6 — see the concurrency finding in `docs/DECISIONS.md`.