kashi · review-comments-audit · remediation code-review

RC57–62 ギャップ — どう直すか(コードレビュー)

19 gap のうち 14 件を、13体のレビューエージェントが main の実コード(eb466f8)を実際に開いて検証 → アプローチ・具体的変更・追加テスト・労力・リスク・安全面を設計しました。安全面に触れる gap は opus + 4-lens adversarial gate を必須として印付け。

最重要 · G06 · RC59 監査より深刻 近いうち

監査が「問題なし」とした箇所に、実在する公開ページの漏洩が1件見つかった

反映監査では「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 ログの確認も推奨。

1
本番露出のある漏洩 (G06)
7
安全面 (opus+gate)
6
直すべき (now/soon)
3
要・判断
5
後回しでOK

全体方針

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.

推奨する実行順(依存関係を尊重)

  1. 1. SAFETY-TRACK FIRST — G06 (opus + 4-lens): anonymize the dyad from/to at the buildDemoVisualPayloads boundary (not per-render-site), add a RESULT_WITH_DYAD fixture, render-prove 'Yamada'/'Tanaka' never appear and inverse synthetic=true still shows real names. This is the only LIVE leak; CI-green is insufficient per the 2026-06-06 lesson, so mutation-prove the no-leak test bites before merge.
  2. 2. Quick wins in parallel (no decision, no safety surface): G13 (commit-msg hook + PR ledger), GTEST (overflow-x:clip + reduced-motion assertions), G10 (mark covered). These are haiku/sonnet, S-effort, and can land independently of everything else.
  3. 3. G08-tests (opus gate because the file is a safety-surface invariant-pin, but trivial): commit runtime-policy-routing + ingest-binding unit tests locking the routing caps before any future wiring.
  4. 4. Put the two decisions to Justine NOW (G00 nav scope, G04-Part2 admin reach). G04-Part2's answer also unblocks G02's line-166 doc stamp.
  5. 5. On Justine's G00 answer: G00 (sonnet, additive admin-console section + optional My View link + i18n + nav test). On her G04 answer: G04-Part1 characterization tests always (opus + 4-lens), then G04-Part2 host-gate tightening ONLY if she says yes. G02 doc stamps land alongside (haiku).
  6. 6. G09 (opus + 4-lens): build the comm-policy audit read page — closes a real Next.js 404, all plumbing exists; the 4-lens gate verifies request_context_json/before_json never render and the role guard fires.
  7. 7. G12 (opus + 4-lens): add the live-Postgres RLS behavioral test for the 0050/0051 invariants + the one-time prod-apply dashboard verification with Justine (record applied-state in the deploy ledger; do NOT automate prod DDL).
  8. 8. G11 interim (opus): keep best-effort audit but add the audit_write_failed metric + the best-effort-contract regression test. Schedule the atomic publish RPC onto the §12 re-baseline, not now.
  9. 9. Deferred tail stays parked: G01, G03, G08-wiring — each with a named trigger, revisited only when that trigger fires.

あなたの判断が要る 3 点 (クリティック指摘:G00・G02・G04 は実は同じ「管理者サポートアクセス方針」1問に集約できる)

[G00] Should /app/history get a nav entry as an Admin Console sub-section ONLY (admin/ceo reach it), or ALSO a persistent My View link for member/manager who have no admin access? Both keep the 6 top-level workspaces unchanged; the question is purely whether non-admin roles deserve a nav-persistent history link or whether the existing UploadMoreCTA in /app/me suffices for them.
[G04] Do you want to ACTUALLY revoke admin's direct-URL reach to /app/ceo and /app/reviewer — i.e. drop 'admin' from those two requireRole host gates and from canViewExecutiveSurface/canReviewDisputes, and promote requiresInternalAccess into a real gate (needs a new internal/support role)? YES = admin loses exec/reviewer/diagnostics support-access you may rely on. NO = keep the documented carve-out and ship only the Part-1 pinning tests. This is the same admin-support-access question that also unblocks the G02 line-166 doc stamp.
[G01] Has any design-partner pilot actually hit (or is about to hit) the multi-file exception-review / repair-queue / batch-speaker-mapping workflow? If no, G01 stays deferred and the three empty panels are fine. If yes, that's the trigger to schedule the L-effort persistence build (and it unblocks G03's host-move thinking too).

すぐ取れる勝ち筋(判断不要・安全面なし)

後回しで問題ない(トリガー付き)

労力見積り: 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.

完全性クリティック — 監査/設計が見落としかけた点

gap 別 コードレビュー・カード (WORSE→safety→fix→decision→defer 順)

G06 · RC59 監査より深刻 近いうち

Seed-name firebreak holds, but the live "Top dyad" chip renders raw (un-anonymized) speaker names

中 (<1日)risk: med — touches the public /demo render coowner: opussafety: public render (display-identity / speaker anon

検証(実コード): 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.

具体的な変更:
  • src/components/demo/buildDemoVisualPayloads.ts: when anonymizeNamedTelemetry is true, build a stable speakerId->Participant-index map from result.speakingShare.participants (reuse anonymizedLabelForIndex / buildSpeakerDisplayMap from src/lib/pipeline/display-privacy-mode.ts) and expose an anonymized directional-pair list (or a label-resolver) on the payload common bundle.
  • src/components/demo/resolveDemoResultContract.ts: in sortedTopFromPairs (lines 652-668), map best.sourceDisplayName/affectedDisplayName through the anonymized label map when the result is in anonymized mode, so topInterruptionDyad.from/to are 'Participant A/B' not the raw speakerLabel. Alternatively resolve from speakerId via the central map rather than the raw displayName.
  • src/components/demo/DemoMetricStrip.tsx (line 156): consume the anonymized from/to (no raw-name interpolation). After the upstream fix this needs no per-site shouldAnonymize call, but assert it reads the anonymized field.
  • OPTIONAL hardening - src/lib/analysis/structural-metrics/interruption-directionality.ts:92-94: leave raw for fixture/debug mode but ensure the public route never forwards raw displayName; the cleanest single chokepoint is to anonymize in buildDemoVisualPayloads so all consumers inherit it.
追加するテスト/ゲート: Extend test/va08-demo-ingest-visual-upgrade.test.tsx (or folder59-p25-display-privacy.test.ts §3): add a RESULT_WITH_DYAD fixture carrying display.directionalInterruption.pairs with named speakers (e.g. sourceDisplayName:'Yamada', affectedDisplayName:'Tanaka', count:4) and canShowDyadLevelMetrics:true, renderToStaticMarkup(<DemoVisualAnalysisResult result={RESULT_WITH_DYAD} isSyntheticSample={false} />), then expect(html).not.toContain('Yamada'); expect(html).not.toContain('Tanaka'); expect(html).toMatch(/Participant [A-Z] → Participant [A-Z]/). Mutation-prove it bites by temporarily reverting the fix. Mirror the inverse (isSyntheticSample={true} shows the real names) so the synthetic guided-demo behavior stays locked.
G09 · RC60 実在 近いうち

Communication-policy audit read screen missing — nav link dead-ends at Next.js 404

小 (<半日)risk: low — the API, RBAC, and data library arowner: opussafety: src/app/app/admin/communication-policy/** (adm

検証(実コード): 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.

具体的な変更:
  • src/app/app/admin/communication-policy/audit/page.tsx — CREATE: server component following publish/page.tsx pattern. requireRole(['admin','ceo','hr_compliance'], { next: '/app/admin/communication-policy/audit' }). Call listCommunicationPolicyAuditEvents(policyServiceDb() as CommunicationPolicyAuditReadDb, orgId, { limit: 50 }) directly (avoids internal HTTP). Render CommunicationPolicyNav (current='audit') + a read-only table of events (id, actor_role, action, changed_area, reason, created_at). No before_json/after_json expansion on first load (too verbose). Add a date-range query-param filter wired to searchParams. Mark 404 shell with a 'Coming soon' fallback if called without the page — not needed once page is created.
  • src/app/app/admin/communication-policy/overview/presentation.ts line 110 — EDIT: change `audit: 'not_started'` to `audit: 'complete'` after the page is created, so the nav badge reflects reality.
  • test/folder60-p16-audit-read-ui.test.tsx — CREATE: render test for the new page component (renderToStaticMarkup / React Testing Library). Assert: (1) page renders without throwing when listCommunicationPolicyAuditEvents returns rows, (2) request_context_json is never present in rendered HTML, (3) nav has aria-current='page' on the audit item, (4) 403 redirect occurs for manager/member roles (test the requireRole guard fires).
追加するテスト/ゲート: Add `test/folder60-p16-audit-read-ui.test.tsx` (vitest, renderToStaticMarkup). Gate: assert rendered HTML does not contain the string `request_context_json` or `ip` (request context leak), assert `aria-current=\"page\"` appears on the audit nav item, assert the table renders at least one row given mocked data. This runs in the existing `npm test` suite and must stay green in CI before any merge.
G12 · RC60 実在 近いうち

P03 DB-floor invariants (RLS / immutability trigger / partial-unique index) are only mock-tested; no live-Postgres assertion exists and prod application is manual/unverified

中 (<1日)risk: low — purely additive test + a seed helpowner: opussafety: RLS + supabase/migrations/** (the test asserts

検証(実コード): 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.

具体的な変更:
  • test/rls/folder60-comm-policy-invariants.test.ts (NEW): live-PG test using the existing setup.ts helpers (connect/resetDatabase/applyAuthShim/applyMigrations/grantSupabaseRoles/seedTwoOrgs/asUser/asAnon). Seed an active comm-policy version for orgA. Assert: (a) inserting a SECOND active row for the same org fails with unique_violation (one_active_communication_policy_per_org); same for a second draft; (b) UPDATE of snapshot_json/snapshot_hash on an active/retired row raises the cpv_freeze check_violation, while a draft->active transition succeeds; (c) UPDATE and DELETE on communication_policy_audit_events both raise the cpae append-only exception even as an org admin; (d) cross-org: orgA admin SELECTs zero of orgB's communication_policy_versions/audit_events/simulator_runs/notice_versions/emergency_pauses (default-deny RLS); (e) a member (non admin/ceo/hr_compliance) and asAnon SELECT zero rows. Wrap each test in BEGIN/ROLLBACK like isolation.test.ts. Add a mutation-test comment (per setup.ts L12-17 convention) naming which 0050 line to comment out to prove the test bites.
  • test/rls/setup.ts (EDIT, minor): extend seedTwoOrgs (or add a seedCommPolicy helper) to insert one communication_policy_versions active row per org so the new test has fixtures; keep it additive so existing tests are unaffected.
  • supabase/migrations/0050_communication_policy_console.sql + 0051 (NO code change): add a one-line prod-apply record to the deploy/migration ledger (e.g. workspace/DEPLOY_LINKS.md or the migrations sign-off log) noting date applied + who confirmed via dashboard introspection. This is a documentation/verification action, not a code edit.
  • docs/dev (or MASTER_EXECUTION_LEDGER): note that 0050/0051 are [SIGN-OFF: schema] manual-apply and link the new RLS test as the CI guard for their invariants.
追加するテスト/ゲート: The new test/rls/folder60-comm-policy-invariants.test.ts is itself the committed regression gate — it runs in CI via the existing `npm run test:rls` step (.github/workflows/ci.yml L54) against the postgres:16 service, with no new infra. It must include a documented mutation check (comment out cpv_freeze_published_snapshot body / drop one_active partial index in 0050 -> the relevant assertions must fail) to prove the test actually exercises the SQL and not a tautology. Prod-apply verification is a one-time manual dashboard introspection with Justine, recorded in the deploy ledger (not a CI gate, since it concerns infra state not code).
G04 · RC58 実在 要・判断

Admin retains direct-URL reach to /app/ceo and /app/reviewer; nav-visibility tightened but host gates and requiresInternalAccess unchanged, and the deliberate boundary has zero committed tests

小 (<半日)risk: low — test-only additions, no runtime/auowner: opussafety: Auth/role boundary surface: src/app/app/ceo +

検証(実コード): 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.

具体的な変更:
  • src/lib/navigation/__tests__/admin-boundary.test.ts (NEW): characterization test pinning, per role, decideWorkspaceAccess()/userCanAccessWorkspace() for ceo, reviewer, admin-console — assert admin gets state 'forbidden' for 'ceo' and 'reviewer' nav workspaces and 'allowed' for 'admin-console'; assert ceo allowed only on ceo workspace, hr_compliance/restricted_investigator allowed only on reviewer. This locks the nav-visibility tightening so re-adding admin to allowedRoles fails CI.
  • src/lib/auth/__tests__/role-host-gates.test.ts (NEW): pin role-routing helpers as the documented support-access state — assert canViewExecutiveSurface('admin')===true and canReviewDisputes('admin')===true and defaultLandingForRole for each role, each with an inline comment that this is the DELIBERATE deferred carve-out (RC58) and flipping it is a sign-off decision. The test failing on edit forces a conscious decision rather than silent drift.
  • src/lib/navigation/admin-console.ts:45 (DOC-ONLY, optional): tighten the requiresInternalAccess JSDoc to state explicitly it is presentation-only today with no access enforcement and points at the C13 deferral, so no reader mistakes it for a live gate.
  • No host-gate or helper logic change in this remediation — dropping 'admin' from ceo/reviewer requireRole arrays is gated behind the DECISION_NEEDED question, not done here.
追加するテスト/ゲート: Two committed Vitest files above are the self-defense gate. The nav test mutation-proves the tightening (flip admin back into workspaces.ts ceo/reviewer allowedRoles -> test goes red). The host-gate/helper test pins the current admin reach so a future narrowing of the requireRole arrays or canView* helpers also goes red, forcing the C03 sign-off conversation. Both run in the existing test step; no new infra. If/when Justine decides to actually tighten, the same tests are the single place to update intentionally.
要・判断: Two-part. (Part 1, no decision, I can do now) Commit characterization tests that pin the current deliberate boundary so it stops being silent. (Part 2, your call) Do you want to ACTUALLY remove admin's direct reach to /app/ceo and /app/reviewer — i.e. drop "admin" from those two host-gate requireRole arrays and from canViewExecutiveSurface/canReviewDisputes, and promote requiresInternalAccess into a real gate (introducing an explicit internal/support role)? Yes = tighten now and admin loses exec/reviewer/diagnostics support-access; No = keep the documented carve-out and ship only the pinning tests.
G01 · RC58 実在 後回しでOK

PD-3 persistence gap: no server-side TranscriptImportBatch, so exception-review / repair-queue / batch-speaker-mapping render empty in prod

大 (>1日)risk: low — leaving it deferred carries no corowner: opussafety: If/when built it touches: src/app/api/** (the

検証(実コード): 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.

具体的な変更:
  • DEFER — no change now. When built (future PR): supabase/migrations/0052_transcript_import_batches.sql — add transcript_import_batches(id, org_id, label, uploaded_by, uploaded_at, status) + transcript_import_files(id, batch_id, org_id, filename, status, detected_* metadata-only cols, quality_summary jsonb, errors[], warnings[]) with org-scoped RLS mirroring meetings (NEVER transcript body text — metadata-only, same invariant as upload route).
  • src/app/api/meetings/upload/route.ts — after the meeting insert, insert/upsert a transcript_import_files row (and parent batch keyed on body.uploadSessionId/batchLabel) carrying ONLY the sanitized metadata already accepted at lines 487-509; reuse insertExtras fields, add nothing new to the payload contract.
  • src/app/app/admin/imports/repair/page.tsx:26 — replace `const persistedBatches: RepairBatch[] = []` with a server read of the org's unsettled transcript_import_files grouped into RepairBatch[], then buildRepairQueue(persistedBatches).
  • src/app/app/admin/speaker-mapping/page.tsx:26 — replace `const groups: SpeakerAliasGroup[] = []` with a server read deriving SpeakerAliasGroup[] from persisted batch files' speakerAliases.
  • src/components/admin/imports (C09 review panel host page) — feed classifyImportBatch(persistedFiles) instead of the empty fixture.
追加するテスト/ゲート: When built: add test/folder59-c08-import-batch-persistence.test.ts — an end-to-end-ish route test that POSTs a 2-file batch (one clean, one missing date) to /api/meetings/upload and asserts (a) a transcript_import_batches row + 2 transcript_import_files rows are written, (b) NO transcript body text is persisted on any file row (mutation-prove the no-leak: temporarily write text → test must go red), (c) buildRepairQueue over the persisted rows surfaces exactly the missing-date file. Plus an RLS test in test/*-rls* asserting cross-tenant admin cannot read another org's import batches. Until then, the existing pure-unit tests (c08/c09/c10/c11) are the committed defense for the model layer and remain valid.
G08 · RC60 非問題に格下げ 後回しでOK

Analysis-time per-output policy binding deferred; routing/evidence logic committed but unwired AND untested

小 (<半日)risk: low — adding tests for currently-unwiredowner: opussafety: detector/capability + routing registry logic (

検証(実コード): 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.

具体的な変更:
  • src/lib/governance/runtime-policy-routing.test.ts (NEW): unit-test resolvePolicyRoutingDestination + computePolicyEvidenceGrade pinning the safety invariants — (a) blocked/insufficient evidence caps to simulation_only/private_self_view, (b) floor_time_imbalance/agreement_asymmetry alone can never reach neutral_review or restricted_review_support, (c) semantic_hybrid lane with semantic disabled => simulation_only + abstention code semantic_lane_not_enabled, (d) emergency-paused surface downgrades to simulation_only, (e) k-anonymity failure suppresses trend_monitoring, (f) no input path ever yields a destination outside RANK_ROUTE. Mutation-prove at least one cap (e.g. flip detectorCap('agreement_asymmetry') to 4) makes the test fail.
  • src/lib/governance/meeting-ingest-policy-binding.test.ts (NEW): assert resolveMeetingIngestPolicyBinding returns policy_binding_status='bound' with the ingest-time column set when a policy resolves, 'legacy_unbound' (NO error code) on NO_ACTIVE_POLICY, and 'failed_policy_resolution'+error code on unexpected RuntimePolicyResolutionError — and that it NEVER throws and NEVER returns any analysis-time field (guards the ingest-vs-analysis boundary so the deferred PR stays correctly scoped).
  • No production code change. Leave migration 0051 columns and runtime-policy-routing.ts as-is; the analysis-time writer remains a tracked follow-up PR.
追加するテスト/ゲート: Commit src/lib/governance/runtime-policy-routing.test.ts (and optionally the binding test) into the existing vitest suite run by CI's test step. This makes the routing cap invariants self-defending before the deferred analysis-time wiring lands. The future analysis-time PR that wires these functions into a meeting_metrics write must, per the 4-lens adversarial gate, add a no-leak/render test proving analysis-time fields never expose surveillance/scoring surfaces — but that gate fires when the wiring PR is written, not now.
G11 · RC60 実在 後回しでOK

Communication-policy audit writes are best-effort, not transactional — a mutation can commit with no audit row

中 (<1日)risk: med — the atomic-RPC fix touches the pubowner: opussafety: RLS + supabase/migrations/** (new publish RPC

検証(実コード): 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.

具体的な変更:
  • supabase/migrations/00NN_communication_policy_publish_rpc.sql (NEW): add PL/pgSQL function publish_communication_policy(p_org, p_actor, p_expected_hash, p_effective_from, p_reason) that, inside a single transaction, runs the retire-then-activate UPDATEs AND inserts the communication_policy_audit_events row, raising (rolling back the publish) if the audit insert fails — i.e. fail-closed. Mirror for the other mutating actions or add a generic mutate-with-audit RPC.
  • src/lib/governance/communication-policy-repository.ts:369-452 — replace the two-step retire/activate in publishDraftCommunicationPolicy with a single db.rpc('publish_communication_policy', {...}) call; remove the NOTE(transaction) comment once atomic.
  • src/app/api/admin/communication-policy/draft/publish/route.ts:186-204 — fold the policy_published audit into the RPC call so publish+audit are one unit; keep recordCommunicationPolicyAudit() only for the pre-mutation publish_blocked/validation_failed events (where best-effort is acceptable because no state changed).
  • src/lib/governance/communication-policy-audit.ts:243-257 (INTERIM, ship now) — keep best-effort recordCommunicationPolicyAudit for non-mutating events but add a durable failure signal: increment an observability counter (e.g. safeLogger.error already fires; add a metric tag audit_write_failed so it is alertable) and keep the correlation_id. Do NOT change throwing behavior here yet — the route-level RPC is what makes mutations fail-closed.
追加するテスト/ゲート: Two committed tests. (1) Interim self-defense (ship now, S): src/lib/governance/__tests__/communication-policy-audit.test.ts — mock db.from().insert() to reject; assert recordCommunicationPolicyAudit() resolves null (pins the documented best-effort contract) AND assert safeLogger.error was called with error_class 'audit_write_error' + a correlation_id (pins that the loss is logged, not silent). Mutation-prove it bites by flipping the catch to rethrow. (2) Fail-closed gate (with the RPC, M): an integration test asserting that when the audit insert fails inside publish_communication_policy, the policy version is NOT left active (transaction rolled back) — proving publish and audit are atomic.
G00 · RC57 実在 近いうち

/app/history fully built but has no persistent nav entry point for any role

小 (<半日)risk: low — adding a new admin sub-section entowner: sonnet

検証(実コード): 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.

具体的な変更:
  • src/lib/navigation/admin-console.ts: Add a new AdminConsoleSectionId 'history' and a new entry to ADMIN_CONSOLE_SECTIONS: { id: 'history', label: 'Upload History', labelI18nKey: 'admin.console.sections.history', href: '/app/history', description: 'Role-scoped view of all uploaded meetings and their analysis status.', order: 3 } — insert between 'imports' (order 3) and 'speaker-mapping' (order 4); renumber speaker-mapping to order 4, readiness to 5, notices to 6, audit to 7, diagnostics to 8.
  • src/app/app/admin/layout.tsx (or the admin shell that renders the subnav): Verify it loops over getAdminConsoleSections() — if so, no change required; the new 'history' section renders automatically.
  • src/app/app/me/page.tsx: Add a persistent 'View upload history' link near the existing /app/history inline link at line 393, elevating it from a contextual CTA to a stable sub-surface link (same pattern as the /app/me/audit link in the nav model).
  • messages/en.json: Add key admin.console.sections.history = 'Upload History'.
  • messages/ja.json: Add key admin.console.sections.history = 'アップロード履歴'.
  • test/folder59-c04-admin-console.test.ts: Add a test that asserts ADMIN_CONSOLE_SECTIONS includes an entry with id 'history' and href '/app/history', and that getAdminConsoleSections() returns it ordered correctly.
  • test/navigation-workspaces.test.ts: No change needed — this test pins exactly 6 top-level workspaces, which stays true under this approach.
追加するテスト/ゲート: In test/folder59-c04-admin-console.test.ts, add: (1) it('includes a history section pointing to /app/history', () => { expect(ADMIN_CONSOLE_SECTIONS.find(s => s.id === 'history')?.href).toBe('/app/history'); }); (2) it('history section is ordered between imports and speaker-mapping', () => { const ordered = getAdminConsoleSections(); const histIdx = ordered.findIndex(s => s.id === 'history'); const importsIdx = ordered.findIndex(s => s.id === 'imports'); const speakerIdx = ordered.findIndex(s => s.id === 'speaker-mapping'); expect(histIdx).toBeGreaterThan(importsIdx); expect(histIdx).toBeLessThan(speakerIdx); }). These tests will catch any future removal of the nav entry.
要・判断: Should /app/history appear as an Admin Console sub-section only (visible to admin/ceo roles who can reach Admin Console), or should it also get a persistent link in My View sub-navigation for member/manager roles who have no admin access? Both approaches keep the 6 top-level workspaces unchanged — the question is whether non-admin roles deserve a nav-persistent history link or whether the UploadMoreCTA in /app/me is sufficient for them.
G13 · RC60 実在 近いうち

P16/P17/P18 commits lack PR numbers in subject lines

小 (<半日)risk: low + zero — documentation + hook only, owner: haiku

検証(実コード): 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.

具体的な変更:
  • .husky/commit-msg: Add hook that checks for PR number (#NNN) in subject when committing to main (allowlist for chore/docs commits that may be automation-driven; warn but don't block on dev branches)
  • docs/dev/parallel-session-coordination.md or AGENTS.md: Add a new 'F60 Console PR Mapping' section documenting: P16 = #499, P17 = #500, P18 = #501
追加するテスト/ゲート: Add a CI gate in `.github/workflows/ci.yml` or pre-commit hook: `git log --format='%s' -1 | grep -E '#[0-9]{3,}|chore|docs' || echo 'FAIL: commit message must include PR number (#NNN) or be marked chore/docs'`. Run this on every commit to main (enforce) and on dev branches (warn). This becomes the recurring regression test that prevents future P16/P17/P18-style gaps.
GTEST · RC61+RC62 実在 近いうち

Homepage 0-overflow + reduced-motion have no committed regression test pinning the CSS invariants

小 (<半日)risk: low — additive assertions in two existinowner: sonnet

検証(実コード): 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.

具体的な変更:
  • test/i18n-day11.5-css-regression.test.ts — add inside the `Day 11.5 — JA line-break CSS posture` describe: `it("body has overflow-x: clip as horizontal-scroll guard", () => { const code = css.replace(/\/\*[\s\S]*?\*\//g, ""); expect(/body\s*\{[^}]*overflow-x\s*:\s*clip/m.test(code)).toBe(true); });`
  • test/homepage62-gates.test.ts — replace the existing loose motion assertion (lines 232-236) with a tighter version: keep the two existing `expect` lines, then add `expect(css).toMatch(/@media\s*\(prefers-reduced-motion:\s*reduce\)[\s\S]*?\.home-v61 \.home-disclosure-motion[\s\S]*?transition-duration/)`
  • test/homepage62-disclosure.test.tsx — add a comment at the top of the file after line 8: `// Note: no jsdom/fireEvent keyboard test is added because Enter/Space activation is // unconditional native behaviour for <button>. The source-level proof that the // trigger IS a <button> (lines 21-25) is the complete and correct regression guard.`
追加するテスト/ゲート: The changes ARE the test. Self-defending: `npm test` (vitest run) will fail if (a) `body { overflow-x: clip }` is removed from globals.css, or (b) the `@media (prefers-reduced-motion: reduce)` block no longer contains `.home-v61 .home-disclosure-motion` with a `transition-duration` rule. The Playwright viewport sweep remains a stub; accept that the CSS guard + `overflow-wrap: anywhere` assertion is the CI-pinned proxy for the 320px overflow invariant until Playwright is installed.
G02 · RC58 実在 要・判断

Stale DRAFT banners in C03/C06 doc after functional GO verdict

小 (<半日)risk: low — doc-only changes; source code is aowner: haiku

検証(実コード): 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.

具体的な変更:
  • docs/kashi-readiness-state-machine.md:1-7 — add a Status header after the frontmatter blockquote: '> **Status:** SIGNED OFF — C15 GO verdict (Scope E PASS, 2026-06-09); C12 calibration approval by Justine (#470, 2026-06-09) anchored the meeting-type branch.'
  • docs/kashi-ux-navigation-model.md:271-272 — replace '**DRAFT —\nNEEDS SIGN-OFF**' with '**SIGNED OFF** — C15 GO verdict (Scope A + E PASS, 2026-06-09).'
  • docs/kashi-ux-navigation-model.md:166 — replace '- Decide the admin support-access model (sign-off).' with '- ~~Decide the admin support-access model~~ — **DEFERRED to C13** (Diagnostics internal-access permission, shipped). Core C03 nav-visibility tightening confirmed intentional by C15 GO verdict. Host-gate unchanged; `internal` role reserved per workspaces.ts comment.' (only if Justine confirms C03 admin-access is settled)
追加するテスト/ゲート: No regression test needed for doc-only banner cleanup. The existing pinned tests already guard the behavioral contracts: `test/folder59-c03-shell.test.ts` (262 lines, pins the role→canonical-route contract + admin nav-tightening) and `test/folder59-c06-readiness-state.test.ts` (171 lines, pins the 5-area state machine). These are the self-defending gates. If desired, a CI grep step could assert that neither doc still contains the literal string 'DRAFT — NEEDS SIGN-OFF' after the fix: `! grep -r 'DRAFT — NEEDS SIGN-OFF' docs/kashi-ux-navigation-model.md docs/kashi-readiness-state-machine.md`.
要・判断: The C15 GO verdict (2026-06-09) passed both Navigation (C03) and Readiness (C06) with zero blockers. C12 approval on the same date anchored the C06 readiness meaning. Two questions: (A) Is the C06 readiness doctrine sign-off formally closed — can we update docs/kashi-readiness-state-machine.md to 'SIGNED OFF' like C12's doc was? (B) Is the C03 admin support-access decision settled — specifically, the item at docs/kashi-ux-navigation-model.md line 166 ('Decide the admin support-access model'): is the answer 'admin sees nav-only tightening, support/diagnostics access deferred to C13 which shipped', so we can close that line?
G03 · RC58 既に解決 後回しでOK

Canonical URL shell-only: content still hosted at legacy routes (/app/ceo, /app/mirror/me, /app/reviewer)

小 (<半日)risk: low — current state is correct and self-owner: sonnet

検証(実コード): 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.

具体的な変更:
  • No changes required now. When the content host-move prompt runs:
  • src/app/app/organization/page.tsx -> move content from src/app/app/ceo/page.tsx here; replace src/app/app/ceo/page.tsx with a guard-then-redirect to /app/organization
  • src/app/app/team-mirror/page.tsx -> move content from src/app/app/mirror/me/page.tsx here; replace src/app/app/mirror/me/page.tsx with a guard-then-redirect to /app/team-mirror
  • src/app/app/review-queue/page.tsx -> move content from src/app/app/reviewer/page.tsx here; replace src/app/app/reviewer/page.tsx with a guard-then-redirect to /app/review-queue
  • src/lib/navigation/workspaces.ts -> flip routeStatus from 'planned' to 'active' for team-mirror, organization-summary, review-queue entries
  • test/folder59-c03-shell.test.ts -> update the guard-before-redirect direction check to cover the NEW legacy->canonical direction
追加するテスト/ゲート: Already covered by test/folder59-c03-shell.test.ts lines 151-170: confirms resolveWorkspaceAccess is called and both unauthenticated+forbidden states are handled before any redirect, and that no legacy host route appears as a primary nav href (line 93-108). When the content move happens, add an assertion that the legacy route file now contains redirect("/app/organization") etc. (reversed direction) with the same guard-first ordering.
G10 · RC60 非問題に格下げ 後回しでOK

Automated a11y tooling: zero jest-axe/axe-core/toHaveNoViolations coverage

小 (<半日)risk: low — existing hand-rolled ARIA assertioowner: haiku

検証(実コード): 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.

具体的な変更:
  • IF this is later prioritized: package.json devDependencies -> add vitest-axe@^4 (works with HTML strings, no jsdom needed). test/folder60-p18-a11y-smoke.test.tsx -> new file: import { axe } from 'vitest-axe'; renderToStaticMarkup(PolicyProfileCards + NoticeVersionsClient representative renderings); expect(await axe(html)).toHaveNoViolations(). No src/ changes required — the ARIA markup is already correct.
追加するテスト/ゲート: The existing hand-rolled assertions in test/homepage62-disclosure.test.tsx and test/folder58-prompt14-design-system.test.tsx already constitute a structural a11y gate. If vitest-axe is added later, the gate is: vitest runs test/folder60-p18-a11y-smoke.test.tsx in CI and toHaveNoViolations() must pass.