File size: 4,354 Bytes
8ca929f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95abc1b
 
 
8ca929f
95abc1b
8ca929f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cdb6b12
8ca929f
 
 
 
 
 
 
 
 
 
 
 
04d92bf
8ca929f
 
 
 
 
04d92bf
8ca929f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import { execFile } from "child_process";
import crypto from "crypto";
import fs from "fs/promises";
import path from "path";
import { promisify } from "util";
import type { FitAnalysis } from "../analysis/types";
import type { CvEditPlan, CvGenerateRequest, CvGenerationRecord } from "./types";
import { prepareSafeEditPlan, validateFinalCvOutput } from "./finalContent";
import { ensureJobDir, writeJobMetadata } from "./storage";

const execFileAsync = promisify(execFile);

export function featureFlagEnabled(name: string, fallback = false): boolean {
  const raw = process.env[name];
  if (raw == null) return fallback;
  return ["1", "true", "yes", "on"].includes(raw.toLowerCase());
}

export function createGenerationRecord(sourceFormat: "docx" | "pdf"): CvGenerationRecord {
  const jobId = crypto.randomUUID();
  const now = new Date().toISOString();
  return {
    jobId,
    status: "queued",
    sourceFormat,
    warnings: [],
    createdAt: now,
    updatedAt: now,
  };
}

export async function runCvGenerationWorker(payload: CvGenerateRequest, record: CvGenerationRecord): Promise<CvGenerationRecord> {
  const safePlan = prepareSafeEditPlan(payload.editPlan);
  const planValidationWarnings: string[] = [];
  for (const instruction of safePlan.plan.instructions) {
    if (!instruction.safeToApply) {
      throw new Error(`Refusing to generate CV because instruction ${instruction.id} is not marked safeToApply.`);
    }
    if (instruction.replacementText) {
      const validation = validateFinalCvOutput(instruction.replacementText, instruction.sourceLanguage as any);
      if (!validation.valid) {
        throw new Error(`Refusing to generate CV because replacement text still contains banned internal phrases: ${validation.violations.join(", ")}`);
      }
    }
  }
  planValidationWarnings.push(...safePlan.warnings);

  const jobDir = await ensureJobDir(record.jobId);
  const sourceBuffer = Buffer.from(payload.sourceDocument.base64, "base64");
  const sourcePath = path.join(jobDir, payload.sourceDocument.fileName);
  const requestPath = path.join(jobDir, "request.json");
  const outputPath = path.join(jobDir, `updated-${payload.sourceDocument.fileName.replace(/\s+/g, "-")}`);
  const redlinePath = path.join(jobDir, `change-report-${path.parse(payload.sourceDocument.fileName).name}.md`);

  await fs.writeFile(sourcePath, sourceBuffer);
  await fs.writeFile(
    requestPath,
    JSON.stringify(
      {
        sourcePath,
        outputPath,
        redlinePath,
        sourceFormat: payload.sourceDocument.format,
        sourceFileName: payload.sourceDocument.fileName,
        resumeText: payload.resumeText,
        analysis: payload.analysis,
        editPlan: safePlan.plan,
      },
      null,
      2,
    ),
    "utf-8",
  );

  const pythonBin = process.env.PYTHON_EXECUTABLE || (process.platform === "win32" ? "python" : "python3");
  const workerPath = path.join(process.cwd(), "scripts", "cv_apply_worker.py");
  record.status = "processing";
  record.updatedAt = new Date().toISOString();
  await writeJobMetadata(record);

  try {
    const { stdout } = await execFileAsync(pythonBin, [workerPath, requestPath], {
      cwd: process.cwd(),
      timeout: 180000,
      maxBuffer: 10 * 1024 * 1024,
    });
    const result = JSON.parse(stdout.trim());
    const downloadPath = result.outputPath || outputPath;
    const updatedRecord: CvGenerationRecord = {
      ...record,
      status: "completed",
      outputFileName: result.outputFileName,
      outputMimeType: result.outputMimeType,
      downloadPath,
      redlinePath,
      warnings: Array.from(new Set([...(result.warnings || []), ...planValidationWarnings])),
      updatedAt: new Date().toISOString(),
    };
    await writeJobMetadata(updatedRecord);
    return updatedRecord;
  } catch (error: any) {
    const updatedRecord: CvGenerationRecord = {
      ...record,
      status: "failed",
      error: error.stderr?.toString() || error.message || "Document generation failed.",
      updatedAt: new Date().toISOString(),
    };
    await writeJobMetadata(updatedRecord);
    throw updatedRecord;
  }
}

export function buildChangeReportSeed(analysis: FitAnalysis, editPlan: CvEditPlan) {
  return {
    summary: analysis.profileSummary,
    warnings: editPlan.warnings,
    instructionCount: editPlan.instructions.length,
  };
}