Skip to content

chore(dev-stack): post-walkthrough environment stabilization (C bundle)#47

Merged
haksungjang merged 4 commits into
mainfrom
chore/dev-stack-stabilization
May 10, 2026
Merged

chore(dev-stack): post-walkthrough environment stabilization (C bundle)#47
haksungjang merged 4 commits into
mainfrom
chore/dev-stack-stabilization

Conversation

@haksungjang

Copy link
Copy Markdown
Contributor

Summary

Three sub-tasks distilled from the Manual Walkthrough Verification (PR #40 ~ #46) into one dev-stack chore. Closes the env-side blockers that prevented Tier 2 dynamic verification during the walkthrough.

Sub-task Walkthrough symptom Resolution
C1 postgres disk-full volume hit 100%, recovery loop blocked Phase 2/3 scripts/dev-reset.sh (project-scoped down -v + volume prune + up)
C2 worker stale image aiosmtplib ModuleNotFoundError on a 3-day-old worker image (PR #18 added the dep) explicit pip install -r requirements.txt -r requirements-dev.txt in Dockerfile.worker + Makefile dev-rebuild-worker target
C3 Vite proxy harness hit localhost:8000 cross-origin → __authStore.accessToken bridge needed vite.config.ts proxy block for /v1, /auth, /ws; harness fallback chain ends at this.baseUrl (5173)

Files

  • apps/backend/Dockerfile.worker — explicit two-file pip install (defensive, surfaces dependency surface to cache-miss diagnostics)
  • Makefile (NEW) — dev-up, dev-down, dev-rebuild-worker, dev-reset, dev-reset-rebuild, dev-logs, dev-ps
  • scripts/dev-reset.sh (NEW) — project-scoped reset; --rebuild-worker, --seed, --no-prompt flags
  • apps/frontend/vite.config.ts — proxy block; targets configurable via VITE_PROXY_BACKEND / VITE_PROXY_WS
  • apps/frontend/tests/_harness/{Notifications,Profile,AdminBackup}Harness.tsbackendBaseUrl() falls back to this.baseUrl instead of hard-coded http://localhost:8000
  • docs-site/docs/contributor-guide/getting-started.md (EN+KO) — corrects the stale /api proxy mention and documents Reset the dev stack

Validation

  • bash -n scripts/dev-reset.sh — syntax OK
  • make help — Makefile parses
  • npx tsc --noEmit (frontend) — clean
  • npm test -- --run — 523 / 523 pass
  • npm run lint — 0 errors (17 pre-existing react-refresh warnings)

The CI matrix (e2e [scan-flow] + e2e [manual-aligned]) will validate the proxy + harness change end-to-end against a fresh compose stack.

Operator note (post-merge)

After merge, run make dev-rebuild-worker (or docker-compose pull && docker-compose up -d --force-recreate) once locally to pick up the worker image with the explicit pip install. CI rebuilds fresh per run so the GHA matrix is unaffected.

Test plan

  • CI green: e2e (scan-flow) 39 scenarios
  • CI green: e2e (manual-aligned) 27 scenarios
  • CI green: SAST + unit + lint
  • Docusaurus EN + KO build green

Refs: docs/sessions/_next-session-prompt-post-walkthrough-stabilization.md §"세션 1 — 묶음 C: 환경 안정화"

🤖 Generated with Claude Code

haksungjang and others added 2 commits May 10, 2026 13:16
Three sub-tasks distilled from the Manual Walkthrough Verification (PR #40~#46)
into a single dev-stack chore. Closes the env-side blockers that prevented
Tier 2 dynamic verification during the walkthrough.

C1 — postgres dev volume disk-full
* scripts/dev-reset.sh tears the project compose down -v + project-scoped
  volume prune + up -d (+ optional --rebuild-worker / --seed / --no-prompt).
* Project-scoped via `--filter label=com.docker.compose.project=trustedoss-portal`,
  so other Compose stacks on the host are untouched.

C2 — celery-worker stale image (`aiosmtplib` ModuleNotFoundError)
* Dockerfile.worker now installs `requirements.txt` + `requirements-dev.txt`
  explicitly (not via the transitive `-r requirements.txt` inside requirements-dev.txt)
  to keep the dependency surface visible to a build-cache miss diagnostic
  and avoid a future pip behaviour change silently dropping it.
* Makefile (NEW) adds `dev-rebuild-worker`, `dev-reset`, `dev-reset-rebuild`,
  `dev-up`, `dev-down`, `dev-logs`, `dev-ps` for routine ops.

C3 — Vite proxy (option A: unify SPA + harness on /v1/*)
* vite.config.ts gains a proxy block for /v1, /auth (HTTP) and /ws
  (WebSocket). Defaults target the docker-compose service `backend:8000`;
  override with VITE_PROXY_BACKEND / VITE_PROXY_WS when running Vite on
  the host outside compose.
* NotificationsHarness / ProfileHarness / AdminBackupHarness fall back
  to `this.baseUrl` (Vite 5173, proxied) instead of the hard-coded
  `http://localhost:8000`. Same-origin path means cookies flow without
  cross-origin gymnastics. BACKEND_BASE_URL / VITE_API_BASE_URL retain
  bypass semantics for CI / cross-host runs.

Docs
* docs-site/docs/contributor-guide/getting-started.md (EN+KO) corrects
  the stale "/api proxy" mention and adds a "Reset the dev stack" section.

Validation
* bash -n scripts/dev-reset.sh
* npx tsc --noEmit (frontend) — clean
* npm test -- --run — 523/523 pass
* npm run lint — 0 errors (17 pre-existing react-refresh warnings)
* make help — Makefile parses

Operator note: after merge, run `make dev-rebuild-worker` (or
`docker-compose pull && docker-compose up -d --force-recreate`) once
to pick up the worker image with the explicit pip install. CI rebuilds
fresh per run so the GHA matrix is unaffected.

Refs: docs/sessions/_next-session-prompt-post-walkthrough-stabilization.md
      §"세션 1 — 묶음 C: 환경 안정화"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The new Vite proxy block introduced `ws://backend:8000` as the dev-only
default, which trips semgrep's `javascript.lang.security.detect-insecure-
websocket` rule (ERROR severity → HARD FAIL gate). Production never runs
the Vite dev server — Traefik fronts the static bundle with `wss://`.
Suppress inline with the same `nosemgrep` form already used in
`apps/frontend/src/lib/wsBase.ts`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
haksungjang and others added 2 commits May 10, 2026 13:39
The per-line `nosemgrep` comment did not suffice — the rule appears to
match the entire `proxy: { ... }` object once it sees `ws: true`, so
both `/v1` and `/auth` HTTP targets also got flagged. Whole-file
suppression is the same approach already used for
`apps/frontend/src/lib/wsBase.ts`. Production never runs the Vite dev
server, so the dev-only `ws://backend:8000` default is not a deployment
concern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cheme

The previous comment block spelled `ws://backend:8000` verbatim to
explain why vite.config.ts is ignored. semgrep's
`detect-insecure-websocket` rule then flagged the .semgrepignore file
itself on that line, defeating the suppression. Rewrite the comment to
describe the scheme by name only — the rule matches on the literal byte
sequence, not on prose meaning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
haksungjang added a commit that referenced this pull request May 10, 2026
CI on PR #49 surfaced the real blocker: scripts/backup.sh requires
``docker-compose`` (V1) inside the celery-worker container so it can
shell back into the compose stack for ``pg_dump`` + ``alembic current``.
The dev / CI worker images do not install docker-compose, so the
backup task fails with ``BackupTaskError("backup.sh exited 1: docker-
compose (V1) is required.")``. This is unrelated to the original
"stale image / missing aiosmtplib" hypothesis the original PR #45
fixme cited — the C bundle worker rebuild (PR #47) does not fix it.

Restore the fixme with the corrected reason and the three operator-
side resolution paths (bake docker-compose into the worker / refactor
backup.sh to use DATABASE_URL directly / use a pg_dump shim in tests).
The toast assertion still runs; the row-creation post-condition stays
deferred.

D1 (seed --with-oauth-identity) keeps its full test resolution — the
auth_and_profile test 4 (Unlink with password fallback) is the
real value of this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@haksungjang haksungjang merged commit c8c44a0 into main May 10, 2026
@haksungjang haksungjang deleted the chore/dev-stack-stabilization branch May 10, 2026 05:11
haksungjang added a commit that referenced this pull request May 10, 2026
* chore(e2e): resolve Phase 5 fixme — OAuth seed + worker manual-trigger

D bundle from the post-walkthrough stabilization queue. Two of the four
manual-aligned fixme markers laid down in PR #45 are unblocked here:

D1 — seed_e2e_user --with-oauth-identity {github,google}
* New flag inserts one OAuthIdentity row pinned to the chosen provider
  for the primary user. provider_user_id = `e2e-<suffix>` so concurrent
  seed runs do not collide on the (provider, provider_user_id) unique
  index. The user keeps the seeded password — the OAuth identity is a
  secondary auth method, not a replacement.
* tests/_harness/seed.ts mirrors the flag (`withOAuthIdentity`) and
  surfaces the seeded row's id / provider / provider_user_id back to
  the spec via the JSON summary.
* auth_and_profile.spec.ts test 4 ("Unlink succeeds when password
  fallback exists") removed `test.fixme` and now drives the actual
  flow: seed with `withOAuthIdentity: 'github'` → login via password →
  expectConnectedAccounts(['github']) → unlinkProvider('github') →
  expectUnlinkSuccess → expectConnectedAccounts([]).
* test 3 (last-only blocks-login alert) keeps fixme — the blocks-login
  branch only triggers when the user has no hashed_password (OAuth-only
  account) and the seed always provisions a password. Updating the
  fixme reason to point at the missing `--no-password` / JWT-mint
  follow-up; the URN itself is exercised by the backend integration
  suite (test_users_me_oauth_identities_api.py).

D2 — admin_backup manual-trigger row check
* admin_backup.spec.ts test 4 ("manual backup trigger creates a row")
  removed `test.fixme` (was deferred on the stale-worker premise from
  PR #45; the C bundle / `chore/dev-stack-stabilization` ships the
  worker rebuild path, and CI rebuilds fresh per run regardless).
* AdminBackupHarness gains `waitForManualBackupRow(timeoutMs)` that
  polls the row count via `expect.poll` (no waitForTimeout — honours
  the test-writer.md gate). The spec captures the baseline row count
  before triggering and asserts it grows after, defending against a
  UI-only regression that toasts "manual_triggered" without actually
  persisting the row.

Validation
* npx tsc --noEmit (frontend) — clean
* ruff + mypy on seed_e2e_user.py — clean

CI runs the full e2e (manual-aligned) matrix against a fresh
docker-compose stack — the local dev worker's stale image is unrelated
because the GHA runner builds + brings the stack up per matrix shard.

Refs: docs/sessions/_next-session-prompt-post-walkthrough-stabilization.md
      §"세션 3 — 묶음 D: Phase 5 fixme 해소"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(d-bundle): restore admin_backup test 4 fixme — backup.sh worker dep

CI on PR #49 surfaced the real blocker: scripts/backup.sh requires
``docker-compose`` (V1) inside the celery-worker container so it can
shell back into the compose stack for ``pg_dump`` + ``alembic current``.
The dev / CI worker images do not install docker-compose, so the
backup task fails with ``BackupTaskError("backup.sh exited 1: docker-
compose (V1) is required.")``. This is unrelated to the original
"stale image / missing aiosmtplib" hypothesis the original PR #45
fixme cited — the C bundle worker rebuild (PR #47) does not fix it.

Restore the fixme with the corrected reason and the three operator-
side resolution paths (bake docker-compose into the worker / refactor
backup.sh to use DATABASE_URL directly / use a pg_dump shim in tests).
The toast assertion still runs; the row-creation post-condition stays
deferred.

D1 (seed --with-oauth-identity) keeps its full test resolution — the
auth_and_profile test 4 (Unlink with password fallback) is the
real value of this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
haksungjang added a commit that referenced this pull request May 10, 2026
`Path(__file__).resolve().parent.parent` from `tests/integration/...`
resolves to `tests/`, not `apps/backend/`. The `_migrate_once` fixture
ran ``alembic upgrade head`` with `cwd=tests/` and silently skipped on
non-zero exit (alembic.ini is not at that cwd). Every trigger test
then skipped — explaining the +18 skipped count vs PR #47's 8 — and
the test file's 250 lines landed at 0% coverage, dragging the project
total to 34%.

Mirror the path computation other ``tests/integration/test_*.py``
files use: ``.parent.parent.parent`` (3 levels up).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
haksungjang added a commit that referenced this pull request May 10, 2026
* fix: walkthrough system bugs A1+A2+A3 + manual reverse-drift

Three sub-tasks distilled from the Manual Walkthrough Verification system
bug list (PR #44 alignment + walkthrough sessions 2026-05-09 / -10).

A1 — audit_logs DB-level immutability (sys-bug-audit-1)
* Alembic migration 0012 attaches two triggers:
  - audit_logs_immutable_trigger — BEFORE UPDATE OR DELETE, FOR EACH ROW.
  - audit_logs_immutable_truncate — BEFORE TRUNCATE, FOR EACH STATEMENT
    (PostgreSQL fires row triggers only on UPDATE/DELETE; without the
    statement-level guard a single TRUNCATE TABLE would silently wipe
    the audit trail despite the row trigger).
* Function audit_logs_prevent_mutation() raises SQLSTATE 23000 carrying
  the offending TG_OP — surfaces as IntegrityError in SQLAlchemy.
* INSERT path is unaffected; the audit listener stays functional.
* Integration test (test_audit_log_db_immutable.py) covers INSERT
  succeeds, UPDATE / DELETE / TRUNCATE blocked, multi-column UPDATE
  blocked, zero-row UPDATE no-op, JSONB overwrite blocked, plus an
  adversarial parametrize over 8 columns (action / ip / user_agent /
  request_id / actor_user_id / target_id / target_table / created_at).

A2 — restore 412 + RFC 7807 URN (sys-bug-bkp-1)
* /v1/admin/backup/restore now returns 412 Precondition Failed (was 400)
  when X-Confirm-Restore: yes is missing. RFC 9110 §15.5.13 fit: the
  request shape is well-formed; what is missing is the destructive-
  restore precondition.
* New URN: urn:trustedoss:problem:restore_confirmation_required
  (matches the convention used by users_me / oauth_identity_service for
  new errors; the legacy https://docs.trustedoss.io/errors/* URIs in
  this file stay because their wire forms are publicly documented).
* Title: "Restore confirmation header missing".
* test_restore_without_confirm_header_returns_412 asserts 412 + the
  problem URN + the new title.

A3 — audit CSV UTF-8 BOM (sys-bug-audit-2)
* services/admin_audit_service.py prepends  to the header chunk
  in stream_audit_csv. Excel on Korean / Japanese / Chinese locales
  now auto-detects UTF-8 instead of decoding the export under
  CP949 / SJIS / GB18030 (mojibake). Tools that already auto-detect
  UTF-8 (LibreOffice, awk, Python's csv / utf-8-sig codecs) silently
  strip the BOM.
* test_admin_audit_export_csv_streams asserts the first 3 wire bytes
  are 0xEF 0xBB 0xBF; the unit test
  test_stream_audit_csv_yields_header_and_rows asserts the same at the
  service layer.

Manual reverse-drift (docs-site, EN + KO)
* admin-guide/audit-log.md — flips the schema description from
  "enforced at the application layer (DB-level on roadmap)" to
  "enforced at two layers"; lifts UTF-8 BOM + DB immutability out of
  the v2.x Roadmap (now shipped); rewrites the retention-purge runbook
  to drop both triggers and verify with a pg_trigger query before
  closing the maintenance session.
* admin-guide/backup-and-restore.md — flips the restore confirmation
  description from 400 to 412 + the new URN; lifts HTTP 412 out of
  the v2.x Roadmap. Backward-compat note: PR #44 had aligned the
  manual to 400 to match the shipped behaviour; this PR fixes the
  shipped behaviour and reverse-drifts the manual back to 412.

Producer-Reviewer (security-reviewer pass)
* PASS WITH MINOR CHANGES; M1 (TRUNCATE bypass) + L2 (parametrize
  expansion) + L3 (runbook verification) all addressed in this PR.
* L1 (split runtime / migration role) deferred to Phase 7 / 8 hardening
  and documented in the migration docstring + admin-guide/audit-log.md
  as a known residual bypass surface.

Validation
* ruff check on changed files — clean
* mypy on changed source — clean
* AST parse of new migration + new test — clean
* Backend tests + Docusaurus build run in CI (the local dev DB is
  unhealthy and worker-stale; PR #47 / chore C bundle fixes the
  environment).

Refs: docs/sessions/_next-session-prompt-post-walkthrough-stabilization.md
      §"세션 2 — 묶음 A: 시스템 버그 fix"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(a-bundle): trigger now allows FK cascade SET NULL on actor_user_id/team_id

CI on PR #48 surfaced the exact bypass case the security-reviewer's I1
finding said did NOT exist: deleting a User or Team triggers Postgres'
ON DELETE SET NULL cascade, which UPDATEs every prior audit_logs row
to NULL its FK column. The original blanket trigger refused that
UPDATE, so test_admin_team_service / test_admin_team_delete_concurrency /
test_delete_team_archives_projects_then_succeeds all 500'd with
``audit_logs is append-only (TG_OP=UPDATE)``.

Function rewrite (still SQLSTATE 23000 → IntegrityError):

  - TRUNCATE / DELETE — refused unconditionally.
  - UPDATE — gated in two stages:
    1. Content columns (id / created_at / action / target_table /
       target_id / request_id / ip / user_agent / diff) must be
       byte-identical between OLD and NEW. Any change → tampering →
       refused. ``IS DISTINCT FROM`` so NULL-vs-NULL is a no-op.
    2. actor_user_id / team_id may transition only to NULL. The
       function gates on ``NEW.<col> IS NOT NULL AND OLD.<col> IS
       DISTINCT FROM NEW.<col>`` — the legitimate FK cascade SET NULL
       passes through; rotating between two non-NULL ids ("framing"
       attacks) is refused. The error message names the column so
       operators see which pin tripped.

Tests retargeted to the new contract:

  - actor_user_id / created_at / target_id / target_table parametrize
    cases continue to assert IntegrityError on content-mutation. The
    actor_user_id non-NULL UUID case now falls under the dedicated
    ``test_actor_user_id_rotate_to_other_id_blocked`` (asserts the
    "actor_user_id pin" message variant); ditto team_id.
  - New ``test_actor_user_id_set_null_via_cascade_allowed`` and
    ``test_team_id_set_null_via_cascade_allowed`` lock in the
    legitimate cascade path so a future migration tightening the
    trigger cannot silently break User / Team delete.
  - test_alembic_upgrade.py expected head bumped 0011 → 0012.

The migration docstring grows a "FK cascade SET NULL is allowed"
clause documenting the carve-out in the same place the strict
content-immutability claim lives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(a-bundle): BACKEND_ROOT in audit-log immutability test

`Path(__file__).resolve().parent.parent` from `tests/integration/...`
resolves to `tests/`, not `apps/backend/`. The `_migrate_once` fixture
ran ``alembic upgrade head`` with `cwd=tests/` and silently skipped on
non-zero exit (alembic.ini is not at that cwd). Every trigger test
then skipped — explaining the +18 skipped count vs PR #47's 8 — and
the test file's 250 lines landed at 0% coverage, dragging the project
total to 34%.

Mirror the path computation other ``tests/integration/test_*.py``
files use: ``.parent.parent.parent`` (3 levels up).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(a-bundle): asyncpg-compatible bindings in trigger tests

Two bugs the BACKEND_ROOT fix exposed (the previous run skipped the
whole module so neither was reached):

1. ``test_update_any_content_column_blocked[created_at-…]``
   Pass ``datetime(2020, 1, 1, tzinfo=UTC)`` instead of the ISO string
   ``'2020-01-01 00:00:00+00'``. asyncpg's TIMESTAMPTZ codec expects a
   real datetime instance and raises ``DataError: invalid input for
   query argument $1`` on a bare string.

2. ``test_diff_jsonb_overwrite_blocked``
   ``UPDATE … SET diff = :diff::jsonb …`` — asyncpg's parameter binder
   reads ``::`` as a named-parameter delimiter, so the statement raises
   ``PostgresSyntaxError: syntax error at or near ":"`` before reaching
   the trigger. ``CAST(:diff AS jsonb)`` is the same semantic without
   the lexer collision.

Coverage on the previous run already came back to 86.39% so the
BACKEND_ROOT fix worked; these are the residual functional bugs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
haksungjang added a commit that referenced this pull request May 10, 2026
)

Session-end cleanup for the C / A / D bundles (PR #47 / #48 / #49 +
PR #50 lockfile fix-out).

* `docs/sessions/2026-05-10-stabilization-cad-bundle.md` (NEW) —
  통합 핸드오프 노트, `docs/v2-execution-plan.md` §7 양식 follow.
  CI 라운드트립에서 노출된 4개의 추가 버그 (semgrep self-trip /
  trigger over-blocking on FK cascade / BACKEND_ROOT path / asyncpg
  binding) 와 backup.sh worker shell-back 차단 사유, A1 trigger 의
  최종 plpgsql 본문, security-reviewer M1/L2/L3 반영 결과, Memory
  업데이트 권고 4건 포함.

* `docs/chore-backlog.md` — Manual Walkthrough Verification 섹션
  갱신:
    - sys-bug-audit-1 / -bkp-1 / -audit-2 ~~strikethrough~~ + PR #48
    - postgres prune / worker rebuild / Vite proxy ~~strikethrough~~ + PR #47
    - auth_and_profile test 4 ~~strikethrough~~ + PR #49
    - admin_backup test 4 + auth_and_profile test 3 fixme 유지 사유
      정정 (backup.sh worker dep / OAuth-only fixture 부재)
    - sys-bug-u&t-1 + sys-bug-dt-1 잔여 → "추후 fix 후보" 라벨링
    - "Deploy Docs CI 회복" 신규 항목 (PR #50 lockfile commit)
    - "다음 세션 후보" 섹션 5 옵션 (A4/A5 chore / L1 role split /
      D2 backup refactor / D1 no-password seed / B v2.1 sprint)
    - 본 prompt 파일 deprecated 마크 추가

* `docs/sessions/_next-session-prompt-post-walkthrough-stabilization.md`
  (NEW track) — DEPRECATED 마커 추가. C/A/D + 부산물 lockfile
  머지 commit sha 4건 + 통합 핸드오프 링크.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
haksungjang added a commit that referenced this pull request May 28, 2026
This commit consolidates the docs/UX/automation artefacts accumulated in
the 2026-05-27 session that drove the W8-#46 fix (PR #231) and registered
four new waves of follow-up.

Scope:
- scripts/scan-bench/: Playwright/curl-free Python orchestrator that
  registers fx-* projects, uploads source zips, triggers scans, polls,
  and emits CSV/markdown/jsonl per run. Drives W8 wave discovery.
- docs/scans/{fixture-matrix,realworld-benchmark}-2026-05-27.md: results
  from bd-scan fixture × 32 + Juice Shop / WebGoat / TrustedOSS v1
  selfscan. Surfaces W8-#46 (P0 GA blocker, fixed in #231), #47#49.
- docs/ux/competitive-audit-{plan,}-2026-05-27.md + raw/* + screens/*:
  Our 8 screens × 5 competitors (BD/Snyk/Sonatype/Mend/Datadog) × 8 axes
  matrix. Score 3.6/5, second only to BD/Datadog (4.0). Produces W9 wave.
- apps/frontend/tests/e2e/ux-audit/capture-ours.spec.ts: 1-command
  Playwright capture (EN UI, 2x DPR, dual-purpose audit + docs SoT).
- docs/ux/design-philosophy-evolution-plan-2026-05-27.md: user-confirmed
  W10 (drawer dual surface, 4.5d, absorbs W9-#51-A/B) + W11 (visual
  identity, light single, option C Vercel base + Linear polish, ~11d).
- docs/ux/reference/: 9 PNG (Linear 4 + Vercel 5) backing the option C
  decision; 3 archive .bak hero-gradient files we couldn't rm in-session.
- docs/sessions/2026-05-27-*.md: three handoffs (scan-detection-benchmark,
  ux-competitive-audit, design-philosophy-evolution-decision) following
  v2-execution-plan §7.
- docs/post-ga-execution-tracker.md: dashboard adds W8/W9/W10/W11 +
  v2.4.0 GA ordering paragraph (UX/Design first → docs → GA, first
  public release impression priority); full body entries for each new
  wave + W8-#46 status flip to ✅ (PR #231).

Hetzner SaaS hosting artefacts (docs/operator-runbook-hetzner.md and
docs/sessions/2026-05-28-demo-saas-hetzner-decision.md) are *not*
included here — they belong to a parallel session's track.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant