From aa1fc69aff539aba5c1ec4e79542d96abc5f7da6 Mon Sep 17 00:00:00 2001 From: goetchstone Date: Thu, 8 Oct 2026 07:25:51 -0400 Subject: [PATCH 1/3] docs: reads confirmed live on GAM 7.48.22 The read-only acceptance pass ran on the operator's tenant with GAM 7.48.22: all eight parser checks pass. The three reads it doesn't run keep their own dates. Co-Authored-By: Claude Opus 5.5 --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a0524e7..1cb89bb 100644 --- a/README.md +++ b/README.md @@ -148,11 +148,11 @@ Every write is audited, so this table comes from real audit logs rather than mem means the operation has succeeded at least once against a production Google Workspace domain. **Not yet** means unproven: the offline suite shows the command matches GAM's grammar and the mock accepts it, nothing more — run it once on a **throwaway** user, group or calendar before you rely on it. All -reads are confirmed (a read-only pass over the parsers ships as `scripts/acceptance.py`; last full -run 2026-09-25 on GAM 7.48.11 — the pin is now 7.48.14, on which setup's scope check passed live on -2026-10-02 — together with setup's scoped `check serviceaccount scopes …`, `print domains` -for the Builder's External-only filter, and reading the connected admin and its granted scopes from -`oauth2.txt`). +reads are confirmed (a read-only pass over the parsers ships as `scripts/acceptance.py`; last run +2026-10-08 on GAM 7.48.22, all eight checks passing). Three reads it doesn't run were last confirmed +separately: setup's scoped `check serviceaccount scopes …` (2026-10-02, GAM 7.48.14), and `print +domains` for the Builder's External-only filter and reading the connected admin and its granted +scopes from `oauth2.txt` (2026-09-25, GAM 7.48.11). | Write | Where | Live | |---|---|---| From 2e53ed943c7b6ed2e3c9f14b8a89b731a1529256 Mon Sep 17 00:00:00 2001 From: goetchstone Date: Thu, 8 Oct 2026 07:28:59 -0400 Subject: [PATCH 2/3] docs: the web-screens runbook names the download-attribute rule Co-Authored-By: Claude Opus 5.5 --- docs/domains/web-screens-jobs.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/domains/web-screens-jobs.md b/docs/domains/web-screens-jobs.md index 781a8f0..9d7360e 100644 --- a/docs/domains/web-screens-jobs.md +++ b/docs/domains/web-screens-jobs.md @@ -34,6 +34,7 @@ Routes read from the cached directory (`request.app.state.gamgui.users()`) so pa - **Directory data into attributes (invariant #8):** templates put Google-sourced strings in autoescaped contexts; never `| tojson` in a double-quoted attribute. ## Gotchas / mock-lies traps +- **A file download needs the `download` attribute on its link.** The app window (pywebview's WKWebView) downloads only what WebKit can't display, or a link marked `download`; a CSV is text, so without the attribute it replaced the page and left no way back (no Back control), whatever `Content-Disposition` said. A browser, TestClient and the Chrome a11y run download either way, so only the app shows it. `tests/test_app_window_downloads.py` finds every GET route that sends an attachment and fails unless each is linked with `download` (failure-log 2026-10-08). - **Placeholders and example data stay generic** (`example.com`, "Sales", "Training Calendar") — the public repo was de-branded once and the operator's company names crept back in via form placeholders and the seeded onboarding role. The local pre-commit hook's private-term tripwire (`.claude/private-terms`, gitignored) now blocks that. The UI says *Department*, never "store". - **Record an outcome with `job.record(item, ok, reason, detail)`, never `job.failed.append(x)`** — the append bypasses the cap (a source tripwire fails on it). `job.failed` is a *sample* of `Outcome`s: count with `failed_total`, not `failed | length`, render `job.more` as "+K more" (the `failures` macro does), and compare names with `failed_items`. - **A 200 proves nothing.** Every route answers 200 whether the write worked or not — a failure renders the amber error partial. Assert success with `tests/helpers.py`: `assert_ok_partial(r)` (no amber box), the `gam_calls` fixture + `gam_writes()` (the exact argv the mock received), and the audit record's `ok`. The mock fails any argv it has no handler for, so an unhandled write can no longer "succeed". From 5256c66b82585e0257160c97bc73e99c60cd4862 Mon Sep 17 00:00:00 2001 From: goetchstone Date: Thu, 8 Oct 2026 07:29:36 -0400 Subject: [PATCH 3/3] docs: rule-feedback entry for the app-window download incident Co-Authored-By: Claude Opus 5.5 --- docs/RULE-FEEDBACK.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/RULE-FEEDBACK.md b/docs/RULE-FEEDBACK.md index 53c44fb..231c55b 100644 --- a/docs/RULE-FEEDBACK.md +++ b/docs/RULE-FEEDBACK.md @@ -30,6 +30,22 @@ layer. Moving it (skill → hook → tripwire) is a no-text-change fix. --- +## 2026-10-08 — A CSV link worked in every test and stranded the operator in the app window +- **What happened:** Download CSV replaced the app window's page with the CSV as text, with no way back + (failure-log 2026-10-08). pywebview's WKWebView downloads only what WebKit can't show, or a `download` + link; a browser downloads either way. +- **Invariant in force:** none — new shape: the app window behaves differently from a browser. +- **Why it didn't hold:** not covered. Every test, the a11y run and the mock preview use TestClient or + Chrome; CI builds the `.app` (`check_app.py`) but never launches it, so WKWebView's choices are never + exercised. +- **Would a rule have caught it?** Only if enforced differently: a launched-app smoke test (open the + window, click each download) would, but the `.app` launch is deliberately the operator's. The narrow + tripwire (`tests/test_app_window_downloads.py`) now covers downloads; other WKWebView-only behaviour + (navigation, dialogs, printing) has no equivalent. +- **Enforcement home if changed:** a CLAUDE.md "Corollary" line for the observer pass to weigh: + passing browser tests do not mean the app window behaves the same; a change to links, navigation or + dialogs is unproven until clicked in the app. + ## 2026-10-02 — Invariant #3 says only the 26 curated commands "can change anything"; a read's Sheet export does too - **What happened:** a repo-wide review found SECURITY.md promising every mutation is confirmed and invariant #3 saying the curated commands are the only ones that can *change* anything. The Builder's