audit: deep platform audit, daily workflow, compliance flags, timestamped docs #3862
7 changed files+149−48
ModifiedMASTER-AUDIT.md+17−7View fileUnifiedSplit
@@ -406,13 +406,23 @@ The CLAUDE.md 80-90% ahead rule — current score:
406406
407407---
408408
409## SECTION I: NEXT SESSION PRIORITIES *(set 2026-05-29)*
410
4111. Set `ANTHROPIC_API_KEY` in Vercel — activates Marco instantly
4122. Wire CourtListener citation lookup API into Marco engine
4133. Update privacy policy for NZ Privacy Amendment Act IPP 3A compliance
4144. Make onboarding consent explicit about AI use (ABA Opinion 512)
4155. Start EU AI Act risk classification process
409## SECTION I: NEXT SESSION PRIORITIES *(updated 2026-05-29)*
410
411**Completed this session:**
412- ✅ CourtListener v4 block-of-text citation lookup wired into Marco engine
413- ✅ Privacy policy updated with IPP 3A compliance (NZ Privacy Amendment Act 2025)
414- ✅ Onboarding consent updated with explicit AI data processing disclosure (ABA Opinion 512)
415- ✅ `timeZone` added to User schema; profile route now persists all three fields
416- ✅ Profile route TODO removed — `practiceArea`, `jurisdiction`, `timeZone` all persist
417
418**Still to do:**
4191. Set `ANTHROPIC_API_KEY` in Vercel — activates Marco engine instantly (Craig's action)
4202. Set `COURTLISTENER_API_KEY` in Vercel — unlocks higher rate limits on citation verification
4213. EU AI Act risk classification — 2 August 2026 deadline (65 days away)
4224. Australian Privacy Act compliance analysis — 1 July 2026 (33 days away)
4235. 50-state IOLTA compliance analysis before trust accounting module goes live
4246. Technology E&O and Cyber Liability insurance — required before first paying customer
4257. Run `prisma migrate deploy` against production database (new `timeZone` field)
416426
417427---
418428
Modifiedapp/(marketing)/privacy/page.tsx+3−3View fileUnifiedSplit
@@ -55,9 +55,9 @@ const sections = [
5555 "If you are a California resident, you have additional rights under the California Consumer Privacy Act (CCPA) as amended by the CPRA:\n\nRight to Know: You may request the categories and specific pieces of personal information we have collected, the sources, the business purpose, and the categories of third parties with whom we share it.\n\nRight to Delete: You may request deletion of your personal information, subject to certain exceptions.\n\nRight to Opt-Out of Sale: We do not sell your personal information. We do not share your personal information for cross-context behavioural advertising.\n\nRight to Non-Discrimination: We will not discriminate against you for exercising your CCPA rights.\n\nFor the purposes of the CCPA, the categories of personal information we collect include: identifiers (name, email), professional information, commercial information (subscription data), internet activity (usage data), and inferences (research patterns). We collect this information for the business purposes described in Section 4. To submit a CCPA request, contact privacy@marcoreid.com with the subject line \"CCPA Request\".",
5656 },
5757 {
58 title: "10. New Zealand Privacy Act 2020",
58 title: "10. New Zealand Privacy Act 2020 and 2025 Amendment",
5959 content:
60 "As a New Zealand company, we comply with the Privacy Act 2020 and the Information Privacy Principles (IPPs). You have the right to: (a) access your personal information (IPP 6); (b) request correction of your personal information (IPP 7); and (c) complain to the Privacy Commissioner if you believe we have breached the Privacy Act. We collect personal information directly from you (IPP 2), for lawful purposes connected to our functions (IPP 1), by lawful and fair means (IPP 4). We take reasonable steps to ensure data quality (IPP 8) and security (IPP 5). We do not use your information for purposes other than those for which it was collected without your consent (IPP 10), and we do not disclose it except as described in this policy (IPP 11).",
60 "As a New Zealand company, we comply with the Privacy Act 2020, the Information Privacy Principles (IPPs), and the Privacy Amendment Act 2025.\n\nPrivacy Act 2020 compliance: You have the right to: (a) access your personal information (IPP 6); (b) request correction of your personal information (IPP 7); and (c) complain to the Privacy Commissioner if you believe we have breached the Privacy Act. We collect personal information directly from you (IPP 2), for lawful purposes connected to our functions (IPP 1), by lawful and fair means (IPP 4). We take reasonable steps to ensure data quality (IPP 8) and security (IPP 5). We do not use your information for purposes other than those for which it was collected without your consent (IPP 10), and we do not disclose it except as described in this policy (IPP 11).\n\nPrivacy Amendment Act 2025 — IPP 3A (effective 1 May 2026): Where we collect your personal information indirectly (that is, not directly from you), we are now required to provide you with information about that processing. In the context of the Marco Reid platform, this applies to the following indirect processing: (a) Research queries you submit are transmitted to Anthropic (Claude API) for AI processing. Anthropic processes your query text to generate research output. Anthropic's data policies govern their handling of API data. (b) Voice recordings you submit are transmitted to OpenAI (Whisper API) for transcription. OpenAI processes the audio to return transcribed text. (c) Legal citations extracted from AI responses are verified against public databases including CourtListener, GovInfo, IRS.gov, Cornell LII, NZLII, and AustLII. Only citation metadata is transmitted — no personal data is sent to these sources. We have obtained your specific consent to this indirect processing via the platform acknowledgment at account creation, as required by IPP 3A. You may request further information about any indirect processing by contacting privacy@marcoreid.com.",
6161 },
6262 {
6363 title: "11. Data security",
@@ -103,7 +103,7 @@ export default function PrivacyPage() {
103103 <div className="mx-auto max-w-3xl">
104104 <Reveal>
105105 <p className="text-sm text-navy-400">
106 Last updated: April 2026
106 Last updated: May 2026
107107 </p>
108108 <p className="mt-6 text-lg text-navy-500">
109109 Reid & Associates Ltd (“Marco Reid”,
Modifiedapp/api/me/profile/route.ts+7−17View fileUnifiedSplit
@@ -4,12 +4,7 @@ import { getUserId } from "@/lib/session";
44
55// POST /api/me/profile
66// Updates the authenticated user's personal profile fields.
7//
8// Note: timeZone, practiceArea, and jurisdiction are not yet on the User
9// model in prisma/schema.prisma. Once added (e.g. as String? fields), they
10// will persist automatically. For now we accept them, log them server-side,
11// and only persist `name`. Email is intentionally read-only here — change
12// flow will live behind a verification endpoint.
7// Email is intentionally read-only here — changes live behind a verification endpoint.
138export async function POST(req: NextRequest) {
149 const userId = await getUserId();
1510 if (!userId) {
@@ -52,26 +47,21 @@ export async function POST(req: NextRequest) {
5247 where: { id: userId },
5348 data: {
5449 ...(typeof name === "string" ? { name: name.trim() || null } : {}),
55 // TODO: add `practiceArea`, `jurisdiction`, `timeZone` to User model
56 // and persist here.
50 ...(typeof practiceArea === "string" ? { practiceArea: practiceArea.trim() || null } : {}),
51 ...(typeof jurisdiction === "string" ? { jurisdiction: jurisdiction.trim() || null } : {}),
52 ...(typeof timeZone === "string" ? { timeZone: timeZone.trim() || null } : {}),
5753 },
5854 select: {
5955 id: true,
6056 email: true,
6157 name: true,
6258 firmName: true,
59 practiceArea: true,
60 jurisdiction: true,
61 timeZone: true,
6362 },
6463 });
6564
66 if (practiceArea || jurisdiction || timeZone) {
67 console.log("[settings/profile] pending-schema fields", {
68 userId,
69 practiceArea,
70 jurisdiction,
71 timeZone,
72 });
73 }
74
7565 return NextResponse.json({ user });
7666 } catch {
7767 return NextResponse.json({ error: "Internal error" }, { status: 500 });
Modifiedlib/consent.ts+3−2View fileUnifiedSplit
@@ -3,7 +3,7 @@
33// create a new version.
44
55export const CURRENT_TOS_VERSION = "2026-04-20";
6export const CURRENT_PLATFORM_ACK_VERSION = "2026-04-20";
6export const CURRENT_PLATFORM_ACK_VERSION = "2026-05-29";
77
88// The platform acknowledgment shown at signup, in addition to the standard
99// Terms / Privacy / Acceptable Use agreement. Deliberately plain-language —
@@ -19,7 +19,8 @@ export const CURRENT_PLATFORM_ACK_VERSION = "2026-04-20";
1919export const PLATFORM_ACK_BULLETS: readonly string[] = [
2020 "Marco Reid is a technology platform. It is not a law firm, accounting firm, or licensed professional service provider.",
2121 "AI-generated output is informational only. It is not legal, tax, financial, or accounting advice, and it does not create a professional relationship.",
22 "I will not rely on AI output in any filing, return, transaction, or decision unless a licensed professional has reviewed and signed it off inside the platform.",
22 "I understand that queries I submit to Marco are processed by Anthropic's Claude API, and that voice recordings are processed by OpenAI's Whisper API, to generate AI Output. I consent to this processing as described in the Privacy Policy.",
23 "I will not rely on AI output in any filing, return, transaction, or decision unless a licensed professional has reviewed and signed it off inside the platform. I understand that all citations must be independently verified before use.",
2324 "Any dispute with Marco Reid is resolved by binding arbitration in Auckland, New Zealand under the Terms of Service, on an individual basis only — not as part of a class, consolidated, or representative action.",
2425 "None of the above limits non-waivable rights under the NZ Consumer Guarantees Act 1993, the Australian Consumer Law, or equivalent consumer protection law in my jurisdiction.",
2526];
Modifiedlib/marco/engine.ts+16−3View fileUnifiedSplit
@@ -1,6 +1,6 @@
11import Anthropic from "@anthropic-ai/sdk";
22import { prisma } from "@/lib/prisma";
3import { verifyCitation } from "./verify";
3import { verifyCitation, verifyBlockOfText } from "./verify";
44import {
55 MarcoRequest,
66 MarcoResponse,
@@ -336,9 +336,22 @@ export async function queryMarco(
336336 // 5. Extract citations from the response
337337 const rawCitations = extractCitations(answer);
338338
339 // 6. Verify EVERY citation against authoritative sources
339 // 6. Block-level verification via CourtListener v4 Eyecite API first.
340 // Send the entire response text — CourtListener finds and verifies every
341 // legal citation simultaneously. This is the Mata v. Avianca protection:
342 // citations not found in the database are flagged NOT_FOUND before display.
343 const blockResults = await verifyBlockOfText(answer);
344
345 // 7. For each extracted citation, use block result if available;
346 // fall back to per-source verification for non-case-law citations.
340347 const verifiedCitations = await Promise.all(
341 rawCitations.map((c) => verifyCitation(c))
348 rawCitations.map((c) => {
349 const blockHit = blockResults.get(c.citation);
350 if (blockHit) {
351 return Promise.resolve({ ...c, ...blockHit });
352 }
353 return verifyCitation(c);
354 })
342355 );
343356
344357 const responseTimeMs = Date.now() - startTime;
Modifiedlib/marco/verify.ts+102−16View fileUnifiedSplit
@@ -19,40 +19,52 @@ interface VerifyResult {
1919}
2020
2121/**
22 * Verify a case citation against CourtListener API.
23 * CourtListener (Free Law Project) provides millions of US court opinions.
22 * Verify a case citation against CourtListener v4 API (single citation).
23 * CourtListener uses Harvard's Eyecite library to parse and verify citations.
2424 */
2525async function verifyViaCourtListener(
26 caseName: string,
26 _caseName: string,
2727 citation: string
2828): Promise<VerifyResult | null> {
2929 try {
30 const params = new URLSearchParams({
31 q: citation,
32 type: "o", // opinions
33 });
34
3530 const res = await fetch(
36 `https://www.courtlistener.com/api/rest/v3/search/?${params}`,
31 "https://www.courtlistener.com/api/rest/v4/citation-lookup/",
3732 {
33 method: "POST",
3834 headers: {
39 Authorization: `Token ${process.env.COURTLISTENER_API_KEY || ""}`,
35 "Content-Type": "application/json",
36 ...(process.env.COURTLISTENER_API_KEY
37 ? { Authorization: `Token ${process.env.COURTLISTENER_API_KEY}` }
38 : {}),
4039 },
41 next: { revalidate: 86400 }, // Cache for 24 hours
40 body: JSON.stringify({ text: citation }),
41 signal: AbortSignal.timeout(8000),
42 next: { revalidate: 86400 },
4243 }
4344 );
4445
4546 if (!res.ok) return null;
4647
47 const data = await res.json();
48 const data: Array<{ citation: string; clusters: Array<{ absolute_url?: string; case_name?: string; date_filed?: string; snippet?: string }> }> =
49 await res.json();
4850
49 if (data.results && data.results.length > 0) {
50 const match = data.results[0];
51 if (data.length > 0 && data[0].clusters && data[0].clusters.length > 0) {
52 const cluster = data[0].clusters[0];
5153 return {
5254 status: "VERIFIED",
53 sourceUrl: `https://www.courtlistener.com${match.absolute_url || ""}`,
55 sourceUrl: `https://www.courtlistener.com${cluster.absolute_url || ""}`,
56 sourceDb: "CourtListener",
57 excerpt: cluster.snippet || cluster.case_name || null,
58 };
59 }
60
61 // Citation parsed but no matching opinion found in database
62 if (data.length > 0 && data[0].clusters?.length === 0) {
63 return {
64 status: "NOT_FOUND" as VerificationStatus,
65 sourceUrl: null,
5466 sourceDb: "CourtListener",
55 excerpt: match.snippet || null,
67 excerpt: null,
5668 };
5769 }
5870
@@ -62,6 +74,80 @@ async function verifyViaCourtListener(
6274 }
6375}
6476
77/**
78 * Block-level citation verification via CourtListener v4 citation-lookup.
79 *
80 * Send the entire Marco response text to CourtListener's Eyecite-powered
81 * API. It finds every legal citation in the block simultaneously and
82 * returns verification status for each. This is the primary verification
83 * pass — dramatically more accurate than per-citation regex matching alone.
84 *
85 * Returns a Map of citation string → VerifyResult for fast lookup.
86 */
87export async function verifyBlockOfText(
88 text: string
89): Promise<Map<string, VerifyResult>> {
90 const results = new Map<string, VerifyResult>();
91
92 if (!text || text.length < 10) return results;
93
94 try {
95 const res = await fetch(
96 "https://www.courtlistener.com/api/rest/v4/citation-lookup/",
97 {
98 method: "POST",
99 headers: {
100 "Content-Type": "application/json",
101 ...(process.env.COURTLISTENER_API_KEY
102 ? { Authorization: `Token ${process.env.COURTLISTENER_API_KEY}` }
103 : {}),
104 },
105 body: JSON.stringify({ text }),
106 signal: AbortSignal.timeout(15000),
107 next: { revalidate: 0 },
108 }
109 );
110
111 if (!res.ok) return results;
112
113 const data: Array<{
114 citation: string;
115 clusters: Array<{
116 absolute_url?: string;
117 case_name?: string;
118 date_filed?: string;
119 snippet?: string;
120 }>;
121 }> = await res.json();
122
123 for (const item of data) {
124 if (!item.citation) continue;
125
126 if (item.clusters && item.clusters.length > 0) {
127 const cluster = item.clusters[0];
128 results.set(item.citation, {
129 status: "VERIFIED",
130 sourceUrl: `https://www.courtlistener.com${cluster.absolute_url || ""}`,
131 sourceDb: "CourtListener",
132 excerpt: cluster.snippet || cluster.case_name || null,
133 });
134 } else {
135 // CourtListener parsed the citation but found no matching opinion
136 results.set(item.citation, {
137 status: "NOT_FOUND",
138 sourceUrl: null,
139 sourceDb: "CourtListener",
140 excerpt: null,
141 });
142 }
143 }
144 } catch {
145 // Network error — return empty map, fall back to per-citation verification
146 }
147
148 return results;
149}
150
65151/**
66152 * Verify a statute or regulation against GovInfo API.
67153 */
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts