kashi · review-comments-audit · remediation code-review
19 gap のうち 14 件を、13体のレビューエージェントが main の実コード(eb466f8)を実際に開いて検証 → アプローチ・具体的変更・追加テスト・労力・リスク・安全面を設計しました。安全面に触れる gap は opus + 4-lens adversarial gate を必須として印付け。
反映監査では「Kimura は意図的なデモ演出、漏洩なし」でした。しかし敵対的な再検証で、本番稼働中の /demo の「Top dyad」チップ(DemoMetricStrip)が、アップロードされた実際の発言者名を匿名化せずそのまま描画することが判明(anonymizeNamedTelemetry が ON でも素通り、テストもゼロ)。種名(Kimura)firebreak は健全 — だが実ユーザーが実名入りの議事録を上げ、dyad 閾値を超えると、第三者の実名(例: 山田→田中)が公開デモ画面に出得る。これは 2026-06-06 の教訓(CI green ≠ 漏洩なし)と同型の、唯一「本番露出あり」の項目。
直し方: Anonymize the dyad displayNames at the payload boundary instead of relying on each render site to remember. Build a single speakerId→"Participant X" map (the existing buildSpeakerDisplayMap is exactly this) when anonymizeNamedTelemetry is true, and apply it to the directional pair from/to before they reach the resolver/DemoMetricStrip — so the dyad chip inherits anonymization the same wa
完全性クリティック: 4-lens ゲートでは dyad チップだけでなく displayName を補間する全描画箇所(hero / tooltip / exec ラベル / aria-label / CSV・export / longitudinal・member-private projection)を列挙すること。さらに 既に実ユーザーに到達していないか demo-analyze ログの確認も推奨。
Triage the 13 gaps into four lanes. (1) One genuinely live safety leak — G06, where a real uploaded transcript renders raw participant names ("山田 → 田中") in the public /demo "Top dyad" chip with zero anonymize gating and zero test coverage — is the only item with active prod exposure and must lead the safety track. (2) A cluster of cheap, additive, test-only or doc-only self-defense items (G02, G13, GTEST, G04-Part1, G08-tests, G12-test, G09-build) that lock in current-correct behavior or close a true 404; do these next, fastest-first. (3) Two real decisions only Justine can make (G00 nav scope for non-admin roles; G04-Part2 whether to actually revoke admin's exec/reviewer support-access). (4) A correctly-deferred tail (G01 persistence, G03 content host-move, G08 analysis-time wiring, G10 axe tooling, G11 atomic-RPC) that carries no live risk and shouldn't be pulled forward without a pilot trigger. The single most important correction to the audit: G06, G04-test, G12-test, G09, G11 are all tagged opus-owner but only G06 is live-exposed — sequence by exposure, not by ticket order.
労力見積り: Quick-wins (G13, GTEST, G02, G08-tests, G10): all S, ~half a day combined, no decisions. Decisions (G00, G04-Part2, G01-trigger): zero eng time until Justine answers; G00+G04-Part1 builds are S each once decided. Safety track: G06 = M (the one live fix, do first); G09 = S build; G12 = M; G11-interim = S, G11-RPC = M (deferred onto §12); G04-Part1 = S; G08-tests = S — all force opus + 4-lens gate. Genuinely deferred (G01 L, G03 S, G08-wiring, G11-RPC M): 0 now, scheduled by trigger. Net near-term: ~1 safety M (G06) + ~5 S items, with two decisions gating ~2 more S builds.
検証(実コード): The seed-isolation firebreak the audit asked me to prove DOES hold: (1) no live-render component imports seeds — only src/app/demo/roles/page.tsx imports sections/{Affected,ManagerMirror,Executive}Section (grep confirmed single-importer); (2) ingest-form.tsx:4541 wires isSyntheticSample={false}, so buildDemoVisualPayloads.ts:257 sets anonymizeNamedTelemetry=true and SpeakingShareBar.tsx:67-69 / TurnDistributionBar:49 / ConversationTimeline:91 / DyadInterruptionMatrix:33 all re-index to "Participant A/B/C"; (3) the #481 exec de-identification holds (ExecutiveSection.tsx renders deident group labels, no {card.managerName}, no /demo/pattern-owner). A non-Kimura transcript cannot surface a SEED name. BUT I found an adjacent un-anonymized render path the audit's narrow framing missed: DemoMetricStrip.tsx:156 renders the "Top dyad" chip as `${topInterruptionDyad.from} → ${topInterruptionDyad.to}` where from/to come from resolveDemoResultContract.ts:665-666 sortedTopFromPairs, which passes sourceDisplayName/affectedDisplayName verbatim. Those trace to interruption-directionality.ts:92-94 (`b.displayName ?? b.speakerId`) and route.ts:234 (`speakerId: t.speakerLabel`) — i.e. the RAW VTT speaker name. This chip is NOT gated by shouldAnonymize (grep of DemoMetricStrip.tsx: no shouldAnonymize/visibility import). So a real uploaded transcript whose speakers cross DIRECTIONAL_DYAD_THRESHOLD=3 (build-demo-display-model.ts:901) renders e.g. "山田 → 田中 (4)" in the live result body — a real participant self-identifying. Not a seed leak, but the same display-identity safety surface. The va08 anonymization test (va08:344) uses RESULT_OK which has NO directional dyad (read fixture lines 16-75), so this path is entirely uncovered.
根本原因: Speaker-label anonymization for the public demo is implemented at the RENDER layer per-primitive (each chart component independently calls shouldAnonymize and re-indexes to "Participant A/B/C"), rather than once at the contract/payload boundary. The four visual-analytics chart primitives implement it; the dyad text chip in DemoMetricStrip was added later (Folder 54 Prompt 04) and reused resolved displayNames directly, missing the per-primitive anonymize switch. Because displayName carries the raw speakerLabel all the way through the payload (route.ts:234 → interruption-directionality.ts:92-94 → display model → resolver), any render site that forgets to anonymize leaks the real name.
アプローチ: Anonymize the dyad displayNames at the payload boundary instead of relying on each render site to remember. Build a single speakerId→"Participant X" map (the existing buildSpeakerDisplayMap is exactly this) when anonymizeNamedTelemetry is true, and apply it to the directional pair from/to before they reach the resolver/DemoMetricStrip — so the dyad chip inherits anonymization the same way the charts do. Add a behavioral render test that exercises a result WITH a directional dyad and asserts the live chip shows "Participant", never the raw name.
検証(実コード): Confirmed at multiple points. (1) `/src/app/app/admin/communication-policy/` has no `audit/` subdirectory — `ls` shows 10 sibling screens, none named `audit`. (2) `presentation.ts:110` hard-codes `audit: "not_started"` and its JSDoc comment at line 93-95 explicitly states "Only the audit read-UI is not yet built." (3) `docs/communication-policy-console.md:214-215` confirms: "The audit screen is honestly `not_started` — the audit API and writer exist, the read UI does not yet." (4) The nav definition at `presentation.ts:68` routes users to `/app/admin/communication-policy/audit`; clicking that link hits a Next.js 404. (5) The API layer is fully built: `src/app/api/admin/communication-policy/audit/route.ts` (GET, RBAC-gated, pagination, field-allowlist with request_context_json dropped) and tested in `test/folder60-p05-audit.test.ts`.
根本原因: P05 (Folder 60) only shipped the backend half: the audit writer library (`communication-policy-audit.ts`), the read library (`listCommunicationPolicyAuditEvents`), and the API route (`/api/admin/communication-policy/audit`). The corresponding server-side page component at `src/app/app/admin/communication-policy/audit/page.tsx` was never created. The nav item was registered in `NAV_ITEMS` as a placeholder (route marked `not_started`) with the intent to build it in a later prompt. That later prompt has not been executed.
アプローチ: Create `src/app/app/admin/communication-policy/audit/page.tsx` following the pattern of sibling screens (publish/page.tsx, simulator/page.tsx). The page should: (1) gate RBAC via `requireRole(["admin","ceo","hr_compliance"])`, (2) call the existing `/api/admin/communication-policy/audit` route (or call `listCommunicationPolicyAuditEvents` directly via `policyServiceDb()` to avoid an internal HTTP round-trip), (3) render a paginated table of audit events using the `EXPOSED_FIELDS` subset (no `request_context_json`), (4) embed `CommunicationPolicyNav` with `current="audit"` so the sidebar shows consistently, (5) update `deriveNavStatuses` in `presentation.ts` to return `"complete"` for `audit` once the page exists. No new libraries or schema changes needed — all plumbing is already in place.
検証(実コード): Confirmed against code. test/folder60-p03-repository.test.ts:24-71 is an in-memory FakeDb that RE-IMPLEMENTS the 0050 invariants (enforceUnique mimics the one_active/one_draft partial unique indexes; enforceImmutability mimics cpv_freeze_published_snapshot). It tests the mock, not the SQL. supabase/migrations/0050_communication_policy_console.sql defines the real triggers (cpv_freeze_published_snapshot_trg L90-92, cpae_block_mutation append-only triggers L134-137), partial unique indexes (one_active/one_draft_communication_policy_per_org L50-53), and default-deny RLS (L223-264). 0051_analysis_output_policy_binding.sql L8 and 0050's header both carry the [SIGN-OFF: schema] manual-dashboard-apply flag. The test/rls/ live-PG harness (setup.ts applyMigrations L91-104) DOES apply 0050/0051 to throwaway Postgres in CI (.github/workflows/ci.yml L54 runs npm run test:rls against a postgres:16 service) — so a broken/parse-failing migration WOULD be caught — but NO test in test/rls/ references communication_policy_* (grep confirmed zero), so the invariants' RUNTIME BEHAVIOR is never asserted on real PG. The append-only RLS-layer test pattern already exists at test/rls/notice-acceptances.test.ts:175 and test/rls/contestability-state-machine.test.ts:190 — the exact template to copy. Migration internals are consistent: hr_compliance referenced in 0050 RLS is a valid org_role (added in 0013), and 0050 wraps cleanly in begin/commit, so application will not fail. The genuine gap is behavioral assurance + prod-apply verification, not a syntax bug.
根本原因: Two distinct root causes. (1) Coverage gap: P03 was tested through an in-memory fake that re-encodes the DB constraints rather than exercising the actual Postgres triggers/indexes/RLS, so a regression in the SQL (e.g. someone weakening cpv_freeze_published_snapshot or dropping the partial unique index) would pass CI green. (2) Deployment-state gap: 0050/0051 are flagged [SIGN-OFF: schema] for manual Supabase-dashboard application, and nothing in-repo records or verifies that they were actually applied to prod, so the app's reliance on the 'DB is the floor' guarantee is unverified in production.
アプローチ: Close the behavioral gap by adding ONE live-Postgres RLS test (test/rls/folder60-comm-policy-invariants.test.ts) that asserts the 0050/0051 invariants against the real migrations the harness already applies, following the existing notice-acceptances.test.ts append-only pattern. Separately, verify prod application out-of-band with Justine (read-only schema introspection in the Supabase dashboard) and record the applied-state — do not automate prod DDL.
検証(実コード): Confirmed against HEAD eb466f8. Nav-visibility tightening is real and intentional: src/lib/navigation/workspaces.ts:198 ceo workspace allowedRoles=["ceo"], :214 reviewer workspace allowedRoles=["hr_compliance","restricted_investigator"] — admin excluded from both nav lists; admin-console workspace :230 allowedRoles=["admin"]. But host page gates remain wide: src/app/app/ceo/page.tsx:115 requireRole(["admin","ceo"]) and src/app/app/reviewer/page.tsx:28 requireRole(["admin","hr_compliance","restricted_investigator"]) — so admin keeps direct/legacy URL reach. role-routing.ts canViewExecutiveSurface() returns true for admin and canReviewDisputes() returns true for admin, confirming admin reach is wired at the helper layer too. The split is explicitly documented as deferred: workspaces.ts:132-136 ("admin can reach those today; the TIGHTENING ... is a C03 SIGN-OFF decision"). requiresInternalAccess is a flag with NO enforcement — defined admin-console.ts:45, set true only on diagnostics :114, and read only for cosmetic styling (admin/layout.tsx:26 secondary, admin/action-center/page.tsx:218 opacity-70); diagnostics/page.tsx:8 comment confirms "for now admin+ceo is the gate". workspace-access.ts states by design it "can only ever DENY access the existing model already denies — it cannot widen it" and that every legacy host gate is preserved. DECISIVE no-test finding: `find src/lib/navigation src/lib/auth -name "*.test.*"` returns NOTHING, and grep for canViewExecutiveSurface/canReviewDisputes/decideWorkspaceAccess/defaultLandingForRole across *.test.ts(x) returns zero — the deliberate boundary is entirely unpinned.
根本原因: Two layers express role-access with different intent and no test ties them together. The C02/C03 nav-visibility model (workspaces.ts) was deliberately tightened to scope admin to Admin Console only, but the legacy per-page requireRole host gates were intentionally left wide to preserve admin support-access. The product-target "admin must not own the exec narrative or reviewer decisions" is documented as a future C03/C13 sign-off, not yet a real permission (no internal/support role exists; requiresInternalAccess is cosmetic). So admin's cross-workspace reach is a known, staged carve-out — but it is undefended: nothing pins the current intended state, so a future edit could either silently widen nav (add admin to ceo/reviewer allowedRoles) or silently narrow the host gate (drop admin) without anyone noticing the boundary moved.
アプローチ: Split into two parts. Part 1 (FIX_NOW, no decision): commit a characterization test that pins the CURRENT deliberate state of the boundary across all five roles, so the staged carve-out becomes documented-and-defended rather than silent — admin is excluded from ceo/reviewer NAV but retained in the ceo/reviewer HOST gates, and requiresInternalAccess is cosmetic-only today. Part 2 (DECISION_NEEDED): whether to actually remove admin's exec/reviewer reach (drop "admin" from the two host gates + canViewExecutiveSurface/canReviewDisputes, and make requiresInternalAccess a real gate) is a genuine auth-boundary change that would remove support-access Justine relies on — that needs her explicit call, not a unilateral fix.
検証(実コード): Confirmed against real code. src/app/app/admin/speaker-mapping/page.tsx:26 hardcodes `const groups: SpeakerAliasGroup[] = []` with a PD-3 comment (lines 6-11). src/app/app/admin/imports/repair/page.tsx:26 hardcodes `const persistedBatches: RepairBatch[] = []` then buildRepairQueue([]) → empty (PD-3 comment lines 6-11). All three model modules are explicitly pure/no-IO: import-batch.ts ("No IO, no React"), import-review.ts ("Pure + deterministic"), repair-queue.ts ("No IO. Persistence ... is a backend follow-up"). The universal importer (TranscriptImporter.tsx) wraps BulkUploadForm which POSTs each file to /api/meetings/upload. That route (src/app/api/meetings/upload/route.ts) persists meetings + meeting_metrics + speaker_baselines + pattern_summaries + audit_log + trace, but emits NO TranscriptImportBatch / import-review / repair-queue row anywhere. grep of supabase/migrations confirms NO table for transcript_import_batch / import_review / repair_queue (latest migration 0051). Committed tests (folder59-c08/c09/c10/c11 *.test.ts) are all pure-unit tests over in-memory model functions — no end-to-end persistence test exists. Not WORSE (upload pipeline itself works end-to-end; only the triage convenience surfaces are empty), not ALREADY_FIXED.
根本原因: Sequence-C shipped the typed model + UI panels + unit tests for the universal importer's exception-review/repair/batch-mapping triage as a pure frontend-batched layer, deliberately deferring the server-side persistence (a TranscriptImportBatch table + the importer emitting/persisting batch+file+review-item rows, plus RLS). The live ingest path (/api/meetings/upload) persists the *meeting* and its analysis but never a *batch*, so the three admin pages that read persisted batches have no data source and render their empty states. This is intentional staging, documented in 3 file headers and docs/kashi-transcript-importer.md (PD-3).
アプローチ: Leave it deferred. The wedge functionality (transcripts ingest, detectors run, pattern summaries + traces render) works today; only the admin triage convenience surfaces are empty, and the empty states are calm/intentional, not broken. Building persistence is an L effort that touches multiple safety surfaces (new migration + RLS, new API route, public render) and needs the 4-lens adversarial gate — not worth pulling forward unless a design-partner pilot actually hits the multi-file exception-review workflow. When it IS built: add a transcript_import_batches + import_files (+ derived review items computed server-side via the existing pure classifyImportBatch) schema with org-scoped RLS mirroring meetings; have /api/meetings/upload (or a new batch-aware wrapper) persist the batch on ingest; then swap the three hardcoded empty arrays for server reads.
検証(実コード): Opened all cited files. meeting-ingest-policy-binding.ts:30-70 writes ingest-time fields incl. policy_binding_status='bound' and is imported at upload route.ts:661 + meet/teams/zoom/notta ingest (363/298/270/434), contradicting the gap's 'bound populated by nothing' / 'rows stay legacy_unbound' claim for NEW rows. runtime-policy-routing.ts:61-133 computes the analysis-time fields but grep shows it is imported by ZERO files (no src, no test). evidence_grade readers (me/page.tsx, member-private-projection.ts, longitudinal-case-analyzer.ts, evaluate-grace) all reference OTHER tables (longitudinal_case_analyses / projection views / evidence_grade_at_send), not the 0051 meeting_metrics.evidence_grade column. No *.test.ts references any of the 3 governance binding/routing modules. Conclusion: analysis-time deferral is REAL and intentional (documented in 0051 header + binding file lines 4-7); the gap's ingest-binding claims are inaccurate; the actionable residue is the missing test for the unwired safety-critical routing logic.
根本原因: F60 Activation staged ingest-time binding (PR-1, live) ahead of analysis-time per-output binding (deferred). Migration 0051 pre-provisions analysis-time columns; their producer (wiring of runtime-policy-routing.ts into the analysis pipeline) is the deferred follow-up. The standing exposure is that the safety-critical routing/evidence-grade decision logic is committed but unreferenced and has zero regression tests.
アプローチ: Keep the analysis-time wiring DEFERRED — it is correct, documented staging, not a defect. Do NOT rush a half-built analysis-time writer. The one cheap, high-value action now is to add a committed unit test for runtime-policy-routing.ts so its safety invariants self-defend before any future PR wires it live; this also makes the dead-code status intentional rather than accidental. Optionally add a tiny test pinning the ingest-binding column set so a future analysis-time PR cannot silently regress the 'bound' status semantics.
検証(実コード): Confirmed as stated. src/lib/governance/communication-policy-audit.ts:243-257 recordCommunicationPolicyAudit() swallows any write failure (try/catch -> safeLogger.error + return null), never throwing; its own JSDoc says "never breaks the caller's already-committed mutation." Callers run the mutation first, audit second: in src/app/api/admin/communication-policy/draft/publish/route.ts:186 publishDraftCommunicationPolicy() commits, then :190 recordCommunicationPolicyAudit() runs — if the insert fails the policy is published with zero audit trail. Same pattern in 7 other routes (draft, validate, simulate, notices/generate, rollback, emergency-pause, emergency-pause/disable). Compounding: publishDraftCommunicationPolicy() in communication-policy-repository.ts:408-413 is itself non-atomic (retire-then-activate over supabase REST, guarded only by the one_active partial unique index). No committed test exists for the audit module at all (grep communication-policy-audit across *.test.ts = 0 hits; no __tests__ dir) — matches the audit's "no committed test" flag. Fix is feasible: migration 0050 already defines PL/pgSQL functions+triggers (cpv_freeze_published_snapshot, cpae_block_mutation), so an atomic publish+audit RPC slots into existing infra. The team self-flagged this in commit c19ac8b and in code comments as a P04 follow-up.
根本原因: supabase-js has no multi-statement REST transaction; mutation and audit insert are separate calls, and the team chose best-effort audit to keep mutations resilient. Atomic RPC deferred behind the §12 runtime-pipeline re-baseline.
アプローチ: Leave the deferral in place but harden the deferral so it self-defends, then schedule the real atomic-RPC fix alongside the already-planned §12 publish-RPC work. The full fix is a Postgres RPC (publish_communication_policy) that wraps retire+activate+audit-insert in one transaction; until then, add a committed regression test pinning the best-effort contract and a metric/alert so a silent audit-loss is observable rather than invisible.
検証(実コード): Confirmed by reading the real code. src/app/app/history/page.tsx exists and is fully implemented (Folder 57 PR05, AC-HISTORY-001..020). src/lib/navigation/workspaces.ts line 14 explicitly comments "history/trace/coverage have no nav home" and defines exactly 6 canonical workspaces (none is History). PortalHeader.tsx:188-193 drives the entire nav from getVisibleWorkspacesForRoles() which reads only WORKSPACES — so /app/history is completely absent from primary nav for all roles. admin-console.ts (8 sub-sections) also has no /app/history entry. The only reachable CTAs are: /app/me/page.tsx:393 (one inline link), UploadMoreCTA (src/components/cta/UploadMoreCTA.tsx:40), ImportResultSummary (src/components/upload/ImportResultSummary.tsx:25), and AdminReadinessCockpit (src/components/admin/AdminReadinessCockpit.tsx:218,304). No test in test/ asserts a nav link to /app/history — test/navigation-workspaces.test.ts pins exactly 6 workspaces and would fail if a 7th were added without updating the test.
根本原因: The workspace model (workspaces.ts) was authored as a strict 6-workspace constitution ("No more without an audit-proven exception" — line 42) AFTER Folder 57 PR05 shipped /app/history. The F59 Sequence C nav migration intentionally deferred History because it did not map cleanly to any of the six role-oriented workspaces. The comment at line 14 acknowledges the omission but does not resolve it. The RC57 acceptance criteria included "a History/logs page in nav" which was never fulfilled because the nav migration happened in a later sequence.
アプローチ: The cleanest resolution that respects the 6-workspace constitution is to add /app/history as an eighth entry in the Admin Console sub-navigation (admin-console.ts), alongside the existing "Transcript Imports" entry that already links to /app/admin/imports. For member/manager roles who cannot access admin-console, a secondary persistent entry should be added to the "My View" sub-navigation (similar to /app/me/audit) so all roles have a discoverable path. This keeps the 6 top-level workspace count unchanged (no WorkspaceId addition needed) and avoids a nav-model schema change.
検証(実コード): Confirmed via git log + gh pr CLI: P16 (99498e0), P17 (cc68242), P18 (46956b6, c19ac8b) have NO PR numbers in their commit subjects. However, they WERE merged via GitHub PRs #499, #500, #501 respectively. P01 (3910894) has `(#484)` in subject; P02 (a8f2747) has `(#485)` in subject. The trace exists in GitHub PR records but is not discoverable in commit graph. This is a real hygiene gap vs. the P01-P05 precedent.
根本原因: Commits merged via GitHub's squash/rebase + fast-forward strategy stripped PR numbers from commit subjects. P01-P05 had PR numbers hand-added to subjects (or were created with `-m` flag that included them). P16-P18 were merged without amending commit messages post-merge to include PR references. No pre-commit or commit-msg hook enforces PR-number presence.
アプローチ: Add a `.husky/commit-msg` hook that validates PR-number presence in commit subjects for merges to main. For already-landed P16/P18, add a one-line ledger (e.g., in AGENTS.md or parallel-session-coordination.md) documenting the PR → commit mapping. This is **documentation-only remediation** — no code change needed. The hook will self-defend future commits.
検証(実コード): Four distinct states found in real code: 1. `overflow-x: clip` on `body` (globals.css:86) — not asserted in ANY committed test. `test/i18n-day11.5-css-regression.test.ts` pins `overflow-wrap: anywhere` and the `word-break: keep-all` ban, but never asserts `overflow-x: clip`. Removal would be silent. 2. Reduced-motion rule — `test/homepage62-gates.test.ts:232-236` does assert co-presence of `prefers-reduced-motion: reduce` and `home-disclosure-motion` as raw strings in the CSS file. This is a real committed test, but it only checks string co-presence, not that the rule is a scoped `@media (prefers-reduced-motion: reduce) { .home-v61 .home-disclosure-motion { transition-duration: 0.01ms } }` block. A move or refactor could satisfy the existing assertion while breaking the actual rule. 3. Keyboard Enter/Space toggle — `test/homepage62-disclosure.test.tsx` proves `DisclosureCard` renders a `<button type="button">` with `aria-expanded` and `aria-controls`. Since Enter/Space activation is unconditional browser native behaviour for `<button>`, this source-of-truth proxy is sound. No additional unit test can improve on this without jsdom/real browser. 4. 320–1440px scrollWidth sweep — `qa/day11.5/no-overflow.spec.ts` exists but is a `@ts-nocheck` stub with an explicit comment: "Playwright is not installed in this repo. Run requires `npm i -D @playwright/test`". No `playwright.config.ts` exists at repo root. The spec does NOT run in `npm test`. The gap is real for this dimension.
根本原因: Two separate root causes: (a) Overflow invariant: `i18n-day11.5-css-regression.test.ts` was written to pin the `word-break: keep-all` rollback risk, not the `overflow-x: clip` guard. The guard was added later as a JIS X 8341-3 safety net and landed without a companion assertion. (b) Viewport scrollWidth sweep: Playwright was never installed when the QA work was done; the spec file was written as a forward-stub but never wired into CI. Because no `playwright.config.ts` exists and the dep is absent, `npm test` (vitest only) never runs it.
アプローチ: Two targeted additions, both in the existing Vitest (no new deps needed): 1. Extend `test/i18n-day11.5-css-regression.test.ts` with a new `it` that strips CSS comments and asserts `body { ... overflow-x: clip ... }` as a regex match on the `body` selector block. Pin in the same file so the Day 11.5 CSS guard is a single coherent test. 2. Extend `test/homepage62-gates.test.ts` (Gate 5 / Disclosure source section) with a tighter reduced-motion assertion: regex-match that the `@media (prefers-reduced-motion: reduce)` block contains `.home-v61 .home-disclosure-motion` AND sets `transition-duration`. The existing loose co-presence check stays; the new assertion is additive. 3. Add a comment to the keyboard section of `test/homepage62-disclosure.test.tsx` that explicitly documents why no further unit test is needed (native button + source-proven), so reviewers don't add a jsdom dependency that buys nothing. This is a one-line doc change, not a new test. 4. For the viewport scrollWidth sweep: document in `qa/day11.5/no-overflow.spec.ts` that installing Playwright and running it is the intended CI gate. Do NOT install Playwright now (L effort, out of scope). Instead add a `// TODO(RC61): add to CI when Playwright installed` comment and accept DEFER_OK on that dimension. The CSS `overflow-x: clip` assertion in step 1 is the committed low-cost proxy.
検証(実コード): The gap is real but narrower than stated. Source files are clean: `src/lib/navigation/workspace-access.ts` and `src/lib/readiness/readiness-state.ts` contain no DRAFT banners — only a `// DOCTRINE (this is the sign-off surface —` comment in readiness-state.ts:8, which is a label, not a pending-status banner. The commit titles in git history ("feat(folder59-seqC-C03): ... [DRAFT — NEEDS SIGN-OFF]", "feat(folder59-seqC-C06): ... [DRAFT — NEEDS SIGN-OFF]") are immutable. However, `docs/kashi-ux-navigation-model.md:271-272` still contains the literal text "**DRAFT — NEEDS SIGN-OFF**" in the Prompt 06 section. The doc `docs/kashi-readiness-state-machine.md` has no Status header, unlike `docs/kashi-meeting-type-validity.md` which was updated to "SIGNED OFF — Justine approved PR #470, 2026-06-09" after C12 approval. The C15 GO verdict (d4fa74d, 2026-06-09) passed both Scope A (Navigation/C03) and Scope E (Readiness/C06) with zero blockers. So the functional sign-off happened implicitly via C15, but no doc was updated to close the loop explicitly for C03/C06 the way C12's doc was.
根本原因: Two gaps in the sign-off closure process: (1) C12's doc got an explicit "SIGNED OFF — Justine approved" header update but C03 and C06 docs did not receive equivalent treatment after C15 GO verdict; (2) The C03 open decision item ("Decide the admin support-access model") at `docs/kashi-ux-navigation-model.md:166` was never explicitly resolved or deferred-acknowledged. The C15 GO verdict and C12 calibration approval together constitute a de facto sign-off, but no one looped back to stamp the C03/C06 docs the way C12 was stamped.
アプローチ: Two minimal doc edits only — no source changes needed. (1) Add a "SIGNED OFF" status line to `docs/kashi-readiness-state-machine.md` header mirroring the C12 doc pattern, citing C15 GO verdict + C12 calibration approval 2026-06-09. (2) Update `docs/kashi-ux-navigation-model.md` Prompt 06 section (lines 271-272) to replace "DRAFT — NEEDS SIGN-OFF" with "SIGNED OFF — C15 GO verdict 2026-06-09"; and in the Prompt 03 section at line 166 ("Decide the admin support-access model"), record the actual disposition (admin tightened to nav-only, host-gate unchanged, support-role deferred to C13 — which shipped — so the core C03 decision closed). Justine must confirm the admin support-access question is settled before the line 166 item is marked closed.
検証(実コード): All three canonical shells confirmed at src/app/app/organization/page.tsx:1-18, src/app/app/team-mirror/page.tsx:1-18, src/app/app/review-queue/page.tsx:1-17. Each calls resolveWorkspaceAccess, returns WorkspaceForbidden on wrong role, then redirects to the legacy host. The deferral is explicitly documented at docs/kashi-ux-navigation-model.md:141-142 ("The content host-move (making the canonical URL the real home) is staged per spec §8 to keep C03 shell-only") and listed under "Remaining work for Prompt 04" at line 167-168. C04 completion notes (lines 173-214) confirm it was not picked up in C04 either — no sequence step has yet claimed this item. The gap description is 100% accurate; calling it ALREADY_FIXED because this is the intentional shipped state, not an oversight.
根本原因: Deliberate staged delivery. C03 was scoped to "shell-only": establish canonical URLs with correct access gates and redirect to existing content hosts, deferring the actual content host-move to a later prompt. The spec (C03 §8) and the navigation model doc both explicitly call this out. The legacy routes remain the real content hosts as a matter of design, not neglect.
アプローチ: No action needed until a future sequence prompt (unassigned, post-C05 based on the remaining-work list) explicitly claims the content host-move. When that prompt runs, for each of the three surfaces: (1) move the real page content from the legacy route to the canonical route file, (2) replace the legacy route with a guard-then-redirect to the canonical URL (reversing the current direction), (3) update workspaces.ts routeStatus from "planned" to "active", and (4) update the C03 test's guard-before-redirect assertions to cover the NEW redirect direction. The existing test already pins the invariant that no legacy host appears as a primary nav href, which is sufficient protection for the current staged state.
検証(実コード): Confirmed zero jest-axe/axe-core/toHaveNoViolations usage in all 336 test files (grep returned empty). However the repo already has substantial hand-rolled ARIA assertions via renderToStaticMarkup: test/homepage62-disclosure.test.tsx:24-60 verifies aria-expanded, aria-controls, role="region", button semantics, inert on collapsed panel, min-h-[44px] tap target, and focus-visible:ring. test/folder58-prompt14-design-system.test.tsx:157,216,271 asserts role="note". The live source confirms solid ARIA implementation: src/components/demo/RoleReportTabs.tsx uses role="tablist/tab/tabpanel" with aria-selected/aria-controls/aria-labelledby; src/app/app/admin/communication-policy/ has role="alert/status/note/radiogroup/radio/tablist" with proper aria-checked, aria-live attributes. No clickable divs/spans exist — all interactive elements use semantic button elements. jsdom is not installed (vitest.config.ts has no environment: "jsdom") so axe-core would require adding vitest-axe + jsdom as dev deps, which is non-trivial. The audit overstated the risk: the highest-value a11y patterns (disclosure widget, tab widget, live regions, radio group, touch target sizing) are already gate-tested.
根本原因: The audit correctly identified zero automated axe tooling but failed to credit the hand-rolled ARIA assertions already in the test suite. The renderToStaticMarkup pattern (no jsdom) is an intentional constraint that limits what axe can scan, and the team compensated with targeted structural assertions on each a11y-critical component.
アプローチ: No new tooling required at this time. If the P18 Communication Policy console pages or the demo ingest form grow significantly, adding vitest-axe (which accepts HTML strings from renderToStaticMarkup without jsdom) would be a lightweight incremental gate. The correct scope is: one vitest-axe smoke test covering the most complex composite widget (PolicyProfileCards role="radiogroup", or NoticeVersionsClient role="tablist") to catch regressions when those components change. This is additive, not blocking.