src/programs/snap-negative/seed-generator.ts
Linked repository file from this build.
import { fixtureReviewKey } from "./fixture-key";
import { selectFinal } from "./calculation";
import { seedPolicy } from "./seed-generator-policy";
import { emptySeedScenario, seedWorkflow } from "./seed-generator-workflow";
import type { SnapPolicy } from "./policy";
import { runFullEditCheck, validateRequiredInput } from "./validation";
import { MAX_HOUSEHOLD_SIZE } from "./input-limits";
import { fiscalYear } from "@/features/reviews/review-key";
import { DEMO_REVIEW_NUMBER } from "./constants";
import { deriveFfyFromSampleMonth } from "@/features/reviews/downstream";
import { CURRENT_CALC_VERSION, CURRENT_CHECK_VERSION } from "./rule-versions";
import { normalizeReviewAnswers } from "./field-catalog";
import { completedReviewAnswers } from "./seed";
import { FIRST_NAMES, LAST_NAMES } from "./seed-generator-names";
import { PA_LOCATIONS, type PaLocation } from "./seed-generator-locations";
import type { HouseholdMember, IncomeStream, Scenario, ScenarioId, SnapReview } from "./types";
import { dropKinds, type ReviewOutcome, type WorkflowState } from "@/features/reviews/types";
/** Federal fiscal year sample months: Oct (prior calendar year) through Sep. */
export function ffySampleMonths(ffy: number): string[] {
const months: string[] = [];
for (let month = 10; month <= 12; month++) {
months.push(`${ffy - 1}-${String(month).padStart(2, "0")}`);
}
for (let month = 1; month <= 9; month++) {
months.push(`${ffy}-${String(month).padStart(2, "0")}`);
}
return months;
}
/** Calendar month used as the QC sample month (YYYY-MM). */
export function currentSampleMonth(asOf: Date = new Date()): string {
return `${asOf.getUTCFullYear()}-${String(asOf.getUTCMonth() + 1).padStart(2, "0")}`;
}
export interface SeedGeneratorOptions {
ffyFrom: number;
ffyTo: number;
perMonth: number;
seed?: number;
/** When set, generate only these sample months instead of every month in the FFY range. */
sampleMonths?: readonly string[];
/** Reference date for workflow fixtures (defaults to noon UTC on the last sample-month day). */
asOf?: Date;
/** Skip raw review numbers across all years, or FFY-qualified keys within one year. */
preserveReviewNumbers?: string[];
}
export interface GeneratedSeedBatch {
asOf: string;
policies: SnapPolicy[];
reviews: SnapReview[];
closedSuspendedBisRows: string[];
rejectedBisRows: string[];
}
const EXAMINERS = ["101", "102", "103", "104", "105", "106", "107", "108"] as const;
const STREETS = [
"Main Street",
"Oak Avenue",
"Maple Road",
"Pine Lane",
"Cedar Drive",
"Elm Street",
"Walnut Avenue",
"Chestnut Road",
"Cherry Lane",
"Birch Drive",
"Willow Street",
"Spruce Avenue",
"Laurel Road",
"Market Street",
"Liberty Avenue",
"Union Road",
] as const;
/** Clerk roster: 201 owns 101–104; 202 owns 105–108. */
const SUPERVISOR_BY_EXAMINER: Record<(typeof EXAMINERS)[number], "201" | "202"> = {
"101": "201",
"102": "201",
"103": "201",
"104": "201",
"105": "202",
"106": "202",
"107": "202",
"108": "202",
};
export function supervisorForExaminer(examiner: string): "201" | "202" {
if (examiner in SUPERVISOR_BY_EXAMINER) {
return SUPERVISOR_BY_EXAMINER[examiner as (typeof EXAMINERS)[number]];
}
throw new Error(`Unknown examiner staff id: ${examiner}`);
}
type ActionCode = "A" | "C" | "S";
interface Rng {
next(): number;
int(min: number, max: number): number;
pick<T>(items: readonly T[]): T;
chance(probability: number): boolean;
}
function mulberry32(seed: number): Rng {
let state = seed >>> 0;
const next = () => {
state += 0x6d2b79f5;
let t = state;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
return {
next,
int(min, max) {
return min + Math.floor(next() * (max - min + 1));
},
pick(items) {
return items[Math.floor(next() * items.length)]!;
},
chance(probability) {
return next() < probability;
},
};
}
function parseSampleMonth(sampleMonth: string): { year: number; month: number } {
const match = /^(\d{4})-(0[1-9]|1[0-2])$/.exec(sampleMonth);
if (!match) throw new Error(`Invalid sample month: ${sampleMonth}`);
return { year: Number(match[1]), month: Number(match[2]) };
}
function monthIndex(year: number, month: number): number {
return year * 12 + (month - 1);
}
function monthsBetweenSampleAndDate(sampleMonth: string, asOf: Date): number {
const { year, month } = parseSampleMonth(sampleMonth);
const asOfIndex = monthIndex(asOf.getUTCFullYear(), asOf.getUTCMonth() + 1);
return asOfIndex - monthIndex(year, month);
}
function isoDate(year: number, month: number, day: number): string {
return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
}
function bisDate(iso: string): string {
return iso.replaceAll("-", "");
}
function randomDayInMonth(rng: Rng, sampleMonth: string): string {
const { year, month } = parseSampleMonth(sampleMonth);
const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate();
return isoDate(year, month, rng.int(1, daysInMonth));
}
function addDays(iso: string, days: number): string {
const date = new Date(`${iso}T12:00:00Z`);
date.setUTCDate(date.getUTCDate() + days);
return isoDate(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate());
}
export function generateCaseNumber(rng: Rng): string {
const prefix = rng.int(10, 59);
const suffix = rng.int(1, 9_999_999);
const separator = rng.pick(["/", "-"] as const);
return `${prefix}${separator}${suffix}`;
}
function randomName(rng: Rng): string {
return `${rng.pick(FIRST_NAMES)} ${rng.pick(LAST_NAMES)}`;
}
function formatPhone(rng: Rng, location: PaLocation): string {
const line = rng.int(1000, 9999);
return `(${location.areaCode}) 555-${line}`;
}
/** Collateral contact codes for child-only households (guardian/parent is not a member). */
const COLLATERAL_CONTACT_RESPONSES = ["02", "04", "06"] as const;
/** Household contact codes when the head or member was contacted directly. */
const HOUSEHOLD_CONTACT_RESPONSES = ["03", "05", "07"] as const;
interface GeneratedHousehold {
members: HouseholdMember[];
clientName: string;
isChildOnly: boolean;
guardianName?: string;
guardianRelationship?: "Legal Guardian" | "Parent";
}
function generateHousehold(rng: Rng, size: number): GeneratedHousehold {
const count = Math.max(1, Math.min(20, size));
const childOnly = count <= 3 && rng.chance(0.1);
if (childOnly) {
const childName = randomName(rng);
const guardianName = randomName(rng);
const guardianRelationship = rng.pick(["Legal Guardian", "Parent"] as const);
return {
isChildOnly: true,
clientName: childName,
guardianName,
guardianRelationship,
members: [{ id: "member-1", line: "01", name: childName, relationship: "Child" }],
};
}
const members: HouseholdMember[] = [
{ id: "member-1", line: "01", name: randomName(rng), relationship: "Head" },
];
let line = 2;
if (count >= 2 && rng.chance(0.65)) {
members.push({
id: `member-${line}`,
line: String(line).padStart(2, "0"),
name: randomName(rng),
relationship: rng.pick(["Spouse", "Partner"] as const),
});
line++;
}
while (members.length < count) {
members.push({
id: `member-${line}`,
line: String(line).padStart(2, "0"),
name: randomName(rng),
relationship: rng.pick(["Child", "Parent", "Sibling", "Other"] as const),
});
line++;
}
return {
isChildOnly: false,
clientName: members[0]!.name,
members,
};
}
function incomeForMember(rng: Rng, member: HouseholdMember, id: string): IncomeStream {
if (rng.chance(0.2)) {
const gross = rng.int(200, 2000) * 100;
return {
id,
memberId: member.id,
typeCode: "21",
category: "self_employment",
selfEmploymentKind: rng.pick(["farm", "non_farm"] as const),
amountCents: gross,
expensesCents: rng.chance(0.3) ? gross + 30000 : Math.floor(gross / 3),
description: "Synthetic business income",
};
}
const earned = member.relationship === "Head" || member.relationship === "Spouse";
if (earned && rng.chance(0.85)) {
return {
id,
memberId: member.id,
typeCode: "11",
category: "earned",
amountCents: rng.int(800, 3200) * 100,
expensesCents: 0,
description: "Synthetic wages",
};
}
if (rng.chance(0.25)) {
return {
id,
memberId: member.id,
typeCode: "31",
category: "unearned",
amountCents: rng.int(100, 900) * 100,
expensesCents: 0,
description: "Synthetic unearned income",
};
}
return {
id,
memberId: member.id,
typeCode: "11",
category: "earned",
amountCents: rng.int(400, 1800) * 100,
expensesCents: 0,
description: "Synthetic wages",
};
}
function populatedScenario(rng: Rng, members: HouseholdMember[], idPrefix: string): Scenario {
const householdSize = Math.min(members.length, MAX_HOUSEHOLD_SIZE);
const incomeMembers = members.slice(0, householdSize);
const income = incomeMembers.map((member, index) =>
incomeForMember(rng, member, `${idPrefix}-income-${index + 1}`),
);
return {
populated: true,
householdSize,
income,
medicalDeductionCents: rng.chance(0.15) ? rng.int(50, 250) * 100 : 0,
dependentCareCents: members.some((m) => m.relationship === "Child")
? rng.int(50, 300) * 100
: 0,
childSupportCents: 0,
rentCents: rng.int(600, 1400) * 100,
taxesInsuranceCents: rng.int(0, 150) * 100,
installationCents: 0,
otherShelterCents: 0,
sua: rng.pick(["heating", "non_heating", "limited", "none"] as const),
elderlyDisabled: rng.chance(0.08),
categoricalEligibility: false,
homeless: false,
};
}
function actionTypeCode(action: ActionCode): string {
return ({ A: "1", C: "2", S: "3" } as const)[action];
}
function negativeActionName(action: ActionCode): string {
return ({ A: "Denial", C: "Termination", S: "Suspension" } as const)[action];
}
function deriveCompletion(
sampleMonth: string,
asOf: Date,
rng: Rng,
): { state: WorkflowState; outcome: ReviewOutcome; completed: boolean } {
const sampleFfy = deriveFfyFromSampleMonth(sampleMonth);
const asOfMonth = `${asOf.getUTCFullYear()}-${String(asOf.getUTCMonth() + 1).padStart(2, "0")}`;
const currentFfy = deriveFfyFromSampleMonth(asOfMonth);
const monthsOld = monthsBetweenSampleAndDate(sampleMonth, asOf);
if (monthsOld < 0) return { state: "new_active", outcome: "", completed: false };
if (sampleFfy < currentFfy || monthsOld > 3) {
return {
state: rng.chance(0.1) ? "dropped" : "released_to_reporting",
outcome: rng.pick(["clean", "error"] as const),
completed: true,
};
}
if (monthsOld >= 2) {
return {
state: rng.pick([
"awaiting_review",
"needs_changes",
"awaiting_pec_approval",
"awaiting_manager",
"awaiting_cao_response",
"released_to_reporting",
"dropped",
] as const),
outcome: rng.chance(0.4) ? rng.pick(["clean", "error"] as const) : "",
completed: rng.chance(0.55),
};
}
if (monthsOld >= 1) {
return {
state: rng.pick([
"new_active",
"awaiting_review",
"needs_changes",
"awaiting_pec_approval",
"awaiting_manager",
"awaiting_cao_response",
"dropped",
] as const),
outcome: "",
completed: false,
};
}
return {
state: rng.pick(["new_active", "new_active", "awaiting_review"] as const),
outcome: "",
completed: false,
};
}
function starterAnswers(
rng: Rng,
action: ActionCode,
completed: boolean,
assignedDate: string,
completedDate: string,
location: PaLocation,
household: GeneratedHousehold,
): ReturnType<typeof normalizeReviewAnswers> {
const contactResponse = completed
? rng.pick(household.isChildOnly ? COLLATERAL_CONTACT_RESPONSES : HOUSEHOLD_CONTACT_RESPONSES)
: undefined;
const base = completed
? completedReviewAnswers({
actionType: actionTypeCode(action),
dateAssigned: assignedDate,
dateCompleted: completedDate,
dateCleared: addDays(completedDate, rng.int(5, 20)),
contactResponse,
})
: normalizeReviewAnswers({
actionType: actionTypeCode(action),
dateAssigned: assignedDate,
narrative: household.isChildOnly
? `Synthetic ${negativeActionName(action)} case for child household; collateral contact is ${household.guardianName} (${household.guardianRelationship}).`
: `Synthetic ${negativeActionName(action)} case awaiting examiner completion.`,
});
const streetNumber = rng.int(10, 9999);
const street = rng.pick(STREETS);
const mailingAddress = `${streetNumber} ${street}, ${location.city}, PA ${location.zip}`;
const actualAddress = household.isChildOnly
? `${mailingAddress}; Collateral/HH contact: ${household.guardianName} (${household.guardianRelationship})`
: rng.chance(0.7)
? mailingAddress
: `Rear unit; use alley entrance off ${street} in ${location.city}`;
return normalizeReviewAnswers({
...base,
telephone: formatPhone(rng, location),
actualAddress,
dateAssigned: assignedDate,
...(completed && household.isChildOnly
? {
narrative: `Child case verified through ${household.guardianRelationship!.toLowerCase()} ${household.guardianName}.`,
}
: {}),
});
}
function buildReview(
rng: Rng,
reviewNumber: string,
action: ActionCode,
sampleMonth: string,
asOf: Date,
policy: SnapPolicy,
): SnapReview {
const location = rng.pick(PA_LOCATIONS);
const district = String(rng.int(1, 12)).padStart(2, "0");
const household = generateHousehold(rng, rng.int(1, 20));
const { members, clientName } = household;
const streetNumber = rng.int(10, 9999);
const street = rng.pick(STREETS);
const address = `${streetNumber} ${street}, ${location.city}, PA ${location.zip}`;
const sampledActionDate = randomDayInMonth(rng, sampleMonth);
const actionDate =
sampleMonth === asOf.toISOString().slice(0, 7)
? [sampledActionDate, asOf.toISOString().slice(0, 10)].sort()[0]!
: sampledActionDate;
const noticeDate = action === "S" ? "" : addDays(actionDate, -rng.int(5, 25));
const assignedDate = addDays(actionDate, -rng.int(10, 45));
const completion = deriveCompletion(sampleMonth, asOf, rng);
const state = completion.state;
const completed = completion.completed || state !== "new_active" || Boolean(completion.outcome);
const drop = state === "dropped" || (state === "awaiting_manager" && rng.chance(0.2));
const outcome = drop
? "drop"
: completed
? completion.outcome || rng.pick(["clean", "error"] as const)
: "";
const completedDate = completed
? [addDays(actionDate, rng.int(14, 75)), asOf.toISOString().slice(0, 10)].sort()[0]!
: "";
const key = fixtureReviewKey(reviewNumber, sampleMonth);
const scenarios: Record<ScenarioId, Scenario> = completed
? {
eligibility: populatedScenario(rng, members, `${key}-eligibility`),
compI: populatedScenario(rng, members, `${key}-compI`),
compII: populatedScenario(rng, members, `${key}-compII`),
}
: {
eligibility: emptySeedScenario(),
compI: emptySeedScenario(),
compII: emptySeedScenario(),
};
const caseNumber = generateCaseNumber(rng);
const sampledExaminer = rng.pick(EXAMINERS);
const examiner = reviewNumber === DEMO_REVIEW_NUMBER ? "101" : sampledExaminer;
const review: SnapReview = {
policyId: policy.id,
policy: structuredClone(policy),
...(drop ? { dropKind: rng.pick(dropKinds) } : {}),
calcVersion: CURRENT_CALC_VERSION,
checkVersion: CURRENT_CHECK_VERSION,
id: key,
reviewNumber,
caseNumber,
program: "SNAP",
subtype: "negative",
sampleMonth,
clientName,
address,
county: location.county,
district,
localAgency: `${location.countyName} County CAO`,
examiner,
supervisor: supervisorForExaminer(examiner),
state,
outcome,
revision: 0,
facts: {
negativeAction: negativeActionName(action),
noticeDate,
actionDate,
members,
scenarios,
inputsRevision: completed ? 1 : 0,
answers: starterAnswers(
rng,
action,
completed,
assignedDate,
completedDate,
location,
household,
),
},
notes: [],
documents: [],
activity: [],
lineage: [
{
sourceSystem: "sample_delim",
role: action === "A" ? "rejected" : "closed_suspended",
fileName: action === "A" ? "rejected.txt" : "closed-suspended.txt",
rowNumber: 0,
rawText: "",
},
],
};
if (completed) {
review.facts.answers.finding = outcome === "error" ? "2" : "1";
review.facts.answers.recordedReasonAccuracy = outcome === "error" ? "2" : "1";
review.facts.answers.noticeClearUnderstandable = "1";
review.facts.answers.noticeReasonAccuracy = outcome === "error" ? "21" : "11";
review.facts.answers.narrative =
outcome === "error"
? "Synthetic verification found the negative action invalid."
: "Synthetic verification supports the negative action.";
if (outcome === "error") {
const finding = rng.pick([
{
variances: [{ element: "416", nature: "096" }],
narrative: "Policy was incorrectly applied to the negative action.",
},
{
variances: [{ element: "415", nature: "177" }],
narrative:
"The household was not given at least ten days to provide requested verification.",
},
{
variances: [{ element: "540", nature: "096" }],
narrative: "Policy was incorrectly applied to the notice.",
},
]);
review.facts.answers.variances = finding.variances;
review.facts.answers.narrative = `Synthetic verification found the negative action invalid. ${finding.narrative}`;
}
if (drop) {
review.facts.answers = normalizeReviewAnswers({
telephone: review.facts.answers.telephone,
actualAddress: review.facts.answers.actualAddress,
dateAssigned: assignedDate,
dateCompleted: completedDate,
actionType: actionTypeCode(action),
disposition: review.dropKind === "not_subject_to_review" ? "2" : "3",
nstrReason: review.dropKind === "not_subject_to_review" ? "08" : "",
narrative: `Synthetic ${review.dropKind} drop.`,
});
}
const blockers = validateRequiredInput(review);
if (blockers.length)
throw new Error(`Invalid completed seed: ${blockers.map((b) => b.message).join(" ")}`);
review.facts.final = selectFinal(review);
review.validation = {
revision: review.revision,
findings: runFullEditCheck(review),
createdAt: asOf.toISOString(),
};
if (review.validation.findings.length)
throw new Error("Completed seed contains open findings.");
}
review.facts.caoIssuedBenefitCents = rng.pick([0, 9900, 10000, 25000]);
seedWorkflow(review, asOf.toISOString(), rng.int(0, 100));
return review;
}
function toBisRow(review: SnapReview, action: ActionCode, examiner: string): string {
const { year, month } = parseSampleMonth(review.sampleMonth);
const columns = Array<string>(42).fill("");
const nameParts = review.clientName.trim().split(/\s+/);
const lastName = nameParts.at(-1) ?? "";
const firstName = nameParts.slice(0, -1).join(" ");
const addressParts = review.address.split(",").map((part) => part.trim());
columns[0] = review.reviewNumber;
columns[1] = "C";
columns[2] = action;
columns[4] = String(month).padStart(2, "0");
columns[5] = String(year);
columns[6] = review.county;
columns[7] = review.district;
columns[8] = review.caseNumber.replaceAll("/", "").replaceAll("-", "");
columns[9] = "FS";
columns[10] = review.facts.actionDate ? bisDate(review.facts.actionDate) : "";
if (action !== "S" && review.facts.noticeDate) {
columns[18] = bisDate(review.facts.noticeDate);
}
columns[22] = addressParts[0] ?? review.address;
columns[24] = addressParts[1] ?? "";
columns[25] = "PA";
columns[26] = /(\d{5})/.exec(addressParts[2] ?? "")?.[1] ?? "17101";
columns[28] = lastName;
columns[29] = firstName;
columns[40] = examiner;
return columns.join("|");
}
function resolveSampleMonths(options: SeedGeneratorOptions): string[] {
if (options.sampleMonths?.length) {
return [...new Set(options.sampleMonths)].sort();
}
const months: string[] = [];
for (let ffy = options.ffyFrom; ffy <= options.ffyTo; ffy++) {
months.push(...ffySampleMonths(ffy));
}
return months;
}
/** Continuous case ranges restart each fiscal year, including the 61000 and 660000 boundaries. */
export function seedReviewNumber(kind: "closed_suspended" | "rejected", sequence: number): string {
const start = kind === "closed_suspended" ? 60000 : 650000;
const limit = kind === "closed_suspended" ? 1999 : 19999;
if (!Number.isInteger(sequence) || sequence < 1 || sequence > limit)
throw new Error(
`${kind === "closed_suspended" ? "Closed/suspended" : "Rejected"} numbering allows ${limit} cases per FFY (${start + 1} through ${start + limit}).`,
);
return String(start + sequence);
}
export function generateSeedBatch(options: SeedGeneratorOptions): GeneratedSeedBatch {
let rng = mulberry32(options.seed ?? 42);
if (
![options.ffyFrom, options.ffyTo].every(
(year) => Number.isInteger(year) && year >= 1901 && year <= 9999,
) ||
options.ffyFrom > options.ffyTo ||
!Number.isInteger(options.perMonth) ||
options.perMonth < 1 ||
(options.seed !== undefined && !Number.isSafeInteger(options.seed))
)
throw new Error("Use valid fiscal years, a positive count and an integer seed.");
const sampleMonths = resolveSampleMonths(options);
for (const month of sampleMonths) parseSampleMonth(month);
const last = parseSampleMonth(sampleMonths.at(-1)!);
const asOf = options.asOf ?? new Date(Date.UTC(last.year, last.month, 0, 12));
if (!Number.isFinite(asOf.getTime())) throw new Error("Use a valid reference date.");
const policies = [...new Set(sampleMonths.map(deriveFfyFromSampleMonth))].map(seedPolicy);
const policiesByYear = new Map(
policies.map((policy) => [deriveFfyFromSampleMonth(policy.effectiveFrom), policy]),
);
const preserve = new Set(options.preserveReviewNumbers ?? []);
const reviews: SnapReview[] = [];
const closedSuspendedBisRows: string[] = [];
const rejectedBisRows: string[] = [];
let activeFfy = 0;
let closedSequence = 0;
let rejectedSequence = 0;
const counts = new Map<number, number>();
for (const sampleMonth of sampleMonths) {
const ffy = fiscalYear(sampleMonth);
counts.set(ffy, (counts.get(ffy) ?? 0) + options.perMonth);
}
for (const count of counts.values()) {
seedReviewNumber("closed_suspended", count);
seedReviewNumber("rejected", count);
}
for (const sampleMonth of sampleMonths) {
const ffy = fiscalYear(sampleMonth);
if (ffy !== activeFfy) {
activeFfy = ffy;
closedSequence = 0;
rejectedSequence = 0;
// Keep FFY 2026's existing seed while making each year's random stream independent.
rng = mulberry32((options.seed ?? 42) + ffy - 2026);
}
const policy = policiesByYear.get(ffy)!;
for (let index = 0; index < options.perMonth; index++) {
const closedNumber = seedReviewNumber("closed_suspended", ++closedSequence);
if (
!preserve.has(closedNumber) &&
!preserve.has(fixtureReviewKey(closedNumber, sampleMonth))
) {
const closedAction: ActionCode = rng.chance(0.72) ? "C" : "S";
const closedReview = buildReview(
rng,
closedNumber,
closedAction,
sampleMonth,
asOf,
policy,
);
reviews.push(closedReview);
const row = toBisRow(closedReview, closedAction, closedReview.examiner);
closedReview.lineage![0]!.rawText = row;
closedReview.lineage![0]!.rowNumber = closedSuspendedBisRows.length + 3;
closedSuspendedBisRows.push(row);
}
const rejectedNumber = seedReviewNumber("rejected", ++rejectedSequence);
if (
!preserve.has(rejectedNumber) &&
!preserve.has(fixtureReviewKey(rejectedNumber, sampleMonth))
) {
const rejectedReview = buildReview(rng, rejectedNumber, "A", sampleMonth, asOf, policy);
reviews.push(rejectedReview);
const row = toBisRow(rejectedReview, "A", rejectedReview.examiner);
rejectedReview.lineage![0]!.rawText = row;
rejectedReview.lineage![0]!.rowNumber = rejectedBisRows.length + 3;
rejectedBisRows.push(row);
}
}
}
reviews.sort(
(left, right) =>
fiscalYear(left.sampleMonth) - fiscalYear(right.sampleMonth) ||
left.reviewNumber.localeCompare(right.reviewNumber),
);
return { asOf: asOf.toISOString(), policies, reviews, closedSuspendedBisRows, rejectedBisRows };
}