@@ -14,6 +14,7 @@ import { setupAllDatabases } from "../../config/setupAllDatabases";
1414import { SEC_DRY_RUN , SEC_RAW_DATA_FOLDER } from "../../config/tokens" ;
1515import { ExtractionDeadLetterRepo } from "../../storage/dead-letter/ExtractionDeadLetterRepo" ;
1616import { FILING_REPOSITORY_TOKEN } from "../../storage/filing/FilingSchema" ;
17+ import { SPAC_CANDIDATE_REPOSITORY_TOKEN } from "../../storage/spac/SpacCandidateSchema" ;
1718import { SpacReportWriter } from "../../storage/spac/SpacReportWriter" ;
1819import { SpacRepo } from "../../storage/spac/SpacRepo" ;
1920import { ExtractorRunRepo } from "../../storage/versioning/ExtractorRunRepo" ;
@@ -63,6 +64,23 @@ async function seedFiling(
6364 } ) ;
6465}
6566
67+ async function seedSpacCandidate ( ) : Promise < void > {
68+ await globalServiceRegistry . get ( SPAC_CANDIDATE_REPOSITORY_TOKEN ) . put ( {
69+ cik : CIK ,
70+ name : "Screened Acquisition Corp" ,
71+ current_sic : 6770 ,
72+ signal_sic_6770 : true ,
73+ signal_name_match : true ,
74+ signal_renamed_from : null ,
75+ first_reg_form : "S-1" ,
76+ first_reg_date : "2020-11-01" ,
77+ reg_while_spac_named : true ,
78+ confidence : "high" ,
79+ identified_at : "2026-01-01T00:00:00.000Z" ,
80+ signal_filed_sic_6770 : null ,
81+ } ) ;
82+ }
83+
6684async function seedSuccessfulRun (
6785 accession : string ,
6886 form : string ,
@@ -268,6 +286,15 @@ describe("ProcessSpacTimelineTask", () => {
268286 // dead-letter under `redemption` / `loi` as well as `8-K`. Counting triage
269287 // without naming those ids left the operator with a placeholder.
270288 await seedFiling ( "0000000000-26-000001" , "8-K" , "2021-01-04" , "d8k.htm" ) ;
289+ await new SpacReportWriter ( ) . recordRegistration ( {
290+ cik : CIK ,
291+ accession_number : "0000000000-26-000000" ,
292+ filing_date : "2021-01-01" ,
293+ form : "S-1" ,
294+ primary_document : "s1.htm" ,
295+ spac_name : "Gated SPAC" ,
296+ spac_sic : 6770 ,
297+ } ) ;
271298
272299 const proto = ProcessAccessionDocFormTask . prototype ;
273300 const real = proto . execute ;
@@ -485,6 +512,50 @@ describe("ProcessSpacTimelineTask", () => {
485512 expect ( out . skipped ) . toBe ( 0 ) ;
486513 } ) ;
487514
515+ it ( "does not process gated 8-Ks or Form 15s while the issuer has no spac row" , async ( ) => {
516+ // `sync spacs` worklist IS high/medium candidates, so the handler's
517+ // `isSpacCandidate` warning fires on every milestone 8-K / Form 15 of a
518+ // false-positive operating company (VolitionRx, CIK 93314) that will never
519+ // mint a row. Date-order replay plus the repair pass already handle a real
520+ // SPAC: skip the gated extractors until the row exists, then pick them up.
521+ await seedSpacCandidate ( ) ;
522+ await seedFiling ( "0000000000-26-000001" , "8-K" , "2011-03-01" , "d8k.htm" , "1.01" ) ;
523+ await seedFiling ( "0000000000-26-000002" , "15-12B" , "2011-04-01" , null ) ;
524+
525+ const warn = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } ) ;
526+ const spy = vi
527+ . spyOn ( ProcessAccessionDocFormTask . prototype , "execute" )
528+ . mockResolvedValue ( { success : true } ) ;
529+ try {
530+ const out = await new ProcessSpacTimelineTask ( ) . run ( { cik : CIK } ) ;
531+
532+ expect ( spy ) . not . toHaveBeenCalled ( ) ;
533+ expect ( warn ) . not . toHaveBeenCalled ( ) ;
534+ expect ( out . matched ) . toBe ( 2 ) ;
535+ expect ( out . skipped ) . toBe ( 2 ) ;
536+ expect ( out . processed ) . toBe ( 0 ) ;
537+ } finally {
538+ warn . mockRestore ( ) ;
539+ }
540+ } ) ;
541+
542+ it ( "repair-passes a never-processed 8-K dated before the S-1 that mints the row" , async ( ) => {
543+ // First-time replay (no success row) used to process the 8-K in the first
544+ // pass because shouldReplay said "unprocessed". Date order then ran it
545+ // before the S-1, dropped the milestone, and warned. The repair pass is
546+ // how gated filings wait for the row — including ones never processed.
547+ await seedFiling ( "0000000000-26-000001" , "8-K" , "2020-12-01" , "d8k.htm" , "5.07" ) ;
548+ await seedFiling ( "0000000000-26-000002" , "S-1" , "2021-01-04" , "s1.htm" ) ;
549+ const spy = mockFormProcessor ( ) ;
550+
551+ const out = await new ProcessSpacTimelineTask ( ) . run ( { cik : CIK } ) ;
552+
553+ const accessions = spy . mock . calls . map ( ( c ) => c [ 0 ] ?. accessionNumber ) ;
554+ expect ( accessions ) . toEqual ( [ "0000000000-26-000002" , "0000000000-26-000001" ] ) ;
555+ expect ( out . matched ) . toBe ( 2 ) ;
556+ expect ( out . skipped ) . toBe ( 0 ) ;
557+ } ) ;
558+
488559 it ( "reaches a fixpoint: a second invocation processes nothing" , async ( ) => {
489560 // The invariant every gated predicate must hold: processing a filing writes
490561 // the artifact the predicate keys on, so it leaves the selected set. This
0 commit comments