From 97607e0bd42c7912a462aa4bed262ed9920a8632 Mon Sep 17 00:00:00 2001 From: Investor Flow Build Date: Tue, 30 Jun 2026 13:56:29 -0400 Subject: [PATCH] slice 6 fix (ornith-35): form4_tx double-.xml URL + correct Form 4 XML elements (rptOwnerCik/rptOwnerName/rptOwnerRelationship). qwopus re-review pending --- app/server/src/adapters/EdgarAdapter.ts | 35 ++++++++++++++++++++----- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/app/server/src/adapters/EdgarAdapter.ts b/app/server/src/adapters/EdgarAdapter.ts index 8a78ab5..3a2fc1b 100644 --- a/app/server/src/adapters/EdgarAdapter.ts +++ b/app/server/src/adapters/EdgarAdapter.ts @@ -421,7 +421,7 @@ export class EdgarAdapter implements SourceFetch { } // --- Step 2: fetch the XML document ------------------------------------ - const xmlUrl = `https://www.sec.gov/Archives/edgar/data/${padded}/${accessionNoDashes}/${xmlName}.xml`; + const xmlUrl = `https://www.sec.gov/Archives/edgar/data/${padded}/${accessionNoDashes}/${xmlName}`; const xmlText = await this.edgarXmlFetch(xmlUrl); // --- Step 3: parse transactions ---------------------------------------- @@ -626,13 +626,34 @@ function parseForm4Transactions(text: string): Array<{ if (!infotables) return transactions; for (const table of infotables) { - // Extract reporter name. - const reporterMatch = table.match(/\s*(\d+[^<]*)/); - const reporter = reporterMatch ? reporterMatch[1].trim() : ''; + // Extract reporter CIK and name from . + const rptOwnerMatch = table.match(/([\s\S]*?)<\/rptOwner>/i); + let reporter = ''; + if (rptOwnerMatch) { + const ownerBlock = rptOwnerMatch[1]; + const cikMatch = ownerBlock.match(/\s*(\d+)/); + const nameMatch = ownerBlock.match(/\s*([\s\S]*?)<\/rptOwnerName>/i); + reporter = cikMatch ? cikMatch[1].trim() : ''; + if (nameMatch) { + reporter = nameMatch[1].replace(/<[^>]+>/g, '').trim(); + } + } - // Extract relationship (direct/indirect). - const relMatch = table.match(/\s*([ADI])/i); - const relationship = relMatch ? (relMatch[1] === 'D' ? 'Direct' : 'Indirect') : ''; + // Extract relationship from . + const relBlockMatch = table.match(/([\s\S]*?)<\/rptOwnerRelationship>/i); + let relationship = ''; + if (relBlockMatch) { + const relBlock = relBlockMatch[1]; + const parts: string[] = []; + if (/\s*1/i.test(relBlock)) parts.push('Director'); + if (/\s*1/i.test(relBlock)) { + const titleMatch = relBlock.match(/\s*([\s\S]*?)<\/officerTitle>/i); + parts.push(titleMatch ? titleMatch[1].replace(/<[^>]+>/g, '').trim() : 'Officer'); + } + if (/\s*1/i.test(relBlock)) parts.push('10% Owner'); + if (/\s*1/i.test(relBlock)) parts.push('Other'); + relationship = parts.join(', '); + } // Extract security title. const titleMatch = table.match(/\s*([\s\S]*?)<\/securityTitle>/i);