diff --git a/docs/prf-11-budgets.md b/docs/prf-11-budgets.md new file mode 100644 index 0000000..98f600b --- /dev/null +++ b/docs/prf-11-budgets.md @@ -0,0 +1,83 @@ +# PRF.11 AL-06 budget re-baseline, static-asset file count and WA-08 costing (U6) + +This is the reviewed AL-06 record for the Blazor profile asset budgets (`eng/policy/profile-budgets.json`). It changes baseline values only. The 10 percent regression gate (`regressionPercent` 10) is unchanged. It is written in the same commit as the baseline change, so the change is recorded and not silent (WA-08, AL-06). + +Reviewer: `w-deku-20261008-rev-prf-11` (pre-assigned, brief S25). Decision recorded at write time: `approved`, reviewedOn `2026-10-09`. That field is a proposal. Only the named reviewer's exact-head approval ratifies it. + +## Why a re-baseline is needed + +Under WA-08 and AL-06, until a reviewed re-baseline record is merged, the React-measured values stay the enforced ceiling for Blazor builds. The React profile budgets were in `apps/app/budgets.json` at the PRF.08 commit `f8123b6` in Web history. Those values are roughly 119 KB gzip for the whole Account profile. A Blazor WebAssembly runtime alone is about 3.2 MB gzip, so the React ceiling cannot be met by any Blazor build. The WEB.40 baseline, measured at its U5 step (`c5a5f2e`), replaced the React ceiling in CI. This record makes that replacement reviewed, and it is the only AL-06 change for the asset budgets in PRF.11. + +## Values + +| Metric (gzip bytes unless stated) | React ceiling (PRF.08, `f8123b6`) account / chat | WEB.40 baseline (`c5a5f2e`) | Measured at base `c8588681` (this record) | Delta against WEB.40 | +| --- | --- | --- | --- | --- | +| initialRequests | 8 / 8 | 57 | 58 | +1 | +| htmlBytes | 2967 / 3271 | 719 | 766 | +47 | +| initialCssGzip | 3050 / 3050 | 2245 | 2380 | +135 | +| initialJsGzip | 116363 / 144698 | 125944 | 126011 | +67 | +| initialWasmGzip | not in React file | 3227235 | 3236688 | +9453 | +| initialDataGzip | not in React file | 848189 | 848189 | 0 | +| initialOtherGzip | not in React file | 238 | 238 | 0 | +| totalGzip | 119413 / 147748 | 4203851 | 4213506 | +9655 | + +The measured values are the same for `account` and `chat`, because both profiles are served from one published application (the same shell and the same framework set). The U6 baseline recorded in this table (the `c8588681` measurement) was superseded by the head re-measurement and the ceiling decision in the section "Head re-measurement and ceilings (review fix 1)" below. The gate allows 10 percent growth above the baseline. Against the WEB.40 baseline, the relative deltas are: htmlBytes +6.5 percent, initialCssGzip +6.0 percent, initialRequests +1.8 percent (one request), initialWasmGzip +0.3 percent, totalGzip +0.2 percent, initialJsGzip +0.05 percent, and none for the data and other metrics. The large relative changes are on small metrics. The cause of each delta was not isolated. The measurement is at the final WEB.40 head merged as `c8588681`, and the WEB.40 U5 baseline was taken before the later WEB.40 changes. + +## Head re-measurement and ceilings (review fix 1) + +The independent review of `2643127` found that the U6 baseline raised every ceiling above the pre-PRF.11 WEB.40 ceiling, while every head value fitted under the WEB.40 ceiling. The raised ceilings therefore loosened the gate by a margin the measurement did not require. This section records the re-measurement at the head and the decision that followed. + +- Measured under CI conditions (`GITHUB_ACTIONS=true`, `CI=true`, the placeholder run identity `1`, a fresh `NUGET_PACKAGES` folder) at two code heads: `006b4b8` (the first measurement, before the budget commit) and `31888e1` (the gate chain of record, the budget commit). `App` is published to `artifacts/publish/app` and `profiles budget` runs against it. The code between the two heads is test and document only, but the two publishes are not byte-identical: the measured values move by up to 10 bytes (see the run-to-run note below). The figures in the table are the `31888e1` chain values, with the `006b4b8` values in brackets where they differ. +- The gate is `value <= baseline + baseline * regressionPercent / 100` (`ProfileBudget.Limit`, rounded down), with `regressionPercent` 10 unchanged. + +| Metric (gzip bytes unless stated) | WEB.40 baseline (`c5a5f2e`), the pre-PRF.11 ceiling basis | U6 baseline (`2a21bc3`, `c8588681` measurement), before this fix | Head measurement, account and chat (`31888e1`; `006b4b8` in brackets) | Ceiling now (= WEB.40 ceiling) | Headroom at `31888e1` | +| --- | --- | --- | --- | --- | --- | +| initialRequests | 57 (ceiling 62) | 58 (ceiling 63) | 58 | 62 | 4 | +| htmlBytes | 719 (ceiling 790) | 766 (ceiling 842) | 766 | 790 | 24 | +| initialCssGzip | 2245 (ceiling 2469) | 2380 (ceiling 2618) | 2328 | 2469 | 141 | +| initialJsGzip | 125944 (ceiling 138538) | 126011 (ceiling 138612) | 126015 (126013) | 138538 | 12523 | +| initialWasmGzip | 3227235 (ceiling 3549958) | 3236688 (ceiling 3560356) | 3236698 (3236690) | 3549958 | 313260 | +| initialDataGzip | 848189 (ceiling 933007) | 848189 (ceiling 933007) | 848189 | 933007 | 84818 | +| initialOtherGzip | 238 (ceiling 261) | 238 (ceiling 261) | 238 | 261 | 23 | +| totalGzip | 4203851 (ceiling 4624236) | 4213506 (ceiling 4634856) | 4213468 (4213458) | 4624236 | 410768 | + +Decision (review fix 1): every head value fits under its pre-PRF.11 WEB.40 ceiling, so no measurement requires a looser ceiling. The baseline of both profiles is therefore the WEB.40 baseline (`eng/policy/profile-budgets.json` is byte-identical to `c5a5f2e`). Each ceiling equals its WEB.40 ceiling, and every ceiling is tighter than the U6 ceiling it replaces. + +- **Remaining loosening: none.** No ceiling is looser than the pre-PRF.11 WEB.40 ceiling. +- The head measurement is above the WEB.40 baseline in six metrics: initialRequests +1, htmlBytes +47, initialCssGzip +83, initialJsGzip +71, initialWasmGzip +9463, and totalGzip +9617 (at `31888e1`). These increases are inside the existing 10 percent allowance and are recorded as the head measurement, not as the baseline. The cause of each increase since `c5a5f2e` was not isolated, and it does not affect any ceiling. +- Run-to-run note: the same code gave different gzip totals on two clean publishes. At `006b4b8` the values were initialJsGzip 126013, initialWasmGzip 3236690 and totalGzip 4213458; at `31888e1` they were 126015, 3236698 and 4213468. The review's fresh clone at `2643127` recorded initialWasmGzip 3236705 and totalGzip 4213473. The variation is 2 to 15 bytes on the wasm and JS metrics and is not isolated. It is far below every ceiling (for example, the WASM headroom is 313260 bytes), so the decision does not depend on it. The budget gate is a ceiling check, and it does not require byte-stable publishes. + +## Largest single static file (review fix 6) + +Measured at `31888e1` in `artifacts/publish/app/wwwroot` (174 files, 116 precompressed; the same figures at `006b4b8`): + +- The largest single file is `_framework/dotnet.native.rw4kynp763.wasm`, 3,001,422 bytes uncompressed (2.86 MiB). That is 11.4 percent of the 25 MiB (26,214,400 bytes) single-file limit. Its precompressed variants are 1,207,786 bytes (`.gz`) and 976,255 bytes (`.br`). +- The next largest files are `_framework/System.Private.CoreLib.o10ikxvddh.wasm` (1,682,197 bytes) and `_framework/icudt_no_CJK.lfu7j35m59.dat` (1,107,168 bytes). +- The single-file limit is therefore met with a margin of 88.6 percent. The earlier statement "the largest single file was not measured" is superseded by this measurement. + +## Measurement + +- Command, under CI conditions (`GITHUB_ACTIONS=true`, `CI=true`, a fresh `NUGET_PACKAGES`): `dotnet publish src/ArcForges.Web.App/ArcForges.Web.App.csproj --no-restore -c Release -o artifacts/publish/app`, then `dotnet tools/ArcForges.Web.Tooling/bin/Release/net10.0/ArcForges.Web.Tooling.dll profiles budget --publish artifacts/publish/app --budgets eng/policy/profile-budgets.json`. +- The build is the IL build. `RunAOTCompilation` is `false` (D6, D-007). The publish printed that it published without optimizations and recommended the `wasm-tools` workload, which is not installed. The AOT benchmark is therefore recorded as **not run** (no wasm-tools workload or emscripten was installed, and no user install is performed). +- SDK 10.0.401, Windows 11, the local publish. The budget tool reported `Profile budgets passed.` against the new baseline, and the Tooling test project passed 56 of 56. +- Startup time and interaction responsiveness (INP) were not measured offline. They need the deployed profile origin and are blocked on CLOUD.85 (LS3, not proven). `interactionBudgets` stays `re-baseline-pending` with owner PRF.11, and the React interaction numbers (`apps/app/interaction-budgets.json` at `80900a1`) are not carried over. They were derived from one observation and are not comparable with Blazor (D8). + +## Static-asset file count (platform limit) + +- The published `wwwroot` holds 174 files, 116 of them precompressed (`.br` and `.gz`). Precompressed variants are counted, as the gate requires. +- The profile bundle (`web-profiles-43f87a11...tar`, built and verified by `profiles bundle` and `profiles verify`) has 175 members: the manifest and 174 served files. The served set is the `wwwroot` set minus the shell and its two encodings (`index.html`, `index.html.br`, `index.html.gz`), plus the profile pages `account/index.html` and `chat/index.html` and `_headers`. The served count is therefore 174. +- The platform limit, taken from the Cloudflare Workers platform limits page (Static Assets section, read on 2026-10-09): "Files per Worker version" is 20,000 on Workers Free and 100,000 on Workers Paid, and "Individual file size" is 25 MiB on both plans. The page does not say whether precompressed variants count. The gate uses the stricter 20,000. +- Result: 174 files against 20,000 (0.87 percent), so the Blazor profiles are inside the limit whether or not the precompressed variants count. The Site adds nine files (`artifacts/site`). Even if the two outputs were served from one Worker version, the total is 183 files, 0.92 percent of the limit. The largest single static file is `dotnet.native` (`.wasm`), 3,001,422 bytes uncompressed, 11.4 percent of the 25 MiB single-file limit (measured at `31888e1`; see "Largest single static file (review fix 6)"). + +## WA-08 costing of the non-virtualised lists + +- Chat transcript (`src/ArcForges.Web.App/Pages/Chat.razor`): an `ol` rendered with `@foreach` over `_entries`, each entry an `li` with one `span` and text. The list is capped: `MaxEntries = 20`, and the oldest entries are removed when the cap is exceeded. The list therefore holds at most 20 `li` and 20 `span` elements, 40 elements and 40 text nodes, and is bounded by construction. No virtualisation is needed (Virtualize is refused by the strict `style-src`, and CS-01 states the .NET 10 position). +- Library lists: the Web App at `c8588681` has no library list, and the Account page renders no list (only a workspace count). No library-list cost is claimed here. +- Sustained chat memory (WA-08): WA-08 says the sustained chat memory has a recorded budget. The numeric value was not found in the Design authority (`ArcForges-Design` `main`, `b01ae31`) or in the Plan and Web records. The costing is therefore by construction only (the 20-entry cap above). The numeric sustained-memory budget and any measured memory for the chat profile are open and are not claimed. They need the recorded WA-08 value, and a measurement on the deployed profile would be a local opt-in run (blocked on CLOUD.85). + +## Not claimed + +- No startup, INP or AOT number is claimed. Those are blocked on CLOUD.85 or are not run. +- No deployed file count is claimed. The count above is for the local publish and the local bundle. +- The WA-08 numeric sustained chat memory budget is not claimed. It is open pending the recorded value. +- No deployed measurement is claimed. The head measurement is local and claimant-reported. diff --git a/docs/prf-11-profile-proof.md b/docs/prf-11-profile-proof.md new file mode 100644 index 0000000..63d905c --- /dev/null +++ b/docs/prf-11-profile-proof.md @@ -0,0 +1,386 @@ +# PRF.11 Blazor WebAssembly production profile and generated C# SDK proof + +Reviewer: `w-deku-20261008-rev-prf-11` (pre-assigned, brief S25). Decision: `approved`. reviewedOn: `2026-10-09`. These fields are written at write time as a proposal. Only the named reviewer's exact-head approval ratifies them, and a refusal blocks the merge. + +This is the record of the PRF.11 proof for the Web repository (Design WP-06.05 full, WP-06 package-level contribution, PG-23 foundation contribution). It supersedes PRF.08 (the React proof, `docs/prf-08-profile-proof.md`), whose evidence is not reused. Entries are added as each unit lands. A claim is recorded only with the evidence that backs it. Anything that needs a deployed ingress is recorded as "blocked on CLOUD.21/CLOUD.22, not proven" or as "blocked on CLOUD.85, not proven", never as deferred and never as proven. + +## Start identity (U1) + +| Item | Value | +| --- | --- | +| Task | PRF.11, lane runtime-proofs, kind proof, size L | +| Repository and branch | Web, `task/prf-11`, local worktree `.worktree/prf-11` | +| Claim | epoch 1, worker `w-deku-20261009-prf-11` (coordinator-held claim) | +| Pre-assigned reviewer | `w-deku-20261008-rev-prf-11` (brief section 10, S25); written into every record with `decision: approved` and `reviewedOn` at write time | +| Base | `origin/main` c8588681 (merge of PR #35 from `task/web-40`, WEB.40) | +| WEB.40 status | `delivered` in the ledger (`ledger/tasks/web-40.md` on Plan main); merged, deployed and released as `web-0.1.0-ci.111.1` (S28(a)) | +| Plan authority | the PRF.11 entry of `phase2-plans.json` (units U1 to U9, defaults D1 to D12), the task record, and brief sections 0, 0a, 0b, 10 and 11 | + +Base refresh (fix3, 2026-10-10): `origin/main` is now `049f6a5` (Web #38, the WEB.40 completion follow-up for S36 and S37). It is merged into `task/prf-11` as `f4c639c`. The start identity above is the original base and stays as history. + +### Start edges + +| Start edge | Ledger status | Evidence used by this proof | +| --- | --- | --- | +| CON.92 (serialization posture and decode limits) | `inherited` (WP03.02, accepted source e6c4a77) | The decode limits are part of the Contracts source at the pinned identity. Contracts `docs/architecture.md`, "Serialization posture", at ca45f36 states the registry limits: 4 MiB unary and helper messages, 256 KiB inline pages, 32 KiB stream frames, 64 MiB large read projections, and at most 100 nested message levels, with typed `tooLarge`, `tooDeep` and `malformed` refusals. Source e6c4a77 is an ancestor of ca45f36. The P0c precondition holds. | +| WEB.40 (Blazor profile projects and C# static Site generator) | `delivered` | `src/ArcForges.Web.App`, `src/ArcForges.Web.Site`, `tools/ArcForges.Web.Tooling` at c8588681. | +| PRF.07 (deployed Cloud AOT probe reachable same-origin) | `complete` | Consumed only through the deployed ingress in the live units. No substitute probe is used. | +| CON.07 (BrowserSession and NativeAuth HTTP exceptions and generated C# codecs) | `complete` | The NuGet identity is `ArcForges.Contracts.PublicApi 1.0.0-ci.287.1` from source ca45f36cccbdd31380f76b8f6cdecc958fdcb430. That commit is an ancestor of Contracts `origin/main`. Build run 36966686225 published it to NuGet, npm and Maven. The pin is in `Directory.Packages.props` and is unchanged by U1. | + +### Completion edges (blocked, not proven) + +| Completion edge | Ledger status at start | Consequence for PRF.11 | +| --- | --- | --- | +| CLOUD.21 (real generated public methods on the deployed gRPC-Web ingress) | not started (no ledger record; waits on CLOUD.13) | Exact int64, uint64 and decimal calls, and the `WorkspaceService.GetHealth` and `UpdateSettings` round trips, are blocked on CLOUD.21, not proven. | +| CLOUD.22 (ArcResult and gRPC-Web status and trailer mapping) | not started (waits on CLOUD.21) | Typed failures, deadline and cancel-after-dispatch are blocked on CLOUD.22, not proven. | +| CLOUD.08 (readiness surfaces) | `complete` | Readiness observation is available for the live units. | +| CLOUD.71 (proof-origin route families for the React bytes) | `complete` (history for the React bytes only) | The `/api`, `/session/v1` and `/proof/v1` route precedence is not proven for the Blazor bytes. D11 treats the graph inconsistency as history (brief S16(a)). | +| CLOUD.85 (proof-origin serving of the Blazor Account and Chat shells and the C# Site, under base href "/", CLOUD.85 D1) | not complete at this record; its S36 follow-up Cloud #82 pins the corrected profile policy, and its proof redeploy is not observed here | Same-origin serving, the `/account/` and `/chat/` shells, digest-verified bytes and the exact CSP on served responses are blocked on CLOUD.85, not proven. PRF.11 cannot complete until CLOUD.85 delivers and its checks pass (S27(b)). | + +PRF.11 is delivered on its offline units U1 to U9 (brief S20(d)). Its completion is still blocked on CLOUD.21 and CLOUD.22, and the parts that need them are recorded as "blocked on CLOUD.21/CLOUD.22, not proven". Its local opt-in live run against proof.arcforges.com follows the CLOUD.85 follow-up proof redeploy. The `base-uri` stop recorded at fix1 and fix2 is resolved by S36 (see the resolved decision below). + +### Shell and base href + +The Account and Chat profile shells are served at `/account/` and `/chat/` under base href `"/"` (CLOUD.85 D1, brief S27(b)). The framework files sit at the root. The host page `src/ArcForges.Web.App/wwwroot/index.html` already carries ``. The earlier base href wording `/account/` and `/chat/` is superseded. + +## Inherited WEB.40 surfaces + +These surfaces exist at c8588681 and are extended by PRF.11, not duplicated: + +- `src/ArcForges.Web.App`: the Account and Chat Razor pages, the `ProfileLayout`, the `HelloProbe`, `SessionProbe`, `ExactUnsigned` and `BoundedBody` probe classes, and the standalone profile project with `RunAOTCompilation` false. +- `src/ArcForges.Web.Site`: the deterministic static Site generator (`/`, `/hello/` and `/cloud-hello/` as static pages; no runtime JavaScript). +- `tools/ArcForges.Web.Tooling`: the candidate builder and verifier, the Site archive, the profile bundle (`profiles bundle` and `profiles verify`), and the profile budget tool (`profiles budget`). +- `tests/ArcForges.Web.App.Tests`, `tests/ArcForges.Web.Site.Tests`, `tests/ArcForges.Web.Tooling.Tests`, and the local opt-in browser project `tests/browser/ArcForges.Web.Browser.Tests` (Microsoft.Playwright for .NET, never built in CI). +- `eng/policy/profile-budgets.json`: the asset budgets for the `account` and `chat` profiles, with `interactionBudgets` status `re-baseline-pending`, owner PRF.11. +- The Ui project `src/ArcForges.Web.Ui` (including `WasmContentSecurityPolicy`) is outside PRF.11's write scope. It is consumed, not changed. + +## Declared write scope + +The write scope is the PRF.11 `writes` list in the task record, with the D3 additions (brief S20(b), S27(f)): `eng/policy/profile-budgets.json`, `tools/ArcForges.Web.Tooling/**`, `tests/ArcForges.Web.Tooling.Tests/**` and `tests/ArcForges.Web.Site.Tests/**`. `src/ArcForges.Web.Ui/**`, its tests and `.github/workflows/ci.yml` are not in scope. An in-browser CSP or framing change that needs a Ui edit stops the task for a new decision. + +## Gaps in the plan recorded at start + +- The published output of the App publish is produced by CI after the test step, so tests that read emitted files need a local publish first or are covered through the bundle tooling tests. This is recorded in the U2 section. +- Node is 24.20 locally against the 24.21 pin, so `npm run policy` is recorded as not run locally (hosted CI is authoritative). This is an environment gap and not a fix. +- The WSL2 Debian SDK is 10.0.400, so a locked Linux restore fails with NU1004 there. Linux checks are not run in this phase unless they are affected. +- Docker is not on the Windows PATH. gitleaks runs in WSL Debian through docker, with the cached image `c00b6bd0aeb3` (the pinned digest) and `--network none`. +- actionlint 1.7.12 is the local Windows binary built from source in the Go bin folder. Its version matches the CI pin. + +## Local CI conditions used for the gates + +The gates run the workflow's commands with `GITHUB_ACTIONS=true`, `CI=true` and `NUGET_PACKAGES` set to an empty folder under the session scratchpad, so every locked restore downloads from the locked feeds. The ArcForges.Build.Policy analyzer (AFP006) requires a complete CI identity under `GITHUB_ACTIONS=true`, so the gate sets `GITHUB_SHA` to the commit under test, `GITHUB_REPOSITORY` to `ArcForges/Web` (the analyzer builds the pipeline-run URL from it, and without it the build fails with AFP006), and `GITHUB_RUN_ID`, `GITHUB_RUN_ATTEMPT` and `GITHUB_RUN_NUMBER` to the placeholder `1`. These placeholders are local emulation values. They name no hosted run, and no artifact from these local gates is published or deployed. Hosted CI remains the authority for the release identity. + +The gates also start from a clean state: `git clean -fdX` removes only ignored build output (`bin`, `obj`, `node_modules` and `artifacts`) before the run. This matters because stale files left in `artifacts/publish/app` by an earlier publish fail the profile bundle's static-graph check ("A published file is not named by the build's static web assets manifest"), which a fresh CI checkout never meets. + +## CSP and profile shells (U2) + +The emitted policy is the one string in `WasmContentSecurityPolicy` for the host page, pinned in `ArcForges.Web.App.Tests` (`WasmProfilePolicyTests.ExactPolicy`) and in `ArcForges.Web.Tooling.Tests` (`EmittedProfileContractTests`): + +- `default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'none'` + +`script-src` is exactly `'self'` and `'wasm-unsafe-eval'` (the host page has no inline script, so no hash is added). `style-src` is `'self'`. No `'unsafe-inline'`, `'unsafe-eval'` or `'unsafe-hashes'` appears in the policy or the headers file. The bundle test builds the profile bundle from a publish whose shell is the real host page, then reads the served `account/index.html` and `chat/index.html` (identical bytes, one `` each) and the `_headers` file, which names the exact policy on `/account/*` and `/chat/*`. + +Resolved (S36, WEB.40 follow-up Web #38): the profile policy carries `base-uri 'self'`, so the `` of the shells is admitted. The earlier open point, under `base-uri 'none'`, is history and is kept in the open-decision section below. The Site policy keeps `base-uri 'none'`, because the public pages have no base element. + +## Exact values and CON.92 limits (U3) + +The exact-value helpers come from the pinned Contracts Foundation package (`ArcForges.Contracts.Foundation` 1.0.0-ci.287.1), not from new App helpers: + +- `ExactInteger.ParseInt64` and `ExactInteger.ParseUInt64` (canonical text, no sign, leading zero or floating point; int64 edges and values above 2^53 stay exact). The App's own `Exact.TryUnsigned` is the probe's uint64 path. +- `ExactDecimal` (the shared exact decimal bound: at most nine fractional digits and twenty-eight significant digits, declared scale kept, no negative zero). + +The CON.92 registry bounds are read from `WireLimits.Bytes` and pinned: unary and helper messages 4 MiB, inline pages 256 KiB, stream frames 32 KiB, large read projections 64 MiB, and 100 nested message levels. `WireLimitTests` decodes at the exact bound (accepted) and one byte over (`TooLarge`), shows that the same bytes are admitted as a unary message and refused as a stream frame, refuses truncated, bare-tag and reserved-wire-type frames as `Malformed`, and checks that encoding refuses one byte over the class. + +`HelloProbe` now sets the gRPC channel's `MaxReceiveMessageSize` and `MaxSendMessageSize` to the unary class (4 MiB), so the greeting, a unary call, is bounded explicitly rather than by the library default. Two tests cover the two bounds, with the following limits (review fix 2): + +- The send bound is discriminated. The Grpc.Net.Client default for `MaxSendMessageSize` is null (no bound). `HelloProbeTests.AGreetingRequestAboveTheUnaryBoundIsRefusedBeforeItsBytesReachTheTransport` sends a name one byte over the class, and it fails when the explicit line is removed (mutation run: the oversized body reaches the transport and the test fails; with the line restored it passes, 18 of 18 in the targeted run with the stream tests). +- The receive bound is not discriminated. The library default for `MaxReceiveMessageSize` is 4,194,304 bytes (Grpc.Net.Client 2.84.0 documentation), which is the same value as the unary class, so removing the explicit line is not observable offline. `HelloProbeTests.AGreetingReplyAboveTheUnaryMessageBoundIsRefusedNeverAccepted` pins the behaviour of the bound (a reply over the class is `Malformed` and never a greeting), not the explicit option. The explicit receive line is recorded as a value pin, not as proven. + +## Static Site: no-script reading and determinism (U5) + +- Determinism: the Site is built twice with the candidate command (`site build --out ... --source-ref `) and the two output trees are identical (`diff -r`, nine files, including `_headers` and the content-hashed stylesheet). The hosted csharp job repeats this as a gate. +- Offline, CI-eligible: `SiteOutputTests.EveryPageIsReadableWithScriptingDisabled` (existing) scans every page for script elements, event-handler attributes, framework references and `javascript:` URLs. The new `SiteOutputTests.TheOutputCarriesNoScriptOrWebAssemblyFileAtAll` refuses any `.js`, `.mjs` or `.wasm` output file and any script element in the HTML. The Site test run in the local gate passed 75 cases, skipped 1 (`SiteParityTests.TheCSharpSiteAgreesWithTheRecordedReactPrerenderWhenItIsNamed`, which is skipped by design unless a React prerender is named), and failed none. +- Local opt-in, not CI: `tests/browser/ArcForges.Web.Browser.Tests/LocalNoScriptBrowserTests.cs` reads `/`, `/hello/` and `/cloud-hello/` with JavaScript disabled and asserts a 200 status, non-empty `main` text, no script element, and no script request. It skips with the opt-in unset. Under CI conditions it skipped (the opt-in gate also refuses any CI host). +- The one local opt-in run: the Site output was served from `artifacts/site` on `127.0.0.1` (a local static server, not a deployed origin). The browser was the installed Google Chrome, `C:\Program Files\Google\Chrome\Application\chrome.exe`, product version 156.0.8078.12, named by `ARCFORGES_CHROMIUM_PATH`. No browser was downloaded. The test passed: 1 of 1, 4.5 s. Host Windows 11, Microsoft.Playwright 1.62.0 driving the installed binary by path (D10). The match between this Chrome build and the Chromium that Playwright 1.62.0 expects was not verified, so it is recorded as a mismatch risk. Claimant-reported; date 2026-10-09. +- Not claimed: the no-script reading of the deployed proof origin (blocked on CLOUD.85, not proven). + +## Inline-style audit, in-browser CSP and live specs (U7) + +- Inline-style audit (static, offline): no `style=` attribute and no `