Enterprise audit remediation v2: CRITICAL/HIGH fixes + financial-services & mental-health verticals - #204
Conversation
- C1 (billing): resolve dispute customer from string charge / payment_intent; the typeof==='object' guard never matched a webhook payload, so both charge.dispute.created and .closed handlers were dead in production. - H4 (billing): team seat check used `if (limit)` — skipped enforcement at limit 0; now `if (limit != null)`, matching the /api/v1/members/invite path. - H6 (tests): extract pure framework pack-registry (no server-only import) so `playwright test --list` no longer crashes (0 tests -> 3370). - H8 (CI): run Jest + lint --max-warnings 25 on the PR quality-gate. - M4 (ops): schedule /api/automation/cron (entitlement-drift + automation triggers) hourly in vercel.json. - M8 (ops): data-retention cron round-robins via last_retention_at cursor (+ migration + best-effort stamp) instead of starving orgs past the first 250. - M9 (CI): pass STRIPE_PRICE_SCALE to deployment-gates env. - M10 (CI): post-deploy health/security curls now fail the job on outage (-Lf, --retry, follow apex->www). - canceled-normalization: write 'canceled' from customer.deleted / admin-sync / account-delete; reader tolerates legacy 'cancelled'. Verified: typecheck clean, 404 affected unit tests pass, playwright --list ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
H1: /app layout admits past_due *during* the grace window (isReadOnly===false) instead of hard-locking on day 0. past_due is the one Stripe-recoverable state (card retry); billing-004's allowlist had made the existing 3-day grace module unreachable. All other unpaid states stay redirected. H2: enforce read-only after grace at a chokepoint — requirePermission() now calls assertOrgCanWrite() for mutating permissions (EDIT_CONTROLS, UPLOAD/APPROVE/REJECT_EVIDENCE, RESOLVE_COMPLIANCE_BLOCK, GENERATE_CERTIFICATIONS, MANAGE_USERS, DRAFT_AI_POLICIES). Read/export permissions stay available so a read-only org can still view and extract its data. Previously only evidence + attestations enforced this. Verified: typecheck clean, 262 gated-action unit tests pass, no circular import, no write-permission checks in page render paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The posture-dashboard snapshot recomputed a heuristic-only score with no evaluator overlay, so it disagreed with the framework-evaluation page — and audit-package emitted BOTH numbers in the same auditor-facing export. - get-org-compliance-snapshot now overlays the persisted evaluator-aware status (org_control_evaluations.control_type='framework_control') onto its heuristic when an evaluation has run for a control. Reading the persisted verdict (not re-running ~253 evaluators live) keeps the dashboard cheap while making it report the same number as the framework page. - Deleted lib/compliance/unified-score.ts + test — a third, unused scoring formula with zero runtime importers; updated the stale builder-page comment. Verified: typecheck clean, 447 compliance/snapshot/audit/executive tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The org_medications table + RLS + administer route already existed, but the prominent "Add Medication" CTA had no handler (server component, no onClick) and there was no other path to add a medication. - Add createMedication server action (mirrors createIncident; inserts org_id — the table scopes by org_id, not organization_id — verifies the participant belongs to the org, validates route, writes a MEDICATION_CREATED audit event, revalidates the page). - MedicationChart gains a collapsible inline add-form bound to the action (reuses the existing inline-form pattern; no new modal primitive per the standing UI-surface guidance). Removed the dead header button. Verified: typecheck clean, 38 care unit tests pass, lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rols) The pack was installed but had zero evaluators, so its 20 controls only got the evidence heuristic. Adds the evaluator directory + registry wiring. - 7 DB-signal evaluators (AFS-003/004, CPS-001/002/004, AML-001, AFCA-002) reading org_policies cadence / org_registers (conflict_of_interest, BCP, complaint) / org_risks / org_audit_logs. All return manual-attestation or fail (never a false pass) when finance-tagged rows are absent. - 13 manual-attestation evaluators with control-specific messages. AFS-006 and AML-004 deliberately fell back from DB-signal: org_regulatory_notifications' CHECK constraint excludes ASIC/AUSTRAC and requires an incident_id FK, so reading it would surface unrelated NDIS rows (false signal). - FrameworkSlug union + register.ts wiring (registry key financial-services-au/<code>). Verified: typecheck clean, check:framework-packs OK (11 packs), evaluator suite 14/14 (107 tests) pass, 20 evaluators registered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "compliance graph" (node-wire) was built in memory on login, logged, and discarded — no backing store, no consumer. Now it's persisted and queryable. - Migration 20260624075: graph_nodes + graph_wires (FK to organizations ON DELETE CASCADE, node_type/wire_type CHECK, unique keys, org-scoped indexes). ENABLE+FORCE RLS, member-only SELECT, append-only RESTRICTIVE write policies (service-role writes bypass; no app.* GUC). - lib/compliance-graph.ts: rebuildOrgGraph() derives nodes/wires from tenant tables and upserts idempotently via the service-role org client; getComplianceGraph() reads via the member session client; initialize/repair now persist. Existing exports + return shapes preserved. - Registered graph_nodes/graph_wires in TENANT_TABLE_SCOPES. - GET /api/v1/compliance/graph: rate-limited, org-guarded, member-session read. - No new UI (the props-driven SVG component is left for separate sign-off). Verified: typecheck clean, compliance-graph + org-scoped tests 25/25, check-rls-current-setting 0 violations, RLS-contracts static scan includes both tables. DB-gated: live _audit_rls_status needs the migration applied to staging to fully validate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Full industry vertical anchored on the National Standards for Mental Health Services (NSMHS) 2010, end-to-end across the platform. Compliance/data layer: - framework-packs/mental-health-au.json: 10 standards / 14 controls (Standard 10 split 10.1-10.5); manifest rehashed (12 packs). - pack-registry entry + FrameworkSlug union. - 14 evaluators (4 DB-signal: org_incidents/org_registers/org_policies/ org_risks; 10 manual attestation) wired into register.ts. App / dashboard / onboarding: - industry-sidebar: 'mental_health' IndustryType + MENTAL_HEALTH_NAV (all hrefs resolve to existing /app routes), isCareIndustry + label. - command-center + 3 industry widgets (reuse existing endpoints) + labels. - WelcomeStep + industry-selector options; industry-packs, roadmap, IndustryFeatureHighlights entries. Marketing / SEO: - /mental-health-compliance page + content + opengraph-image (NSMHS-specific, enterprise-toned; no fabricated personas/metrics/backstory, Adelaide HQ). - Listed in IndustryVerticals / IndustriesContent / homepage Industries. - sitemap, llms.txt, llms-full.txt. - Updated industry-roadmaps test (10 industries); shortened marketing subheadline to satisfy the strict copy budget. Verified: typecheck clean, full Jest 5505 passed/0 failed, lint 0 errors, check:framework-packs 12 packs, marketing-enterprise-audit --strict clean, playwright --list 3375 tests, RLS GUC guard 0 violations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The framework-limit enforcement in org-frameworks.ts (getOrgFrameworkLimit +
existing-frameworks read) pairs the admin client with .eq('organization_id'),
which tripped the tenant-isolation ratchet (265 vs baseline 263) without a
justification. Both are single-org reads during provisioning sync with a
server-derived orgId — annotated with eslint-disable + rationale per the
rule's guidance. Ratchet back to baseline (passes).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds mental-health and financial-services compliance packs with many evaluators, persists the compliance graph (tables + RLS + rebuild/upsert), refactors pack registry, tightens deployment gates, and expands marketing, UI, billing, retention, permissions, and tests. ChangesCI / Deployment Gates
Compliance Graph Persistence & API
Framework Packs & Evaluators
Marketing, Discovery & Product
Billing, Retention, Permissions & Medication
Scoring & Snapshot Overlay
Reports
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d19d62c71a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // READ path: getComplianceGraph uses the member-facing session client | ||
| // (no service-role exposure); the org-membership SELECT RLS policy on | ||
| // graph_nodes/graph_wires gates row visibility. | ||
| const { nodes, wires } = await getComplianceGraph(orgId); |
There was a problem hiding this comment.
Backfill persisted graphs before serving only graph tables
For organizations that already existed before this migration, graph_nodes/graph_wires start empty and the existing-login path only calls validateComplianceGraph, not rebuildOrgGraph (repo-wide search shows rebuild is only called during new-org initialization and repair). In that context this new endpoint will return { nodes: [], wires: [] } indefinitely for otherwise populated orgs until some manual repair path runs, so the newly exposed graph API is blank for existing customers; add a migration/backfill or rebuild-on-empty fallback before relying solely on the persisted read.
Useful? React with 👍 / 👎.
| const { data: upsertedNodes, error: nodeError } = await admin | ||
| .from('graph_nodes') | ||
| .upsert(nodePayload, { | ||
| onConflict: 'organization_id,node_type,source_id', | ||
| }) | ||
| .select('id, node_type, source_id'); |
There was a problem hiding this comment.
Remove stale graph rows during rebuild
When source rows are deleted or unlinked after a graph has been persisted (for example a task/evidence row or member is removed), this rebuild only upserts the currently-derived rows and never deletes graph_nodes/graph_wires whose source rows are no longer present. Because the graph tables reference only their own persisted node ids, those obsolete nodes and edges remain visible through the new graph API even after subsequent rebuilds; prune rows not refreshed in this run or otherwise mark/delete stale edges.
Useful? React with 👍 / 👎.
🔒 Compliance Testing ResultsTest Date: Mon Jun 1 09:54:17 UTC 2026 GDPR ComplianceSOC2 ComplianceCompliance Reports: Available in the artifacts section below |
♿ Accessibility Test Results✅ PASSED - No critical accessibility issues found Tests Performed:
Artifacts: Download the accessibility reports from the "Artifacts" section for detailed results. |
Guard the 8 graph_nodes/graph_wires CREATE POLICY statements with DROP POLICY IF EXISTS so the migration is idempotent (the rest already uses IF NOT EXISTS). Applied to prod via MCP this session; this keeps a future `supabase db push` from erroring on "policy already exists". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🔒 Compliance Testing ResultsTest Date: Mon Jun 1 10:20:08 UTC 2026 GDPR ComplianceSOC2 ComplianceCompliance Reports: Available in the artifacts section below |
♿ Accessibility Test Results✅ PASSED - No critical accessibility issues found Tests Performed:
Artifacts: Download the accessibility reports from the "Artifacts" section for detailed results. |
The duplicate `npm test -- --ci` added to formaos-quality-gates' Core Build Gate flaked on an order-sensitive hook test (useFeatureUsage/hasHighUsage) under that job's worker scheduling — it passes locally (5505/0) and in the qa-pipeline run. Unit tests are ALREADY gated on PRs by the qa-pipeline "Unit & Integration Tests" job (npm run test:coverage, blocking on home-repo PRs), so the duplicate added no coverage. Keep the --max-warnings 25 lint hardening (the real H8 gap for this workflow). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🔒 Compliance Testing ResultsTest Date: Mon Jun 1 10:30:48 UTC 2026 GDPR ComplianceSOC2 ComplianceCompliance Reports: Available in the artifacts section below |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
lib/frameworks/pack-registry.ts (1)
1-14: 💤 Low valueModule is not fully client/edge-safe.
The comment (lines 6-9) claims this module is "client/edge-safe" and can be consumed "from any context," but
getPackFileForSluguses Node.js-only APIs (pathmodule andprocess.cwd()). While the other exports (PACK_REGISTRY, PACK_SLUGS, getFrameworkCodeForSlug, getFrameworkSlugForCode) are indeed safe, importing this module in a browser or edge runtime will fail ifgetPackFileForSlugis called.The current usage (e2e tests only import PACK_SLUGS) avoids the problem, but the comment is misleading.
Suggested improvements
Consider one of these options:
- Update the comment to clarify that only specific exports are client-safe:
/** * Pure framework-pack registry + slug/code lookups. * - * This module deliberately has **no** `server-only` / admin-client imports so - * it can be consumed from any context (tests, edge, client-safe code) without - * dragging in the Supabase admin client. `framework-installer.ts` re-exports - * everything here for back-compat and adds the server-only install routines. + * Most exports (PACK_REGISTRY, PACK_SLUGS, getFrameworkCodeForSlug, + * getFrameworkSlugForCode) are client/edge-safe. `getPackFileForSlug` uses + * Node.js APIs and is server-only. This module avoids importing Supabase admin + * client so test collection doesn't crash. `framework-installer.ts` re-exports + * everything here for back-compat and adds the server-only install routines.
- Move
getPackFileForSlugto framework-installer since it's server-only anyway.Also applies to: 75-79
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/frameworks/pack-registry.ts` around lines 1 - 14, The module comment is inaccurate because getPackFileForSlug uses Node-only APIs (path import and process.cwd()), so either remove Node-specific code from this file and move the server-only function getPackFileForSlug (and any use of path/process.cwd()) into the server-only framework-installer module, leaving only PACK_REGISTRY, PACK_SLUGS, getFrameworkCodeForSlug and getFrameworkSlugForCode here; and/or update the top comment to explicitly state which exports are client/edge-safe and that getPackFileForSlug is server-only so consumers won’t be surprised. Ensure any imports of path or process references are removed from this file when you move the function.lib/compliance-graph.ts (1)
803-805: 💤 Low valueConsider logging the rebuild result for observability consistency.
initializeComplianceGraphlogs a warning whenrebuildOrgGraphfails, butrepairComplianceGraphsilently discards the result. Adding a similar warning would help diagnose persistence issues after repairs.🔧 Suggested fix
// Re-derive and persist the graph so the repaired wires (newly-linked // tasks, role assignments) are reflected in graph_nodes/graph_wires. - await rebuildOrgGraph(organizationId, userId); + const persisted = await rebuildOrgGraph(organizationId, userId); + if (!persisted.success) { + graphLogger.warn('graph_persist_warning_after_repair', { + organizationId, + error: persisted.error, + }); + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/compliance-graph.ts` around lines 803 - 805, repairComplianceGraph currently calls rebuildOrgGraph but ignores its result; mirror initializeComplianceGraph by checking the rebuildOrgGraph result and logging a warning if it fails (or catching and logging thrown errors). Update repairComplianceGraph to inspect the return value from rebuildOrgGraph(organizationId, userId) and emit a warning (including the returned result or caught error details) using the same logger used elsewhere so persistence failures are observable, matching the behavior in initializeComplianceGraph.app/(marketing)/mental-health-compliance/MentalHealthComplianceContent.tsx (1)
890-931: ⚡ Quick winAlign FAQ schema with displayed FAQ content.
The FAQ content displayed here differs from the
faqSchemainpage.tsx:
- Schema has 5 questions; display has 7
- Question 4 wording differs ("worker screening for clinical staff" vs "state-specific worker screening")
- Two questions appear only in display, not in schema
For optimal SEO, the FAQ structured data should mirror the visible FAQ section. Search engines expect consistency between schema markup and user-visible content.
♻️ Recommended fix
Update the
faqSchemainpage.tsxto include all 7 FAQ items with exact question wording from this component, or vice versa—ensure both sources are in sync.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/`(marketing)/mental-health-compliance/MentalHealthComplianceContent.tsx around lines 890 - 931, The FAQ structured data is out of sync with the visible FAQs in this component; update the source of truth so they match exactly. Locate the faqSchema definition referenced in page.tsx and either (A) replace its list with the seven FAQ entries and exact question/answer strings used in the IndustryFAQ instantiation here (questions like "How does FormaOS handle state-specific worker screening?" and the two additional items shown), or (B) change the IndustryFAQ props to match the existing faqSchema—ensuring identical wording and the same count (7) so the schema and visible content are exact matches for SEO; keep the unique identifiers faqSchema and IndustryFAQ to find the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/`(marketing)/mental-health-compliance/page.tsx:
- Around line 81-88: The page metadata in
app/(marketing)/mental-health-compliance/page.tsx is overriding the generated
route image by hardcoding openGraph.images and twitter.images to
`${siteUrl}/og-image.png`; remove the explicit openGraph.images and
twitter.images fields from the exported metadata (or change their URL to
`${siteUrl}/mental-health-compliance/opengraph-image`) so Next.js can use the
generated app/(marketing)/mental-health-compliance/opengraph-image.tsx; locate
the metadata object in page.tsx and update the openGraph and twitter properties
accordingly, keeping the siteUrl variable usage consistent.
In `@app/app/actions/care-operations.ts`:
- Around line 425-440: createMedication currently skips the write-permission
check and can create medications during an org's read-only grace period; update
createMedication to call requirePermission (or at minimum assertOrgCanWrite)
using the resolved organization id/membership before performing any DB writes so
the grace-period/readonly enforcement is applied (locate createMedication in
app/app/actions/care-operations.ts and add the same
requirePermission/assertOrgCanWrite invocation pattern used elsewhere in this
file to validate the org can write prior to the insert).
In `@components/onboarding/IndustryFeatureHighlights.tsx`:
- Around line 99-119: In IndustryFeatureHighlights, update the route values for
the mental-health related feature cards so they match the sidebar canonical path
'/app/participants' (currently set to '/app/patients'); locate the objects with
title 'Consumer Management' and 'Incident & Restrictive Practice Tracking' (and
any other mental-health card entries) and change their route property to
'/app/participants' to ensure onboarding links align with the mental-health
navigation.
In `@lib/compliance/evaluators/financial-services-au/_shared.ts`:
- Around line 460-491: The current status calculation can yield 'pass' even when
there are stale elevated risks (gaps contains 'stale_elevated_risks'); change
the logic that sets status (the variable named status computed from overall) to
gate a 'pass' on both overall >= 0.9 AND no stale elevated risks (i.e.,
elevated.length === elevatedFresh); if elevated.length > elevatedFresh then
downgrade what would have been 'pass' to at least 'partial' (keep the existing
thresholds otherwise). Update the computation near the return that builds
controlCode, status, gaps, confidence, and reason so status reflects this
additional elevated-risk check.
In `@tests/compliance/reports/gdpr-compliance-report.json`:
- Around line 4-6: The committed GDPR baseline was overwritten with
environment-outage output (empty arrays for "dataProtection", "userRights",
"consent") instead of real evaluation results; update the report-generation flow
so that when an environment/connection error occurs it fails the generation and
does not write/update gdpr-compliance-report.json, and instead writes a separate
diagnostics/outage file or throws a non-zero error. Locate the code that
populates those keys ("dataProtection", "userRights", "consent") and add
explicit error-checking for environment errors (capture the exception or check
the error flag), prevent writing the canonical report if any control results are
missing due to outage, and write diagnostics with details (timestamp, error
message, affected controls) to a separate file or log for debugging.
---
Nitpick comments:
In `@app/`(marketing)/mental-health-compliance/MentalHealthComplianceContent.tsx:
- Around line 890-931: The FAQ structured data is out of sync with the visible
FAQs in this component; update the source of truth so they match exactly. Locate
the faqSchema definition referenced in page.tsx and either (A) replace its list
with the seven FAQ entries and exact question/answer strings used in the
IndustryFAQ instantiation here (questions like "How does FormaOS handle
state-specific worker screening?" and the two additional items shown), or (B)
change the IndustryFAQ props to match the existing faqSchema—ensuring identical
wording and the same count (7) so the schema and visible content are exact
matches for SEO; keep the unique identifiers faqSchema and IndustryFAQ to find
the code.
In `@lib/compliance-graph.ts`:
- Around line 803-805: repairComplianceGraph currently calls rebuildOrgGraph but
ignores its result; mirror initializeComplianceGraph by checking the
rebuildOrgGraph result and logging a warning if it fails (or catching and
logging thrown errors). Update repairComplianceGraph to inspect the return value
from rebuildOrgGraph(organizationId, userId) and emit a warning (including the
returned result or caught error details) using the same logger used elsewhere so
persistence failures are observable, matching the behavior in
initializeComplianceGraph.
In `@lib/frameworks/pack-registry.ts`:
- Around line 1-14: The module comment is inaccurate because getPackFileForSlug
uses Node-only APIs (path import and process.cwd()), so either remove
Node-specific code from this file and move the server-only function
getPackFileForSlug (and any use of path/process.cwd()) into the server-only
framework-installer module, leaving only PACK_REGISTRY, PACK_SLUGS,
getFrameworkCodeForSlug and getFrameworkSlugForCode here; and/or update the top
comment to explicitly state which exports are client/edge-safe and that
getPackFileForSlug is server-only so consumers won’t be surprised. Ensure any
imports of path or process references are removed from this file when you move
the function.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 02824f0c-d553-4792-9688-5dd7ce93139e
📒 Files selected for processing (90)
.github/workflows/deployment-gates.yml.github/workflows/formaos-quality-gates.yml__tests__/lib/compliance-graph.test.ts__tests__/lib/compliance/unified-score.test.ts__tests__/lib/onboarding/industry-roadmaps.test.tsapp/(marketing)/components/homepage/Industries.tsxapp/(marketing)/industries/IndustriesContent.tsxapp/(marketing)/industries/components/IndustryVerticals.tsxapp/(marketing)/mental-health-compliance/MentalHealthComplianceContent.tsxapp/(marketing)/mental-health-compliance/opengraph-image.tsxapp/(marketing)/mental-health-compliance/page.tsxapp/api/billing/webhook/route.tsapp/api/cron/data-retention/route.tsapp/api/v1/account/delete/route.tsapp/api/v1/compliance/graph/route.tsapp/app/actions/care-operations.tsapp/app/actions/rbac.tsapp/app/actions/team.tsapp/app/admin/actions.tsapp/app/dashboard/builder/page.tsxapp/app/layout.tsxapp/app/participants/[id]/medications/page.tsxapp/llms-full.txt/route.tsapp/llms.txt/route.tsapp/sitemap.tscomponents/care/medication-chart.tsxcomponents/dashboard/command-center.tsxcomponents/dashboard/industry-labels.tscomponents/dashboard/industry-selector.tsxcomponents/dashboard/industry-widgets.tsxcomponents/onboarding/IndustryFeatureHighlights.tsxcomponents/onboarding/steps/WelcomeStep.tsxe2e/full-platform-matrix.spec.tsframework-packs/manifest.jsonframework-packs/mental-health-au.jsonlib/compliance-graph.tslib/compliance/evaluators/financial-services-au/AFCA-001.tslib/compliance/evaluators/financial-services-au/AFCA-002.tslib/compliance/evaluators/financial-services-au/AFS-001.tslib/compliance/evaluators/financial-services-au/AFS-002.tslib/compliance/evaluators/financial-services-au/AFS-003.tslib/compliance/evaluators/financial-services-au/AFS-004.tslib/compliance/evaluators/financial-services-au/AFS-005.tslib/compliance/evaluators/financial-services-au/AFS-006.tslib/compliance/evaluators/financial-services-au/AFS-007.tslib/compliance/evaluators/financial-services-au/AFS-008.tslib/compliance/evaluators/financial-services-au/AML-001.tslib/compliance/evaluators/financial-services-au/AML-002.tslib/compliance/evaluators/financial-services-au/AML-003.tslib/compliance/evaluators/financial-services-au/AML-004.tslib/compliance/evaluators/financial-services-au/AML-005.tslib/compliance/evaluators/financial-services-au/CPS-001.tslib/compliance/evaluators/financial-services-au/CPS-002.tslib/compliance/evaluators/financial-services-au/CPS-003.tslib/compliance/evaluators/financial-services-au/CPS-004.tslib/compliance/evaluators/financial-services-au/CPS-005.tslib/compliance/evaluators/financial-services-au/_shared.tslib/compliance/evaluators/mental-health-au/MHS-1.tslib/compliance/evaluators/mental-health-au/MHS-10.1.tslib/compliance/evaluators/mental-health-au/MHS-10.2.tslib/compliance/evaluators/mental-health-au/MHS-10.3.tslib/compliance/evaluators/mental-health-au/MHS-10.4.tslib/compliance/evaluators/mental-health-au/MHS-10.5.tslib/compliance/evaluators/mental-health-au/MHS-2.tslib/compliance/evaluators/mental-health-au/MHS-3.tslib/compliance/evaluators/mental-health-au/MHS-4.tslib/compliance/evaluators/mental-health-au/MHS-5.tslib/compliance/evaluators/mental-health-au/MHS-6.tslib/compliance/evaluators/mental-health-au/MHS-7.tslib/compliance/evaluators/mental-health-au/MHS-8.tslib/compliance/evaluators/mental-health-au/MHS-9.tslib/compliance/evaluators/mental-health-au/_shared.tslib/compliance/evaluators/register.tslib/compliance/evaluators/types.tslib/compliance/get-org-compliance-snapshot.tslib/compliance/unified-score.tslib/data-governance/retention.tslib/frameworks/framework-installer.tslib/frameworks/org-frameworks.tslib/frameworks/pack-registry.tslib/industry-packs.tslib/navigation/industry-sidebar.tslib/onboarding/industry-roadmaps.tslib/supabase/org-scoped.tslib/system-state/server.tssupabase/migrations/20260624074_audit_2026_06_01_org_last_retention_at.sqlsupabase/migrations/20260624075_audit_2026_06_01_compliance_graph_persistence.sqltests/compliance/reports/gdpr-compliance-report.jsontests/compliance/reports/soc2-compliance-report.jsonvercel.json
💤 Files with no reviewable changes (2)
- lib/compliance/unified-score.ts
- tests/lib/compliance/unified-score.test.ts
| images: [ | ||
| { | ||
| url: `${siteUrl}/og-image.png`, | ||
| width: 1200, | ||
| height: 630, | ||
| alt: 'Mental Health Compliance Software by FormaOS', | ||
| }, | ||
| ], |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if opengraph-image.tsx exists and confirm the correct OG path
fd -t f 'opengraph-image\.tsx' app/\(marketing\)/mental-health-compliance/Repository: ejay-dev/FormaOS
Length of output: 121
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the relevant metadata section in the page
sed -n '1,140p' app/'(marketing)'/mental-health-compliance/page.tsx | nl -ba | sed -n '40,120p'
# Confirm where siteUrl and openGraph images are used (quick grep)
rg -n "openGraph\\.images|twitter\\.images|og-image\\.png" app/'(marketing)'/mental-health-compliance/page.tsx
# Check whether a global og-image.png exists (public folder)
ls -ლა public 2>/dev/null | head -n 50 || true
test -f public/'og-image.png' && echo "public/og-image.png exists" || echo "public/og-image.png missing"Repository: ejay-dev/FormaOS
Length of output: 103
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app/(marketing)/mental-health-compliance/page.tsx"
# Print the metadata block area (around the reported snippet)
awk 'NR>=60 && NR<=110 {printf "%5d:%s\n", NR, $0}' "$FILE"
# Show exact occurrences for the image URLs
rg -n "openGraph\.images|twitter\.images|og-image\.png" "$FILE" || true
# Check whether a global og-image exists in public/
if [ -f "public/og-image.png" ]; then
echo "public/og-image.png exists"
else
echo "public/og-image.png missing"
fiRepository: ejay-dev/FormaOS
Length of output: 2056
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app/(marketing)/mental-health-compliance/opengraph-image.tsx"
echo "---- file info ----"
wc -l "$FILE"
echo "---- top of file ----"
sed -n '1,200p' "$FILE" | cat -nRepository: ejay-dev/FormaOS
Length of output: 4355
🌐 Web query:
Next.js 15 opengraph-image.tsx precedence when metadata openGraph.images is explicitly set
💡 Result:
In Next.js 15, when you define an opengraph-image.tsx (or other metadata file conventions) in a route segment, Next.js automatically treats it as the source for that segment's Open Graph image [1][2]. If you explicitly define openGraph.images within the metadata object of your page.tsx or layout.tsx, this metadata configuration will take precedence over the automatically generated image from the file convention [3]. When images are provided in the metadata object, Next.js expects you to provide the full URL (or absolute path relative to your metadataBase) [4][5], whereas opengraph-image.tsx handles the generation and URL construction for you automatically [1][2]. Key takeaways: 1. Precedence: Explicitly setting openGraph.images in your metadata object overrides the default behavior of the opengraph-image.tsx file convention [3]. 2. Manual vs. Automatic: If you want to use the dynamic capabilities of opengraph-image.tsx (e.g., ImageResponse), you should generally avoid defining openGraph.images in your metadata object for that same route segment, or ensure your manual configuration points to the desired image URL [6]. 3. MetadataBase: Note that metadataBase is used by Next.js to resolve relative URLs in metadata. There have been reported edge cases where metadataBase might behave inconsistently when mixing file conventions and manual metadata, so always ensure your URLs are correctly constructed if you choose to manually override the default file-based approach [7]. In summary, the metadata object is the "source of truth" for the final rendered HTML; the file-based convention is a convenience feature that Next.js uses to populate that metadata unless you manually override it [2][8].
Citations:
- 1: https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image
- 2: https://nextjs.org/docs/app/api-reference/file-conventions/metadata
- 3: https://nextjs.org/docs/app/getting-started/metadata-and-og-images
- 4: https://dev.to/joseph_anady_214bacedf939/cross-stack-seo-implementation-framework-jlm
- 5: https://medium.com/@j.hariharan005/how-to-handle-seo-in-next-js-15-a-complete-developer-guide-1e2e2a9f21af
- 6: https://medium.com/@uyiosazeeirvin/complete-guide-to-dynamic-og-images-in-next-js-15-5f69fd583dbe
- 7: metadataBase is ignored for OpenGraph images when opengraph-image.tsx is used vercel/next.js#76408
- 8: https://supertokens.com/blog/nextjs-metadata
Fix Open Graph/Twitter OG image override for this route
app/(marketing)/mental-health-compliance/page.tsx hardcodes openGraph.images and twitter.images to ${siteUrl}/og-image.png (lines 81-88, 90-96), which overrides the route-specific app/(marketing)/mental-health-compliance/opengraph-image.tsx. Remove the explicit images fields to let Next.js use the generated image, or point them at ${siteUrl}/mental-health-compliance/opengraph-image.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/`(marketing)/mental-health-compliance/page.tsx around lines 81 - 88, The
page metadata in app/(marketing)/mental-health-compliance/page.tsx is overriding
the generated route image by hardcoding openGraph.images and twitter.images to
`${siteUrl}/og-image.png`; remove the explicit openGraph.images and
twitter.images fields from the exported metadata (or change their URL to
`${siteUrl}/mental-health-compliance/opengraph-image`) so Next.js can use the
generated app/(marketing)/mental-health-compliance/opengraph-image.tsx; locate
the metadata object in page.tsx and update the openGraph and twitter properties
accordingly, keeping the siteUrl variable usage consistent.
| export async function createMedication(formData: FormData) { | ||
| try { | ||
| const supabase = await createSupabaseServerClient(); | ||
|
|
||
| const { | ||
| data: { user }, | ||
| } = await supabase.auth.getUser(); | ||
| if (!user) redirect('/auth/signin'); | ||
|
|
||
| const { data: membership } = await supabase | ||
| .from('org_members') | ||
| .select('organization_id') | ||
| .eq('user_id', user.id) | ||
| .maybeSingle(); | ||
|
|
||
| if (!membership) throw new Error('No organization found'); |
There was a problem hiding this comment.
CRITICAL: Grace period write enforcement bypassed.
createMedication doesn't call requirePermission or assertOrgCanWrite, so it allows medication creation even when the org is in read-only state after the 3-day grace period. This contradicts the PR's core goal (H2) of enforcing read-only via assertOrgCanWrite in requirePermission.
Add a permission check to block writes during grace period:
🔒 Proposed fix to enforce grace period
export async function createMedication(formData: FormData) {
try {
const supabase = await createSupabaseServerClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) redirect('/auth/signin');
const { data: membership } = await supabase
.from('org_members')
.select('organization_id')
.eq('user_id', user.id)
.maybeSingle();
if (!membership) throw new Error('No organization found');
+
+ // Enforce grace period: block writes when org is read-only
+ await requirePermission('UPLOAD_EVIDENCE');
const participantId = (formData.get('participant_id') as string) || '';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function createMedication(formData: FormData) { | |
| try { | |
| const supabase = await createSupabaseServerClient(); | |
| const { | |
| data: { user }, | |
| } = await supabase.auth.getUser(); | |
| if (!user) redirect('/auth/signin'); | |
| const { data: membership } = await supabase | |
| .from('org_members') | |
| .select('organization_id') | |
| .eq('user_id', user.id) | |
| .maybeSingle(); | |
| if (!membership) throw new Error('No organization found'); | |
| export async function createMedication(formData: FormData) { | |
| try { | |
| const supabase = await createSupabaseServerClient(); | |
| const { | |
| data: { user }, | |
| } = await supabase.auth.getUser(); | |
| if (!user) redirect('/auth/signin'); | |
| const { data: membership } = await supabase | |
| .from('org_members') | |
| .select('organization_id') | |
| .eq('user_id', user.id) | |
| .maybeSingle(); | |
| if (!membership) throw new Error('No organization found'); | |
| // Enforce grace period: block writes when org is read-only | |
| await requirePermission('UPLOAD_EVIDENCE'); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/app/actions/care-operations.ts` around lines 425 - 440, createMedication
currently skips the write-permission check and can create medications during an
org's read-only grace period; update createMedication to call requirePermission
(or at minimum assertOrgCanWrite) using the resolved organization id/membership
before performing any DB writes so the grace-period/readonly enforcement is
applied (locate createMedication in app/app/actions/care-operations.ts and add
the same requirePermission/assertOrgCanWrite invocation pattern used elsewhere
in this file to validate the org can write prior to the insert).
| { | ||
| icon: Users, | ||
| title: 'Consumer Management', | ||
| description: | ||
| 'Track consumers with care status, risk levels, and safety flags. Every interaction becomes compliance evidence.', | ||
| route: '/app/patients', | ||
| }, | ||
| { | ||
| icon: Calendar, | ||
| title: 'Service Delivery Scheduling', | ||
| description: | ||
| 'Schedule service delivery with automatic audit trails. No double entry—session logs become compliance evidence.', | ||
| route: '/app/visits', | ||
| }, | ||
| { | ||
| icon: Activity, | ||
| title: 'Incident & Restrictive Practice Tracking', | ||
| description: | ||
| 'Log incidents and restrictive practices with severity classification, authorisation, and review. Reporting-ready.', | ||
| route: '/app/patients', | ||
| }, |
There was a problem hiding this comment.
Align mental-health onboarding routes with the sidebar canonical route.
The new mental-health feature cards point to /app/patients, while the mental-health navigation uses /app/participants. This inconsistency can send users to the wrong surface from onboarding.
Suggested fix
{
icon: Users,
title: 'Consumer Management',
@@
- route: '/app/patients',
+ route: '/app/participants',
},
@@
{
icon: Activity,
title: 'Incident & Restrictive Practice Tracking',
@@
- route: '/app/patients',
+ route: '/app/participants',
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| { | |
| icon: Users, | |
| title: 'Consumer Management', | |
| description: | |
| 'Track consumers with care status, risk levels, and safety flags. Every interaction becomes compliance evidence.', | |
| route: '/app/patients', | |
| }, | |
| { | |
| icon: Calendar, | |
| title: 'Service Delivery Scheduling', | |
| description: | |
| 'Schedule service delivery with automatic audit trails. No double entry—session logs become compliance evidence.', | |
| route: '/app/visits', | |
| }, | |
| { | |
| icon: Activity, | |
| title: 'Incident & Restrictive Practice Tracking', | |
| description: | |
| 'Log incidents and restrictive practices with severity classification, authorisation, and review. Reporting-ready.', | |
| route: '/app/patients', | |
| }, | |
| { | |
| icon: Users, | |
| title: 'Consumer Management', | |
| description: | |
| 'Track consumers with care status, risk levels, and safety flags. Every interaction becomes compliance evidence.', | |
| route: '/app/participants', | |
| }, | |
| { | |
| icon: Calendar, | |
| title: 'Service Delivery Scheduling', | |
| description: | |
| 'Schedule service delivery with automatic audit trails. No double entry—session logs become compliance evidence.', | |
| route: '/app/visits', | |
| }, | |
| { | |
| icon: Activity, | |
| title: 'Incident & Restrictive Practice Tracking', | |
| description: | |
| 'Log incidents and restrictive practices with severity classification, authorisation, and review. Reporting-ready.', | |
| route: '/app/participants', | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/onboarding/IndustryFeatureHighlights.tsx` around lines 99 - 119,
In IndustryFeatureHighlights, update the route values for the mental-health
related feature cards so they match the sidebar canonical path
'/app/participants' (currently set to '/app/patients'); locate the objects with
title 'Consumer Management' and 'Incident & Restrictive Practice Tracking' (and
any other mental-health card entries) and change their route property to
'/app/participants' to ensure onboarding links align with the mental-health
navigation.
| const gaps: ControlGap[] = []; | ||
| if (elevated.length > elevatedFresh) { | ||
| gaps.push({ | ||
| code: 'stale_elevated_risks', | ||
| message: `${elevated.length - elevatedFresh}/${elevated.length} elevated risks not reviewed within 90 days.`, | ||
| severity: 'high', | ||
| }); | ||
| } | ||
| if (routine.length > routineFresh) { | ||
| gaps.push({ | ||
| code: 'stale_routine_risks', | ||
| message: `${routine.length - routineFresh}/${routine.length} routine risks not reviewed within 12 months.`, | ||
| severity: 'medium', | ||
| }); | ||
| } | ||
|
|
||
| const status: ControlResult['status'] = | ||
| overall >= 0.9 ? 'pass' : overall >= 0.5 ? 'partial' : 'fail'; | ||
|
|
||
| return { | ||
| controlCode, | ||
| status, | ||
| evidenceRefs: risks.slice(0, EVIDENCE_CAP).map((r) => ({ | ||
| source: 'org_risks', | ||
| ref: r.id, | ||
| capturedAt: r.updated_at ?? r.created_at ?? undefined, | ||
| })), | ||
| gaps, | ||
| confidence: round2(0.5 + 0.4 * overall), | ||
| reason: `elevated ${elevatedFresh}/${elevated.length} fresh (90d); routine ${routineFresh}/${routine.length} fresh (365d).`, | ||
| evaluatedAt, | ||
| }; |
There was a problem hiding this comment.
pass can be returned alongside a high-severity elevated-risk gap.
With overall >= 0.9, a single stale critical/high risk (e.g. 1 of 10 elevated stale → elevatedRatio 0.9, routineRatio 1 → overall 0.95) yields status: 'pass' while gaps still contains the high-severity stale_elevated_risks entry. A clean pass that ships an unreviewed elevated risk is misleading for CPS 230. Consider gating pass on zero stale elevated risks.
♻️ Suggested gating
- const status: ControlResult['status'] =
- overall >= 0.9 ? 'pass' : overall >= 0.5 ? 'partial' : 'fail';
+ const elevatedAllFresh = elevated.length === elevatedFresh;
+ const status: ControlResult['status'] =
+ overall >= 0.9 && elevatedAllFresh
+ ? 'pass'
+ : overall >= 0.5
+ ? 'partial'
+ : 'fail';🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/compliance/evaluators/financial-services-au/_shared.ts` around lines 460
- 491, The current status calculation can yield 'pass' even when there are stale
elevated risks (gaps contains 'stale_elevated_risks'); change the logic that
sets status (the variable named status computed from overall) to gate a 'pass'
on both overall >= 0.9 AND no stale elevated risks (i.e., elevated.length ===
elevatedFresh); if elevated.length > elevatedFresh then downgrade what would
have been 'pass' to at least 'partial' (keep the existing thresholds otherwise).
Update the computation near the return that builds controlCode, status, gaps,
confidence, and reason so status reflects this additional elevated-risk check.
| "dataProtection": [], | ||
| "userRights": [], | ||
| "consent": [], |
There was a problem hiding this comment.
Do not replace baseline compliance evidence with environment-outage output.
This report now encodes localhost unavailability (empty control results + environment error) instead of GDPR evaluation outcomes. That makes the committed artifact non-actionable for audit comparison and can hide actual compliance regressions. Prefer failing report generation in this state (or storing outage diagnostics separately) rather than overwriting the canonical report JSON.
Also applies to: 11-13, 17-17, 21-22
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/compliance/reports/gdpr-compliance-report.json` around lines 4 - 6, The
committed GDPR baseline was overwritten with environment-outage output (empty
arrays for "dataProtection", "userRights", "consent") instead of real evaluation
results; update the report-generation flow so that when an
environment/connection error occurs it fails the generation and does not
write/update gdpr-compliance-report.json, and instead writes a separate
diagnostics/outage file or throws a non-zero error. Locate the code that
populates those keys ("dataProtection", "userRights", "consent") and add
explicit error-checking for environment errors (capture the exception or check
the error flag), prevent writing the canonical report if any control results are
missing due to outage, and write diagnostics with details (timestamp, error
message, affected controls) to a separate file or log for debugging.
♿ Accessibility Test Results✅ PASSED - No critical accessibility issues found Tests Performed:
Artifacts: Download the accessibility reports from the "Artifacts" section for detailed results. |
Summary
Acts on a full enterprise audit of FormaOS: ships the CRITICAL + HIGH fixes, three MEDIUM correctness/ops fixes, and two greenfield builds the owner requested (financial-services evaluator suite + a brand-new Mental Health Services / NSMHS industry vertical).
Stacked on
fix/enterprise-audit-remediationso the diff is exactly the 8 commits below. Can be retargeted tomainif preferred (that view also carries 3 inherited base-branch commits).What's included
dispute.chargeis a string in webhooks, so thetypeof === 'object'guard never matched. Now resolves the customer via charge / payment-intent retrieval; chargebacks correctly pause entitlements.past_dueis admitted during the 3-day grace window (was hard-locked on day 0, making the grace module unreachable); read-only enforced after grace via anassertOrgCanWritechokepoint inrequirePermission.unified-score.ts.if (limit)→if (limit != null)) — was granting unlimited seats at limit 0; aligned with the API path.createMedicationaction + inline form.playwright test --listcrash fixed (0 → 3375 tests) by extracting a purepack-registrymodule out of theserver-onlychain.--max-warnings;STRIPE_PRICE_SCALEwired into deploy env; post-deploy health checks now fail on outage.graph_nodes/graph_wiresmigration (FORCE RLS, append-only, service-role writes / member reads) +GET /api/v1/compliance/graph.vercel.json; data-retention cron round-robins via alast_retention_atcursor (no longer starves orgs past the first 250).financial-services-auevaluator suite — 20 controls (7 DB-signal, 13 honest manual attestation; never false-passes).canceled/cancelledstatus normalization; annotated two legitimate single-org admin reads to keep the tenant-isolation ratchet flat.Verification (all green, this session)
next build— success (exit 0)check:framework-packs12 packs ·check:app-links382/0 broken ·check:admin-navOK ·check:security-baselinepass=8/0/0check-rls-current-setting0 violations · RLS contracts static scan PASS (226 tables) · tenant-isolation ratchet passes (263 baseline)playwright test --list3375 tests (was crashing at 0)Needs the maintainer (can't be done from CI)
20260624074(retention cursor) and20260624075(graph tables). Both written deploy-safe; the graph table's live_audit_rls_statuscan only be validated post-apply.E2E_GATE_STRICT=true(makes the E2E gate blocking) andSTRIPE_PRICE_SCALE.past_duegrace flow (H1/H2), and the new MH vertical end-to-end.Deliberately out of scope (flagged, not done)
Wiring the props-only
ComplianceGraphSVG to the new API (new UI surface — needs sign-off); a clinical-domain review of the NSMHS evaluator predicates (noted inregister.ts).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Bug Fixes