Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|---|---|---|
Expand Down
16 changes: 16 additions & 0 deletions docs/RULE-FEEDBACK.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/domains/web-screens-jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down
Loading