#!/usr/bin/env node import { readFileSync, writeFileSync } from 'fs'; import { resolve } from 'path'; import { fileURLToPath } from 'url'; export const APPLICATION_ANSWERS_HEADING = '## Application Answers'; const VALID_STATES = new Set(['filled', 'submitted']); function inline(value) { return String(value ?? '').replace(/\s+/g, ' ').trim(); } function valueText(value) { if (Array.isArray(value)) return value.map(inline).filter(Boolean).join(', '); return String(value ?? '').trim(); } function pick(object, keys) { for (const key of keys) { const value = object?.[key]; if (Array.isArray(value)) { if (value.length > 0) return value; continue; } if (value !== undefined && value !== null && String(value).trim()) return value; } return ''; } function list(value) { return Array.isArray(value) ? value : []; } function normalizeState(state) { const normalized = inline(state || 'filled').toLowerCase(); if (!VALID_STATES.has(normalized)) { throw new Error(`Application answer state must be one of: ${[...VALID_STATES].join(', ')}`); } return normalized; } function normalizeDate(date) { return inline(date || new Date().toISOString().slice(0, 10)); } function quoteBlock(value) { const text = String(value ?? '').replace(/\r\n/g, '\n').trim(); if (!text) return '> Not recorded.'; return text.split('\n').map((line) => `> ${line}`).join('\n'); } function qaLines(entries, { labelKeys, valueKeys, fallback }) { if (entries.length === 0) return ['- None captured.']; return entries.flatMap((entry, index) => { const label = inline(pick(entry, labelKeys)) || `${fallback} ${index + 1}`; const answer = pick(entry, valueKeys); return [ `${index + 1}. **${label}**`, '', quoteBlock(answer), '', ]; }).slice(0, -1); } function compactLines(entries, { labelKeys, valueKeys, fallback }) { if (entries.length === 0) return ['- None captured.']; return entries.map((entry, index) => { const label = inline(pick(entry, labelKeys)) || `${fallback} ${index + 1}`; const value = valueText(pick(entry, valueKeys)) || 'Not recorded'; return `${index + 1}. **${label}:** ${value}`; }); } function fileLines(entries) { if (entries.length === 0) return ['- None captured.']; return entries.map((entry, index) => { const label = inline(pick(entry, ['field', 'name', 'label', 'type'])) || `File ${index + 1}`; const file = inline(pick(entry, ['path', 'file', 'filename', 'url'])) || 'Not recorded'; const version = inline(pick(entry, ['version', 'variant'])); return `${index + 1}. **${label}:** ${version ? `${file} (${version})` : file}`; }); } export function normalizeApplicationAnswersSnapshot(snapshot = {}) { return { date: normalizeDate(snapshot.date), state: normalizeState(snapshot.state), freeText: list(snapshot.freeText ?? snapshot.freeTextAnswers ?? snapshot.answers), selections: list(snapshot.selections ?? snapshot.selectedOptions), fieldValues: list(snapshot.fieldValues ?? snapshot.otherFields ?? snapshot.fields), files: list(snapshot.files ?? snapshot.uploads ?? snapshot.filesUsed), }; } export function formatApplicationAnswersSection(snapshot = {}) { const normalized = normalizeApplicationAnswersSnapshot(snapshot); const lines = [ APPLICATION_ANSWERS_HEADING, '', `**Date:** ${normalized.date}`, `**State:** ${normalized.state}`, '', '### Free-text answers', '', ...qaLines(normalized.freeText, { labelKeys: ['question', 'field', 'label', 'prompt'], valueKeys: ['answer', 'response', 'value', 'text'], fallback: 'Answer', }), '', '### Selections made', '', ...compactLines(normalized.selections, { labelKeys: ['question', 'field', 'label', 'prompt'], valueKeys: ['selection', 'selected', 'answer', 'value', 'options'], fallback: 'Selection', }), '', '### Other field values', '', ...compactLines(normalized.fieldValues, { labelKeys: ['question', 'field', 'label', 'prompt'], valueKeys: ['answer', 'response', 'value', 'text'], fallback: 'Field', }), '', '### Files used', '', ...fileLines(normalized.files), ]; return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trim()}\n`; } export function upsertApplicationAnswersSection(reportText, snapshot = {}) { const report = String(reportText ?? '').replace(/\r\n/g, '\n'); const section = formatApplicationAnswersSection(snapshot).trimEnd(); const heading = /^## Application Answers\s*$/m.exec(report); if (!heading) { return `${report.trimEnd()}\n\n${section}\n`; } const start = heading.index; const afterHeading = start + heading[0].length; const nextHeading = /^## .+$/m.exec(report.slice(afterHeading)); const end = nextHeading ? afterHeading + nextHeading.index : report.length; const before = report.slice(0, start).trimEnd(); const after = report.slice(end).trimStart(); return [before, section, after].filter(Boolean).join('\n\n') + '\n'; } function parseArgs(argv) { const args = {}; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; if (arg === '--help' || arg === '-h') args.help = true; else if (arg.startsWith('--')) { const value = argv[i + 1]; if (!value || value.startsWith('--')) { throw new Error(`Missing value for ${arg}`); } args[arg.slice(2)] = value; i += 1; } } return args; } function usage() { return [ 'Usage: node application-answers.mjs --report --input [--state filled|submitted] [--date YYYY-MM-DD]', '', 'The input JSON may contain: freeText, selections, fieldValues, files, date, state.', ].join('\n'); } async function main() { let args; try { args = parseArgs(process.argv.slice(2)); } catch (err) { console.error(`${err.message}\n\n${usage()}`); process.exitCode = 1; return; } if (args.help) { console.log(usage()); return; } if (!args.report || !args.input) { console.error(usage()); process.exitCode = 1; return; } const inputText = args.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(resolve(args.input), 'utf-8'); const input = JSON.parse(inputText); const snapshot = { ...input, date: args.date || input.date, state: args.state || input.state, }; const reportPath = resolve(args.report); const updated = upsertApplicationAnswersSection(readFileSync(reportPath, 'utf-8'), snapshot); writeFileSync(reportPath, updated, 'utf-8'); const normalized = normalizeApplicationAnswersSnapshot(snapshot); console.log(JSON.stringify({ report: reportPath, date: normalized.date, state: normalized.state }, null, 2)); } if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { main().catch((err) => { console.error(err.message); process.exitCode = 1; }); }