diff --git a/.planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-06-PLAN.md b/.planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-06-PLAN.md index be62bef..988aeba 100644 --- a/.planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-06-PLAN.md +++ b/.planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-06-PLAN.md @@ -58,6 +58,7 @@ files_modified: - ../vue-journal-layer/package.json - .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-REVIEW-DISPOSITION.md - .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-VALIDATION.md + - .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-06-PROGRESS.md autonomous: false requirements: [SC-1, SC-3, SC-4, D-03, D-04, D-05, D-10, D-11, D-12, D-17, D-18] estimate: @@ -67,7 +68,7 @@ estimate: confidence: low must_haves: truths: - - "SC-1 / D-05 (CR-01, the verification gap): every response of a route that names cors:, on the route or on an enclosing group, carries the profile's Access-Control-Allow-Origin (the exact allowed origin), Access-Control-Allow-Credentials and Vary: Origin whatever the entry's position in the list: a 401 from a guard listed before it or set on the group, a 429 from throttle, a 413 from a middleware reading an over-limit body, and a recovered panic 500 in JSON and raw groups; a disallowed origin gets Vary: Origin and no Allow-Origin; a handler's own Vary is merged, not replaced." + - "SC-1 / D-05 (CR-01, the verification gap): every response of a route that names cors:, on the route or on an enclosing group, carries the profile's Access-Control-Allow-Origin (the exact allowed origin), Access-Control-Allow-Credentials and Vary: Origin whatever the entry's position in the list: a 401 or 403 from a guard listed before it or set on the group, a 422 or 503 from turnstile:closed, a 429 from throttle, a 413 from a middleware reading an over-limit body, and a recovered panic 500 in JSON and raw groups; a disallowed origin gets Vary: Origin and no Allow-Origin; a handler's own Vary is merged, not replaced." - "CR-01 gate and docs: TestCORSProfileHeadersOnRefusals and TestCORSProfileVaryKeptWithHandlerVary are required by name in SURF_TESTS of scripts/check-phase19.3.sh; docs/services/routing.md and modules/surf/README.md state that the profile applies to every response of the route whatever its position in the list or the group nesting." - "D-04 (WR-04): with supports_credentials true, an allowed_origins entry whose wildcard is not a whole leftmost host label (https://*, *://*, https://*example.com, a * in the scheme), an allowed_origins_patterns entry without ^ and $, and any origin pattern that matches a probe origin outside the configured domains (https://*.com, ^https?://.*$) fail boot naming the key and index, globally and per profile; credentialed https://*.example.com and anchored patterns keep loading; non-credentialed configs load exactly as before and TestCORSOptionsMatchesLaravel stays green." - "D-03 (WR-05, WR-09): in the production environment LoadCookieConfig refuses a token cookie whose Secure resolves to false whether auth.cookie.secure was set or inferred from an http or unset app.url, with an error naming auth.cookie.secure and app.url; the zero CookieConfig writes token, Path=/, HttpOnly, SameSite=Lax and Secure, and any config with SameSite unset writes SameSite=Lax." @@ -150,7 +151,7 @@ This plan's slice (plan 6, gap closure, wave 4): close the one verification gap | Missing item | Task | |--------------|------| | `Router.wrap` skips the `cors:` entry and applies `r.corsProfiles[profile].profileMiddleware` after recovery, just inside `recordPattern` | Task 1 | -| Test: guard listed before `cors:spa` → 401 with ACAO, ACAC, `Vary: Origin`; same for a body-limit 413 and a recovered 500; in SURF_TESTS | Task 1 (plus group-level guard and 429) | +| Test: guard listed before `cors:spa` → 401 with ACAO, ACAC, `Vary: Origin`; same for a body-limit 413 and a recovered 500; in SURF_TESTS | Task 1 (plus a group-level guard, a 403, a 429 and the Turnstile 422 and 503) | | docs/services/routing.md and modules/surf/README.md: the profile applies to every response regardless of position | Task 1 | Review warnings: WR-04, WR-05, WR-09 in Task 2; WR-06, WR-07, WR-08 in Task 3; WR-02, WR-01 in Task 4; WR-03 in Task 5. Unit-test breadth and gate wiring in Task 6; release in Task 7. IN-01..IN-11 are out of scope (user decision). @@ -172,6 +173,15 @@ Review warnings: WR-04, WR-05, WR-09 in Task 2; WR-06, WR-07, WR-08 in Task 3; W Detector signal: `chosen` on "configurable" (SC-1 cookie attributes). Primary noun: the resolved token-cookie attributes (`bouncer.CookieConfig`). Decision: no-change. The attributes have been configurable since plan 02; this plan only tightens the derived Secure value in production (WR-05) and fills the zero-config defaults (WR-09). +## Context budget and resume point + +The plan stays one plan (user decision), so its estimate exceeds one executor context. It runs in at most two contexts, split after Task 4. Tasks 1-4 hold the six framework fixes and the three plugin repositories; Tasks 5-7 hold the layers, the unit-test tables, the full gate and the release. + +- **Record.** The last step of Task 4 writes `.planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-06-PROGRESS.md` and commits it as a planning-docs commit. The record is deliberately not a SUMMARY. execute-phase treats a plan that has a `-SUMMARY.md` as finished, so a partial SUMMARY would skip Tasks 5-7 and the release checkpoint. +- **Resume rule.** Before Task 1, an executor of this plan checks for the record. When the record exists and `19.3-06-SUMMARY.md` does not, it does three things. It confirms every commit the record lists (`git -C cat-file -e `). It does not redo the tasks the record marks done. It starts at the task named on the record's `next:` line. A later task is skipped only when its commits exist (subject with `(19.3-06)` and the finding ID) and its acceptance criteria hold. A partly done task resumes at its first missing commit. +- **After a reset.** If the executor's context ends after Task 4, execute-phase's resume gate finds plan-06 commits without a SUMMARY and asks how to recover. The fitting answer is a continuation executor: its completed tasks are the record's table and its resume point is the `next:` task. Re-executing from scratch would duplicate the plugin commits in the sibling repositories. +- **Close-out.** The SUMMARY carries over the record's commits, verify results and notes. The record stays as written. + ## Artifacts this phase produces (this plan) - surf: `Router.wrap` applies `cors:` outermost (inside `recordPattern`); `bufferedResponse.commit` merges `Vary`; credentialed origin rules in `validateCORSConfig` (probe origins); api-docs page with a nonce on every script and a nonce-only `script-src`; boot warning `surf: turnstile.secret is empty` with a route count. @@ -206,7 +216,7 @@ Output: commits in summercms.go, the three plugins (tests only) and the three la @.planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-04-SUMMARY.md @.planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-05-SUMMARY.md -Rules: one commit per finding where practical, README and docs in the same commit as the code they describe, planning docs in separate commits; stage by explicit path, never `git add -A`; no co-author tags. Before each task run `git status --porcelain` on the paths it touches and stop on foreign uncommitted edits (other sessions work in this checkout; `.planning/state.json` and `.msd/` are theirs, leave them alone; sm-journal-plugin's untracked go.work.sum stays untouched). Framework commits land before the plugin and layer commits that depend on them. The plugins build against this checkout through their checked-in go.work. Docker suites skip under `-short`; run them on a Docker host (`sudo -u golem` when the socket is denied) and record NOT RUN when no daemon answers. Framework code and docs never name a consuming application (the gate's docs stage scans added lines). Nothing is pushed or tagged before the Task 7 reply. State updates go through `msd_run query state.*` handlers. +Rules: one commit per finding where practical, README and docs in the same commit as the code they describe, planning docs in separate commits; stage by explicit path, never `git add -A`; no co-author tags. Before each task run `git status --porcelain` on the paths it touches and stop on foreign uncommitted edits (other sessions work in this checkout; `.planning/state.json` and `.msd/` are theirs, leave them alone; sm-journal-plugin's untracked go.work.sum stays untouched). Framework commits land before the plugin and layer commits that depend on them. The plugins build against this checkout through their checked-in go.work. Docker suites skip under `-short`; run them on a Docker host (`sudo -u golem` when the socket is denied) and record NOT RUN when no daemon answers. Framework code and docs never name a consuming application (the gate's docs stage scans added lines). Nothing is pushed or tagged before the Task 7 reply. State updates go through `msd_run query state.*` handlers. Before Task 1, apply the resume rule in "Context budget and resume point" (19.3-06-PROGRESS.md). Seams to change (read them; do not re-derive): @@ -225,21 +235,24 @@ Seams to change (read them; do not re-derive): - Task 1: Tracer: a cross-origin SPA reads every refusal of a cors:<profile> route; Router.wrap applies the profile outermost, so a 401 from a guard listed before it or set on the group, a 429, a 413 and a recovered 500 all carry Allow-Origin, Allow-Credentials and Vary: Origin (CR-01) + Task 1: Tracer: a cross-origin SPA reads every refusal of a cors:<profile> route; Router.wrap applies the profile outermost, so a 401 or 403 from a guard listed before it or set on the group, a Turnstile 422 or 503, a 429, a 413 and a recovered 500 all carry Allow-Origin, Allow-Credentials and Vary: Origin (CR-01) Internal composition order in wrap; the cors:<profile> name and the http.cors_profiles keys are unchanged. `git status --porcelain -- modules/surf docs/services/routing.md scripts/check-phase19.3.sh` in summercms.go prints nothing. modules/surf/router.go, modules/surf/cors.go, modules/surf/cors_profile_test.go, modules/surf/README.md, docs/services/routing.md, scripts/check-phase19.3.sh - modules/surf/router.go (wrap :404-473, BuildRouter cors factory :521-527, recoverJSON, recoverBare and bufferedResponse.commit :735-805), modules/surf/cors.go (corsPolicy.apply, addVary, profileMiddleware, routeCORSProfile, addCORSPreflights), modules/surf/accesslog.go (recordPattern), modules/surf/bodylimit.go, modules/surf/cors_profile_test.go (the helpers in the interfaces block, TestCORSProfilePreflightRoute), modules/surf/transport_smoke_test.go (TestCORSProfileSmoke), modules/surf/cors_coverage_test.go (TestCORSOptionsMatchesLaravel), docs/services/routing.md (middleware table :101, CORS profiles :252-270), modules/surf/README.md (:30-31), scripts/check-phase19.3.sh (SURF_TESTS), 19.3-REVIEW.md (CR-01), 19.3-VERIFICATION.md (gap missing list) + modules/surf/router.go (wrap :404-473, BuildRouter cors factory :521-527, recoverJSON, recoverBare and bufferedResponse.commit :735-805), modules/surf/cors.go (corsPolicy.apply, addVary, profileMiddleware, routeCORSProfile, addCORSPreflights), modules/surf/accesslog.go (recordPattern), modules/surf/bodylimit.go, modules/surf/cors_profile_test.go (the helpers in the interfaces block, TestCORSProfilePreflightRoute), modules/surf/transport_smoke_test.go (TestCORSProfileSmoke, turnstileRoutes, TestTurnstileMiddlewareSmoke: the published verifier and the production app), modules/bouncer/turnstile.go (Verify: an empty token is refused before any siteverify call), modules/surf/cors_coverage_test.go (TestCORSOptionsMatchesLaravel), docs/services/routing.md (middleware table :101, CORS profiles :252-270), modules/surf/README.md (:30-31), scripts/check-phase19.3.sh (SURF_TESTS), 19.3-REVIEW.md (CR-01), 19.3-VERIFICATION.md (gap missing list) Per D-05 (CR-01, the verification gap). router.go wrap: before the middleware loop, resolve the route's profile with routeCORSProfile(rt) (return its error as is) and keep today's unknown-profile boot error with its message. Inside the loop, an entry whose base is cors (with a param) is validated and then skipped, so it is never composed at its list position. Group entries arrive through rt.middleware, so group nesting needs nothing extra. After the recovery wrapper (recoverJSON or recoverBare) and before recordPattern, wrap h in r.corsProfiles[profile].profileMiddleware when the route names a profile. Preflight routes keep their current path. Keep the cors factory registration in BuildRouter (the name must keep resolving and stay reserved for surf) and change its comment to say wrap applies the profile itself. bufferedResponse.commit: copy every buffered header key as today except Vary. Merge each comma-separated Vary value into the destination through addVary, so a Vary: Origin the profile set on the outer writer survives a handler that sets its own Vary. Preflights never pass through commit, so the bytes TestCORSOptionsMatchesLaravel pins must not change. Change the profileMiddleware doc comment: it runs outermost, outside the body limit, locale and recovery. -Test in cors_profile_test.go, reusing the helpers: TestCORSProfileHeadersOnRefusals with spaProfileYAML, no http.cors, and a test plugin that registers two named middlewares: deny, which answers 401 without calling next, and readall, which reads the whole body and answers 413 on *http.MaxBytesError. Subtests, each sending Origin https://www.example.com: +Test in cors_profile_test.go, reusing the helpers: TestCORSProfileHeadersOnRefusals with spaProfileYAML, no http.cors, and a test plugin that registers three named middlewares: deny, which answers 401 without calling next, forbid, which answers 403 without calling next, and readall, which reads the whole body and answers 413 on *http.MaxBytesError. Subtests, each sending Origin https://www.example.com: - route [deny, cors:spa]: 401; +- subtest guard_403, route [forbid, cors:spa]: 403; - a group Use(deny) whose route names cors:spa: 401; - [throttle:1,1, cors:spa]: the second request gets 429; - [body.limit:16, readall, cors:spa] with a 64-byte body: 413; - a handler that panics behind cors:spa: 500 (JSON); -- a GroupRaw with cors:spa and a panicking handler: 500. -Each asserts Access-Control-Allow-Origin equal to the origin, Access-Control-Allow-Credentials true and Vary containing Origin. One more case: [deny, cors:spa] with Origin https://evil.example.org gives 401 with no Allow-Origin and with Vary containing Origin. TestCORSProfileVaryKeptWithHandlerVary: a handler behind cors:spa sets Vary: Accept-Language, and the response's Vary names both Origin and Accept-Language. Add both names to SURF_TESTS in scripts/check-phase19.3.sh. +- a GroupRaw with cors:spa and a panicking handler: 500; +- subtest turnstile_422: an app with a published bouncer.NewTurnstile("test-secret", false), as TestTurnstileMiddlewareSmoke publishes one, and a POST route [turnstile:closed, cors:spa]; a request without a token answers 422. The empty token is refused before any siteverify call, so no fake server is needed; +- subtest turnstile_503: transportApp in the production environment with no turnstile.secret, POST route [turnstile:closed, cors:spa]: 503. +The two Turnstile subtests assemble their own app, because BuildRouter builds one verifier per router. Every status the docs sentence below names (401, 403, 422, 503, 429, 413, 500) has a subtest here. Each asserts Access-Control-Allow-Origin equal to the origin, Access-Control-Allow-Credentials true and Vary containing Origin. One more case: [deny, cors:spa] with Origin https://evil.example.org gives 401 with no Allow-Origin and with Vary containing Origin. TestCORSProfileVaryKeptWithHandlerVary: a handler behind cors:spa sets Vary: Accept-Language, and the response's Vary names both Origin and Accept-Language. Add both names to SURF_TESTS in scripts/check-phase19.3.sh. Docs, in the same commit: - docs/services/routing.md "CORS profiles": replace the sentence that tells readers to list the profile beside the guard. The new text says the route (or its group) lists cors:spa among its middleware names, and surf applies the profile outside every other layer of the route, whatever its position in the list or the group nesting. Every response of the route therefore carries the profile's headers, including refusals from guards (401, 403), throttle (429) and Turnstile (422, 503), the body limit (413) and recovered panics (500); otherwise the browser would hide them from the page. Shorten the same statement into the cors:<profile> row of the middleware table. @@ -247,11 +260,12 @@ Docs, in the same commit: Commit: `fix(19.3-06): cors: headers on every response of the route (CR-01)`. - go vet ./... && go test -count=1 -short ./modules/surf && go test -count=1 -short -run '^(TestCORSProfileHeadersOnRefusals|TestCORSProfileVaryKeptWithHandlerVary|TestCORSOptionsMatchesLaravel|TestCORSProfilePreflightRoute|TestCORSProfileSmoke)$' -v ./modules/surf 2>&1 | grep -cE '^--- PASS: Test[A-Za-z]+ \(' | grep -qx 5 && bash scripts/check-phase19.3.sh go && go test -count=1 -run '^TestDocsTree$' ./cmd/summer - Non-zero exit: vet or the surf short suite is red, fewer than five top-level "--- PASS" lines (a refusal still lacks the profile headers, a handler's Vary erased Origin, the Laravel preflight bytes changed or the profile smoke broke), the gate's go stage refuses (a SURF_TESTS name missing, skipped or red), or the docs checker reports a problem. + go vet ./... && go test -count=1 -short ./modules/surf && go test -count=1 -short -run '^(TestCORSProfileHeadersOnRefusals|TestCORSProfileVaryKeptWithHandlerVary|TestCORSOptionsMatchesLaravel|TestCORSProfilePreflightRoute|TestCORSProfileSmoke)$' -v ./modules/surf 2>&1 | grep -cE '^--- PASS: Test[A-Za-z]+ \(' | grep -qx 5 && go test -count=1 -short -run '^TestCORSProfileHeadersOnRefusals$/^(guard_403|turnstile_422|turnstile_503)$' -v ./modules/surf 2>&1 | grep -cE '^\s+--- PASS: TestCORSProfileHeadersOnRefusals/(guard_403|turnstile_422|turnstile_503) \(' | grep -qx 3 && bash scripts/check-phase19.3.sh go && go test -count=1 -run '^TestDocsTree$' ./cmd/summer + Non-zero exit: vet or the surf short suite is red, fewer than five top-level "--- PASS" lines (a refusal still lacks the profile headers, a handler's Vary erased Origin, the Laravel preflight bytes changed or the profile smoke broke), fewer than three passing guard_403, turnstile_422 and turnstile_503 subtests (a 403 or Turnstile refusal the docs name is untested or lacks the headers), the gate's go stage refuses (a SURF_TESTS name missing, skipped or red), or the docs checker reports a problem. - `grep -c 'TestCORSProfileHeadersOnRefusals\|TestCORSProfileVaryKeptWithHandlerVary' scripts/check-phase19.3.sh` prints 2. + - `grep -cE '"(guard_403|turnstile_422|turnstile_503)"' modules/surf/cors_profile_test.go` prints at least 3, and the verify step shows each of the three subtests passing (every status the routing.md sentence names has a subtest). - `grep -c 'profileMiddleware' modules/surf/router.go` prints at least 2 (the factory and the wrap call). - `grep -n 'whatever its position' docs/services/routing.md modules/surf/README.md` prints at least one line per file. - `grep -c 'next to its guard' docs/services/routing.md` prints 0. @@ -348,7 +362,7 @@ Regression in cors_profile_test.go: TestTurnstileEmptySecretWarnsAtBoot. Publish Task 4: An admin save no longer waits for the site, and a bulk delete sends at most two purges: the purge client never follows redirects (WR-02), and PurgeSiteCacheAfterCommit queues onto a coalescing per-purger worker that Wait and shutdown drain (WR-01) cooler.SiteCachePurger.Wait joins cooler's published API and the plugin tests call it; reshaping it later needs a coordinated plugin release. `git status --porcelain -- modules/cooler docs/services/caching.md` in summercms.go prints nothing, and `git -C ../sm-faq-plugin status --porcelain`, `git -C ../sm-menu-plugin status --porcelain` and `git -C ../sm-journal-plugin status --porcelain -uno` print nothing. - modules/cooler/sitecache.go, modules/cooler/cooler.go, modules/cooler/sitecache_test.go, modules/cooler/README.md, docs/services/caching.md, ../sm-faq-plugin/site_cache_admin_test.go, ../sm-menu-plugin/site_cache_admin_test.go, ../sm-journal-plugin/site_cache_admin_test.go, ../sm-journal-plugin/classes/site_cache_test.go + modules/cooler/sitecache.go, modules/cooler/cooler.go, modules/cooler/sitecache_test.go, modules/cooler/README.md, docs/services/caching.md, ../sm-faq-plugin/site_cache_admin_test.go, ../sm-menu-plugin/site_cache_admin_test.go, ../sm-journal-plugin/site_cache_admin_test.go, ../sm-journal-plugin/classes/site_cache_test.go, .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-06-PROGRESS.md modules/cooler/sitecache.go (whole), modules/cooler/cooler.go (Handle, Handle.Close :110-117, Install :163-215), modules/surf/serve.go (:40-120, coolerHandle.Close at shutdown), modules/lagoon/transaction.go (AfterCommit, runAfterCommit), modules/cooler/sitecache_test.go (purgeReceiver, newTestPurger, captureLogs, the outside-a-transaction subtest :374-381, TestSiteCacheAfterCommitOnly :445, TestSiteCacheFailureNeverFails :519), modules/cooler/README.md (:39, :175-180), docs/services/caching.md (Site cache purge :100-140), ../sm-faq-plugin/site_cache_admin_test.go, ../sm-menu-plugin/site_cache_admin_test.go, ../sm-journal-plugin/site_cache_admin_test.go, ../sm-journal-plugin/classes/site_cache_test.go (:205-216), ../sm-faq-plugin/controllers/site_cache.go and ../sm-journal-plugin/classes/site_cache.go (call sites that stay unchanged), 19.3-REVIEW.md (WR-01, WR-02) Framework first, two commits. @@ -392,7 +406,16 @@ Commit: `fix(19.3-06): queue site-cache purges after commit and coalesce them (W Plugins, test-only, one commit per repository after the framework commit; plugin controllers and the Journal aliases stay untouched: - In site_cache_admin_test.go of sm-faq-plugin, sm-menu-plugin and sm-journal-plugin, every read of the purge receiver first drains the purger used by the app under test. That covers take, expectOnePurge, expectNone and the direct take calls. Drain through cooler.SiteCacheFromApp(the harness's app).Wait with a 5-second context, so each save's purge is asserted after it was sent. The probe and Seen assertions stay. - In sm-journal-plugin classes/site_cache_test.go, the outside-a-transaction subtest waits before reading hits, and its name says the purge is queued at once. -Each commit: `test(19.3-06): wait for the queued site-cache purge before asserting (WR-01)`, staged by explicit path. +Each commit: `test(19.3-06): wait for the queued site-cache purge before asserting (WR-01)`, staged by explicit path. + +Progress record, the last step of Task 4, after the three plugin commits and this task's verify (see "Context budget and resume point"). Write .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-06-PROGRESS.md with: +- a first line saying it is the mid-plan record of 19.3-06, superseded by 19.3-06-SUMMARY.md once that file exists; +- the line `next: Task 5` on its own; +- a Completed Tasks table for Tasks 1-4 in the executor's checkpoint shape: task, name, commit (repository, short hash and subject), key files, one row per commit; +- the HEAD of summercms.go and of the six sibling repositories (the layers still at their planning-time heads); +- each task's verify result, with the docker stage marked as run or NOT RUN; +- the notes the SUMMARY needs from Tasks 1-4: deviations, decisions taken while executing, and what the admin shows for a refused protected upload (Task 3). +Commit it alone as `docs(19.3-06): progress record after Task 4`, staged by its path. Do not write 19.3-06-SUMMARY.md here. go vet ./... && go test -count=1 -short -race ./modules/cooler && go test -count=1 -short -run '^(TestSiteCachePurgeNoRedirect|TestSiteCachePurgeAsyncCoalesced|TestSiteCachePurgeRequest|TestSiteCachePurgeConfig|TestSiteCacheFromApp)$' -v ./modules/cooler 2>&1 | grep -cE '^--- PASS: Test[A-Za-z]+ \(' | grep -qx 5 && bash scripts/check-phase19.3.sh plugins && bash scripts/check-phase19.3.sh docker Non-zero exit: vet, a data race report, a cooler unit test, fewer than five top-level "--- PASS" lines (a redirect was followed, a call blocked or more than two requests went out), the plugins stage, or the docker stage. The docker stage fails when a framework or plugin purge test is red or skipped, for example a plugin test that read the receiver before the queued purge was sent, or when no Docker daemon answers. @@ -403,8 +426,10 @@ Each commit: `test(19.3-06): wait for the queued site-cache purge before asserti - `git -C ../sm-faq-plugin diff fa90e83 -- controllers`, `git -C ../sm-menu-plugin diff fe27b9a -- controllers` and `git -C ../sm-journal-plugin diff a99e9a1 -- controllers classes/site_cache.go plugin.go` each print nothing (call sites and aliases unchanged). - `grep -c 'Wait(' ../sm-faq-plugin/site_cache_admin_test.go ../sm-menu-plugin/site_cache_admin_test.go ../sm-journal-plugin/site_cache_admin_test.go` prints at least 1 for each file. - `git -C ../sm-faq-plugin log --format=%ct -1` is not earlier than `git log --format=%ct -1 --grep='(WR-01)' -- modules/cooler/sitecache.go` (framework commit first). + - `grep -cx 'next: Task 5' .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-06-PROGRESS.md` prints 1, and `for id in CR-01 WR-04 WR-05 WR-09 WR-06 WR-07 WR-08 WR-02 WR-01; do grep -q "($id)" .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-06-PROGRESS.md || echo "missing $id"; done` prints nothing. + - `git log --oneline -1 -- .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-06-PROGRESS.md` prints one line, and `test -e .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-06-SUMMARY.md` exits non-zero (no SUMMARY before the plan's end). - An editor's save, or a 50-row bulk delete, answers without waiting for the site, the site still gets its purge after the last commit, and the bearer token never follows a redirect. + An editor's save, or a 50-row bulk delete, answers without waiting for the site, the site still gets its purge after the last commit, and the bearer token never follows a redirect. The committed progress record lets a fresh executor start at Task 5. @@ -452,7 +477,7 @@ Commits: Task 6: Unit tests for every fix, required by name in scripts/check-phase19.3.sh, the whole gate green with Docker, and the review disposition recorded modules/surf/cors_profile_test.go, modules/surf/apidocs_test.go, modules/bouncer/cookie_test.go, modules/cooler/sitecache_test.go, scripts/check-phase19.3.sh, .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-REVIEW-DISPOSITION.md, .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-VALIDATION.md - scripts/check-phase19.3.sh (the lists, go_named, LAYER_REQUIRE), `git log --oneline --grep='(19.3-06)'` (the Task 1-5 commits), modules/surf/cors.go, modules/surf/apidocs.go, modules/bouncer/cookie.go, modules/cooler/sitecache.go, modules/cooler/cooler.go (Handle.Close), modules/cooler/install_test.go (Install with a test config), modules/cooler/sitecache_test.go (the Postgres harness of TestSiteCacheAfterCommitOnly), 19.3-REVIEW-DISPOSITION.md, 19.3-VALIDATION.md (Per-Task Verification Map) + scripts/check-phase19.3.sh (the lists, go_named, LAYER_REQUIRE), 19.3-06-PROGRESS.md (Tasks 1-4), `git log --oneline --grep='(19.3-06)'` (the Task 1-5 commits), modules/surf/cors.go, modules/surf/apidocs.go, modules/bouncer/cookie.go, modules/cooler/sitecache.go, modules/cooler/cooler.go (Handle.Close), modules/cooler/install_test.go (Install with a test config), modules/cooler/sitecache_test.go (the Postgres harness of TestSiteCacheAfterCommitOnly), 19.3-REVIEW-DISPOSITION.md, 19.3-VALIDATION.md (Per-Task Verification Map) Unit tests, Docker-free unless noted, committed as `test(19.3-06): unit tests for the review fixes`: - surf cors_profile_test.go: - TestCORSCredentialedOriginRules: a table over http.cors and http.cors_profiles.spa with credentials (per D-04). @@ -507,7 +532,7 @@ Planning docs, a separate commit `docs(19.3-06): review disposition and validati Task 7: Release gate: with the whole gate green, a human decides on tagging v0.1.28, bumping the plugin requires, tagging the layers and the pushes - .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-05-PLAN.md (Task 7) and 19.3-05-SUMMARY.md (release record), .planning/notes/core-plugins-own-repos.md, .planning/notes/2026-10-07-embedded-admin-release-gate.md, this plan's Task 1-6 SUMMARY notes + .planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-05-PLAN.md (Task 7) and 19.3-05-SUMMARY.md (release record), .planning/notes/core-plugins-own-repos.md, .planning/notes/2026-10-07-embedded-admin-release-gate.md, 19.3-06-PROGRESS.md (Tasks 1-4) and this plan's Task 5-6 notes Do everything that can be automated, print a release sheet, then stop. Checks before the sheet: @@ -608,10 +633,10 @@ Record the reply verbatim, with every pushed hash and tag. -- Task 1: the surf short suite, the five CORS tests, the gate's go stage with the new SURF_TESTS names, TestDocsTree. +- Task 1: the surf short suite, the five CORS tests, the guard_403, turnstile_422 and turnstile_503 subtests (every status routing.md names is tested), the gate's go stage with the new SURF_TESTS names, TestDocsTree. - Task 2: the surf and bouncer short suites and seven regression and parity tests, TestDocsTree. - Task 3: vet in both tag modes, the surf and lagoon short suites, six named tests, TestDocsTree. -- Task 4: the cooler suite under -race, five named purge tests, the gate's plugins and docker stages. +- Task 4: the cooler suite under -race, five named purge tests, the gate's plugins and docker stages; the committed progress record naming Task 5 as next. - Task 5: the gate's layers stage (cmp, then the existing specs), both new specs in every layer, TestDocsTree, docs:build --check. - Task 6: `PHASE193_REQUIRE_DOCKER=1 bash scripts/check-phase19.3.sh --all`, TestDocsTree, the examples/hello workspace tests. - Task 7: the full gate again before the human decision. @@ -626,7 +651,7 @@ Record the reply verbatim, with every pushed hash and tag. -Create `.planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-06-SUMMARY.md` when done. Include: +Create `.planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/19.3-06-SUMMARY.md` when done, never before Task 7; the Tasks 1-4 part comes from 19.3-06-PROGRESS.md, which stays as written. Include: - the commits per repository and per finding; - the identifiers added (`attach.ErrProtectedNeedsPrivateBucket`, `cooler.SiteCachePurger.Wait`, runtimeConfig `siteCache.allowProxied`); - what the admin shows for a refused protected upload;