{"record":{"id":"2b8f524cf0c425e2","repo":"santifer/career-ops","slug":"not-applied","errorCode":"NOT_APPLIED","errorMessage":"Application #${appNum} is not Applied (status: \"${row.status.trim()}\"); use --force to seed anyway","messagePattern":"Application #(.+?) is not Applied \\(status: \"(.+?)\"\\); use --force to seed anyway","errorType":"error_code","errorClass":"SeedError","httpStatus":null,"severity":"error","filePath":"followup-seed.mjs","lineNumber":565,"sourceCode":"  if (options.date != null && !isValidCalendarDate(options.date)) {\n    throw new SeedError('INVALID_DATE', `--date must be a real calendar date in YYYY-MM-DD form: ${options.date}`);\n  }\n\n  const trackerPath = resolveTrackerPath(options.trackerPath);\n  const followupsPath = resolveFollowupsPath(options.followupsPath);\n\n  if (!existsSync(trackerPath)) {\n    throw new SeedError('ROW_NOT_FOUND', `Tracker not found at ${trackerPath}`);\n  }\n  const rows = readTrackerRows(trackerPath);\n  const row = rows.find(r => r.num === appNum);\n  if (!row) {\n    throw new SeedError('ROW_NOT_FOUND', `Application #${appNum} not found in ${trackerPath}`);\n  }\n\n  const normalized = normalizeStatus(row.status);\n  if (normalized !== 'applied' && !options.force && !options.assumeApplied) {\n    throw new SeedError('NOT_APPLIED', `Application #${appNum} is not Applied (status: \"${row.status.trim()}\"); use --force to seed anyway`);\n  }\n\n  // appDateSource travels with the result so a caller can tell a measured apply\n  // date from a proxy or a guess — the same contract followup-cadence.mjs and\n  // company-history.mjs already expose.\n  const { appliedDate, appDateSource } = resolveAppliedDate(row, options.date);\n  const cadence = resolveCadenceConfig({ profilePath: options.profilePath });\n  const nextDate = addDays(parseDate(appliedDate), cadence.applied_first);\n  const setDate = todayStr();\n  const pin = formatPinLine(appNum, nextDate, setDate);\n\n  if (options.dryRun) {\n    const existingContent = existsSync(followupsPath) ? readFileSync(followupsPath, 'utf-8') : '';\n    if (isAlreadySeeded(existingContent, appNum) && !options.force) {\n      return { seeded: false, appNum, pin: null, nextDate, appliedDate, appDateSource, setDate, reason: 'already-seeded', dryRun: true };\n    }\n    return { seeded: true, appNum, pin, nextDate, appliedDate, appDateSource, setDate, dryRun: true };\n  }","sourceCodeStart":547,"sourceCodeEnd":583,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/followup-seed.mjs#L547-L583","documentation":"followup-seed.mjs seeds a first follow-up date into data/follow-ups.md, but only for applications that are in the Applied state. When the tracker row's status, after normalization, is anything other than 'applied', seedFollowup refuses to seed and throws SeedError with code NOT_APPLIED, suggesting --force as an explicit override. This guard prevents follow-up cadence tracking from starting on rows that were never actually applied to (evaluated, rejected, discarded rows).","triggerScenarios":"Calling seedFollowup (or the CLI `node followup-seed.mjs <appNum>`) for an application number whose tracker row status normalizes to something other than 'applied' — e.g. 'Evaluated', 'Interview', 'Rejected', 'Discarded' — without passing --force (or options.force / options.assumeApplied when calling the API directly).","commonSituations":"Typo or stale row number passed on the CLI pointing at an evaluated-but-never-applied row; trying to seed follow-ups for a row backfilled with status 'N/A' or 'Discarded'; a workflow automation that assumes every tracker row is Applied; calling the exported API without setting options.force after an earlier manual status change.","solutions":["Check the row's status in data/applications.md — only seed follow-ups for rows actually in Applied state","If seeding is intentional despite the non-Applied status, re-run with --force (CLI) or options.force: true (API)","If the row should be Applied, fix the tracker status first via `node set-status.mjs <appNum> Applied` and then re-run the seed","Use options.assumeApplied: true in the API when the caller has already verified the row semantically represents an application"],"exampleFix":"// before\nawait seedFollowup({ appNum: 42 });\n// SeedError NOT_APPLIED: Application #42 is not Applied (status: \"Evaluated\")\n\n// after — verify then force or fix\nconst row = readTrackerRows(trackerPath).find(r => r.num === 42);\nif (normalizeStatus(row.status) !== 'applied') {\n  await seedFollowup({ appNum: 42, force: true }); // deliberate override\n}","handlingStrategy":"validation","validationCode":"import { readTrackerRows } from './tracker-parse.mjs';\nimport { normalizeStatus } from './normalize-statuses.mjs';\nconst row = readTrackerRows('data/applications.md').find(r => r.num === appNum);\nif (!row) throw new Error(`Application #${appNum} not found`);\nif (normalizeStatus(row.status) !== 'applied') {\n  // decide: fix the tracker status, or pass force:true deliberately\n}","typeGuard":"const isApplied = (row) => row && typeof row.status === 'string' && normalizeStatus(row.status) === 'applied';","tryCatchPattern":"try {\n  await seedFollowup({ appNum });\n} catch (e) {\n  if (e.code === 'NOT_APPLIED') {\n    console.warn(`#${appNum} skipped: ${e.message} (status not Applied; use --force to override)`);\n  } else throw e;\n}","preventionTips":["Only run follow-up seeding on rows that are genuinely in the Applied state","Fix tracker status first with `node set-status.mjs <appNum> Applied` rather than reaching for --force by default","Treat --force as a deliberate, logged override — never bake it into automation for all rows","Filter candidate rows by normalized status 'applied' before iterating seeds"],"tags":["cli","tracker","state-validation"],"backgroundTag":"invalid-state-transition","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}