{"record":{"id":"ec7567adc2e4d282","repo":"santifer/career-ops","slug":"key-must-be-an-array-in-path","errorCode":null,"errorMessage":"${key} must be an array in ${path}","messagePattern":"(.+?) must be an array in (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"verify-cv-facts.mjs","lineNumber":354,"sourceCode":"export function auditClaims(targetText, sourceText, config = {}) {\n  const allowed = allowedMetricSet(sourceText, config.allow_metrics);\n  const invented = [...metricClaims(targetText)].filter(claim => !allowed.has(claim));\n  // Hoisted: stripMarkup re-ran the whole markup pass once per configured\n  // phrase (CodeRabbit, reviewing #2175). Same result, one pass.\n  const targetPlain = stripMarkup(targetText).toLowerCase();\n  const forbidden = (config.forbidden_phrases || [])\n    .filter(Boolean)\n    .filter(phrase => targetPlain.includes(String(phrase).toLowerCase()));\n  return { invented, forbidden };\n}\n\n/** Load and validate the optional fact-gate configuration file. */\nfunction loadConfig(path) {\n  if (!existsSync(path)) return { allow_metrics: [], allow_facts: [], forbidden_phrases: [], warn_phrases: [] };\n  const config = JSON.parse(readFileSync(path, 'utf-8'));\n  for (const key of ['allow_metrics', 'allow_facts', 'forbidden_phrases', 'warn_phrases']) {\n    if (config[key] == null) config[key] = [];\n    else if (!Array.isArray(config[key])) throw new Error(`${key} must be an array in ${path}`);\n  }\n  return config;\n}\n\n/** Resolve a CLI or configuration path relative to the selected working directory. */\nfunction resolveInputPath(path, cwd = process.cwd()) {\n  return isAbsolute(path) ? path : join(cwd, path);\n}\n\n/** Check a normalized fact as a complete token or phrase, not a substring. */\nfunction sourceContainsFact(sourceText, value) {\n  const escaped = value\n    .replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n    .replace(/\\s+/g, '\\\\s+');\n  return new RegExp(`(?:^|[^\\\\p{L}\\\\p{N}+#/-])${escaped}(?=$|[^\\\\p{L}\\\\p{N}+#/-])`, 'iu').test(sourceText);\n}\n\n/**","sourceCodeStart":336,"sourceCodeEnd":372,"githubUrl":"https://github.com/santifer/career-ops/blob/60398d6549a46f5266929538af21cfab94badc75/verify-cv-facts.mjs#L336-L372","documentation":"verify-cv-facts.mjs's loadConfig() validates the optional fact-gate JSON: each of the four keys (allow_metrics, allow_facts, forbidden_phrases, warn_phrases) may be absent or null (defaulted to []) but must be an array if present. A scalar, string, or object value throws this error naming the offending key and file.","triggerScenarios":"Running `node verify-cv-facts.mjs <doc> --config my-gate.json` (or using the DEFAULT_CONFIG file) where e.g. `\"allow_metrics\": \"40%\"` or `\"forbidden_phrases\": {\"a\": 1}` instead of `[\"40%\"]` / `[\"a\"]`. JSON.parse succeeded; only the shape check failed.","commonSituations":"Hand-editing the fact-gate config and quoting a single value instead of wrapping it in an array; merging configs where a list got collapsed to a string; YAML-vs-JSON confusion producing an object.","solutions":["Open the path shown in the message and wrap the named key's value in a JSON array","Re-run `node verify-cv-facts.mjs --self-test` or your document check to confirm the config now loads","If the config is optional for you, move/rename it to fall back to the all-empty default (all lists, no gating)"],"exampleFix":"// before (my-gate.json)\n{\n  \"allow_metrics\": \"40%\",\n  \"forbidden_phrases\": \"guaranteed\"\n}\n// after\n{\n  \"allow_metrics\": [\"40%\"],\n  \"forbidden_phrases\": [\"guaranteed\"]\n}","handlingStrategy":"validation","validationCode":"const raw = JSON.parse(readFileSync(configPath, 'utf-8'));\nfor (const key of ['allow_metrics', 'allow_facts', 'forbidden_phrases', 'warn_phrases']) {\n  if (raw[key] != null && !Array.isArray(raw[key])) {\n    console.error(`${key} must be an array — fix ${configPath} before running`);\n    process.exit(1);\n  }\n}","typeGuard":"function isFactGateConfig(v) {\n  if (v == null || typeof v !== 'object') return false;\n  return ['allow_metrics', 'allow_facts', 'forbidden_phrases', 'warn_phrases']\n    .every((k) => v[k] == null || Array.isArray(v[k]));\n}","tryCatchPattern":"try {\n  const config = loadConfig(path);\n} catch (err) {\n  if (/must be an array in/.test(err.message)) {\n    // config shape problem: edit the named key, then re-run\n    console.error(err.message);\n    process.exit(1);\n  }\n  throw err; // JSON.parse syntax errors surface here too — fix the JSON first\n}","preventionTips":["Always wrap fact-gate lists in JSON arrays, even single-entry lists","Validate edited configs with a JSON schema check in your editor or a one-liner before running","Absent keys default to []; omit rather than null/fill with empty strings"],"tags":["config","json","validation","fact-gate"],"backgroundTag":"invalid-config-value","analyzedSha":"60398d6549a46f5266929538af21cfab94badc75","analyzedAt":"2026-08-20T23:00:06.764Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}