Skip to content

Commit d5d7ba1

Browse files
[dev] [carhartlewis] lewis/comp-audit-handoff-information-fixes-cs-103 (#2111)
* feat(context): resolve framework IDs to human-readable names in context entries * refactor(auditor): exclude framework selection and auditor sections from context --------- Co-authored-by: Lewis Carhart <lewis@trycomp.ai>
1 parent 4e5f321 commit d5d7ba1

4 files changed

Lines changed: 111 additions & 15 deletions

File tree

apps/app/src/app/(app)/[orgId]/settings/context-hub/data/getContextEntries.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,48 @@ import { headers } from 'next/headers';
44
import { cache } from 'react';
55
import 'server-only';
66

7+
const FRAMEWORK_ID_PATTERN = /\bfrk_[a-z0-9]+\b/g;
8+
9+
/**
10+
* Detects framework IDs (frk_xxx) in context entry answers and replaces them
11+
* with human-readable framework names. Handles legacy data from before the
12+
* write-time fix was applied.
13+
*/
14+
async function resolveFrameworkIdsInEntries<T extends { answer: string }>(
15+
entries: T[],
16+
): Promise<T[]> {
17+
// Collect all unique framework IDs across all entries
18+
const allIds = new Set<string>();
19+
for (const entry of entries) {
20+
const matches = entry.answer.match(FRAMEWORK_ID_PATTERN);
21+
if (matches) {
22+
for (const id of matches) {
23+
allIds.add(id);
24+
}
25+
}
26+
}
27+
28+
if (allIds.size === 0) return entries;
29+
30+
// Batch-fetch framework names for all IDs
31+
const frameworks = await db.frameworkEditorFramework.findMany({
32+
where: { id: { in: Array.from(allIds) } },
33+
select: { id: true, name: true },
34+
});
35+
36+
const idToName = new Map(frameworks.map((f) => [f.id, f.name]));
37+
38+
// Replace IDs with names in each entry's answer
39+
return entries.map((entry) => {
40+
const resolvedAnswer = entry.answer.replace(
41+
FRAMEWORK_ID_PATTERN,
42+
(id) => idToName.get(id) ?? id,
43+
);
44+
if (resolvedAnswer === entry.answer) return entry;
45+
return { ...entry, answer: resolvedAnswer };
46+
});
47+
}
48+
749
export const getContextEntries = cache(
850
async ({
951
orgId,
@@ -42,6 +84,10 @@ export const getContextEntries = cache(
4284
});
4385
const total = await db.context.count({ where });
4486
const pageCount = Math.ceil(total / perPage);
45-
return { data: entries, pageCount };
87+
88+
// Resolve any legacy framework IDs to display names
89+
const resolvedEntries = await resolveFrameworkIdsInEntries(entries);
90+
91+
return { data: resolvedEntries, pageCount };
4692
},
4793
);

apps/app/src/app/(app)/setup/actions/create-organization-minimal.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import { initializeOrganization } from '@/actions/organization/lib/initialize-organization';
44
import { authActionClientWithoutOrg } from '@/actions/safe-action';
5+
import { env } from '@/env.mjs';
56
import { createTrainingVideoEntries } from '@/lib/db/employee';
67
import { auth } from '@/utils/auth';
7-
import { env } from '@/env.mjs';
88
import { db } from '@db';
99
import { revalidatePath } from 'next/cache';
1010
import { headers } from 'next/headers';
@@ -46,6 +46,13 @@ export const createOrganizationMinimal = authActionClientWithoutOrg
4646
// Check if self-hosted
4747
const isSelfHosted = env.NEXT_PUBLIC_SELF_HOSTED === 'true';
4848

49+
// Resolve framework IDs to display names (e.g. "SOC 2", "ISO 27001")
50+
const frameworks = await db.frameworkEditorFramework.findMany({
51+
where: { id: { in: parsedInput.frameworkIds } },
52+
select: { name: true },
53+
});
54+
const frameworkNames = frameworks.map((f) => f.name).join(', ');
55+
4956
// Create a new organization
5057
const newOrg = await db.organization.create({
5158
data: {
@@ -68,7 +75,7 @@ export const createOrganizationMinimal = authActionClientWithoutOrg
6875
context: {
6976
create: {
7077
question: 'Which compliance frameworks do you need?',
71-
answer: parsedInput.frameworkIds.join(', '),
78+
answer: frameworkNames || parsedInput.frameworkIds.join(', '),
7279
tags: ['onboarding'],
7380
},
7481
},

apps/app/src/app/(app)/setup/actions/create-organization.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ export const createOrganization = authActionClientWithoutOrg
4141
// Create a new organization directly in the database
4242
const randomSuffix = Math.floor(100000 + Math.random() * 900000).toString();
4343

44+
// Resolve framework IDs to display names (e.g. "SOC 2", "ISO 27001")
45+
const frameworks = await db.frameworkEditorFramework.findMany({
46+
where: { id: { in: parsedInput.frameworkIds } },
47+
select: { name: true },
48+
});
49+
const frameworkNames = frameworks.map((f) => f.name).join(', ');
50+
4451
const newOrg = await db.organization.create({
4552
data: {
4653
name: parsedInput.organizationName,
@@ -62,7 +69,7 @@ export const createOrganization = authActionClientWithoutOrg
6269
question: step.question,
6370
answer:
6471
step.key === 'frameworkIds'
65-
? parsedInput.frameworkIds.join(', ')
72+
? frameworkNames || parsedInput.frameworkIds.join(', ')
6673
: (parsedInput[step.key as keyof typeof parsedInput] as string),
6774
tags: ['onboarding'],
6875
})),

apps/app/src/trigger/tasks/auditor/generate-auditor-content.ts

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getOrganizationContext } from '@/trigger/tasks/onboarding/onboard-organization-helpers';
2-
import { groq } from '@ai-sdk/groq';
2+
import { openai } from '@ai-sdk/openai';
33
import { db } from '@db';
44
import { logger, metadata, schemaTask } from '@trigger.dev/sdk';
55
import { generateText } from 'ai';
@@ -89,24 +89,54 @@ RULES:
8989
- No bullet points.
9090
${TONE_RULES}`,
9191

92-
'critical-vendors': `List vendors in this EXACT format, one per line:
92+
'critical-vendors': `Using the provided vendor/software list, narrow it down to ONLY the critical vendors from a SOC 2 perspective for the audit report.
9393
94+
A critical vendor is one that:
95+
- Hosts or processes customer data (cloud infrastructure providers like AWS, GCP, Azure)
96+
- Provides core identity / authentication services (e.g. Okta, Google Workspace, Microsoft 365 — but ONLY if used as the primary identity provider)
97+
- Is essential to the company's production system or service delivery
98+
- Handles sensitive data (e.g. payment processors IF the company processes payments as a core service)
99+
100+
DO NOT INCLUDE vendors that are:
101+
- Internal productivity / collaboration tools (e.g. Notion, Slack, Teams, Jira, Confluence, Asana)
102+
- General business tools (e.g. Stripe, HubSpot, Intercom, Zendesk)
103+
- HR / payroll tools (e.g. Rippling, Gusto, BambooHR)
104+
- Marketing or analytics tools
105+
- Version control or CI/CD tools (e.g. GitHub, GitLab) unless they host production infrastructure
106+
- Security monitoring tools (e.g. Vanta, Drata, CrowdStrike)
107+
108+
Typically a SOC 2 report includes only 3-6 critical vendors. Be very selective.
109+
110+
FORMAT — one vendor per line:
94111
[Vendor Name] – [Type: SaaS/IaaS/PaaS] – ([Brief description of service])
95112
96113
EXAMPLE:
97-
Zoom – SaaS – (Video conferencing / collaboration)
98114
AWS – IaaS / PaaS – (Cloud infrastructure and hosting)
99-
Microsoft 365 – SaaS – (Office productivity and identity)
115+
Google Workspace – SaaS – (Primary identity provider and email)
116+
Datadog – SaaS – (Production monitoring and observability)
100117
101118
RULES:
102119
- Do NOT include the section title.
103120
- Each vendor on its own line.
104121
- Follow the exact format: Name – Type – (Description)
105-
- Only include vendors explicitly mentioned in sources.
122+
- Only include vendors from the provided sources — do not add vendors not mentioned.
123+
- Aim for 3-6 vendors maximum.
106124
${TONE_RULES}`,
107125

108-
'subservice-organizations': `List subservice organizations in this EXACT format:
126+
'subservice-organizations': `Identify the subservice organisations from a SOC 2 perspective.
127+
128+
A subservice organisation is an external service provider whose infrastructure or platform the company DIRECTLY RELIES ON to deliver its own services to customers. In SOC 2 terms, these are typically the main cloud infrastructure / hosting providers (IaaS/PaaS) — e.g. AWS, Google Cloud Platform, Microsoft Azure.
129+
130+
DO NOT INCLUDE:
131+
- SaaS tools the company merely uses internally (e.g. Slack, Notion, Jira, GitHub, Stripe, HubSpot)
132+
- Communication or collaboration platforms (e.g. Teams, Zoom)
133+
- HR, payroll, or admin tools
134+
- Security or monitoring tools
135+
- Any tool that is NOT the primary infrastructure hosting the company's production system
136+
137+
Typically there is only 1 (sometimes 2) subservice organisations. Be very selective.
109138
139+
FORMAT:
110140
Subservice organisations: [Name1], [Name2], ...
111141
112142
If only one: "Subservice organisations: [Name]"
@@ -118,7 +148,7 @@ RULES:
118148
- Do NOT include the section title.
119149
- Use "Subservice organisations:" prefix.
120150
- Just list the names, comma-separated if multiple.
121-
- Only include organizations explicitly mentioned as subservice providers in sources.
151+
- Look for where the company hosts its applications and data — that is the subservice organisation.
122152
${TONE_RULES}`,
123153
};
124154

@@ -145,7 +175,7 @@ async function scrapeWebsite(website: string): Promise<string> {
145175
urls: [website],
146176
prompt:
147177
'Extract all text content from this website, including company information, services, mission, vision, and any other relevant business information. Return the content as plain text or markdown.',
148-
limit: 10
178+
limit: 10,
149179
}),
150180
});
151181

@@ -223,7 +253,7 @@ async function generateSectionContent(
223253
contextHubText: string,
224254
): Promise<string> {
225255
const { text } = await generateText({
226-
model: groq('openai/gpt-oss-120b'),
256+
model: openai('gpt-5.2'),
227257
system: `You are an expert at extracting and organizing company information for audit purposes.
228258
229259
CRITICAL RULES:
@@ -326,10 +356,16 @@ export const generateAuditorContentTask = schemaTask({
326356
};
327357
}
328358

329-
// Build context from organization data (excluding auditor sections to avoid circular reference)
359+
// Build context from organization data, excluding:
360+
// 1. Auditor sections (to avoid circular reference)
361+
// 2. Framework selection (contains raw IDs like "frk_xxx" and isn't relevant to auditor content)
330362
const auditorQuestions = new Set(Object.values(SECTION_QUESTIONS));
363+
const excludedQuestions = new Set([
364+
...auditorQuestions,
365+
'Which compliance frameworks do you need?',
366+
]);
331367
const contextHubText = questionsAndAnswers
332-
.filter((qa) => !auditorQuestions.has(qa.question))
368+
.filter((qa) => !excludedQuestions.has(qa.question))
333369
.map((qa) => `Q: ${qa.question}\nA: ${qa.answer}`)
334370
.join('\n\n');
335371

0 commit comments

Comments
 (0)