{"record":{"id":"defe0bc1e41a22a7","repo":"nexu-io/open-design","slug":"field-must-be-a-string","errorCode":null,"errorMessage":"${field} must be a string","messagePattern":"(.+?) must be a string","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/daemon/src/live-artifacts/refresh.ts","lineNumber":514,"sourceCode":"    const source = options.artifact.document.sourceJson;\n    const mapped = source.toolName === 'public_github_repository_metric' && source.outputMapping?.dataPaths !== undefined\n      ? asBoundedRefreshOutput(applyDataPaths(options.documentOutput.output, source.outputMapping.dataPaths))\n      : applyLiveArtifactOutputMapping({\n          source,\n          output: options.documentOutput.output,\n        });\n    deepMergeBoundedJsonObject(dataJson, mapped);\n    if (source.toolName === 'public_github_repository_metric') {\n      applyLegacyGithubRepositoryMetricCompat(dataJson, options.documentOutput.output);\n    }\n  }\n\n  return { dataJson: asBoundedRefreshOutput(dataJson) };\n}\n\nfunction optionalString(value: BoundedJsonValue | undefined, field: string): string | undefined {\n  if (value === undefined) return undefined;\n  if (typeof value !== 'string') throw new Error(`${field} must be a string`);\n  return value;\n}\n\nfunction optionalPositiveInteger(value: BoundedJsonValue | undefined, field: string, defaultValue: number, maxValue: number): number {\n  if (value === undefined) return defaultValue;\n  if (!Number.isSafeInteger(value) || typeof value !== 'number' || value < 1) throw new Error(`${field} must be a positive integer`);\n  return Math.min(value, maxValue);\n}\n\nfunction selectJsonPath(input: ProjectFilesReadJsonInput): string {\n  const rawPath = optionalString(input.path, 'input.path') ?? optionalString(input.file, 'input.file') ?? optionalString(input.name, 'input.name');\n  if (rawPath === undefined) throw new Error('project_files.read_json requires input.path');\n  return validateProjectPath(rawPath);\n}\n\nfunction compactTextPreview(text: string, query: string | undefined): string {\n  const normalized = text.replace(/\\s+/g, ' ').trim();\n  if (normalized.length <= 240) return normalized;","sourceCodeStart":496,"sourceCodeEnd":532,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/live-artifacts/refresh.ts#L496-L532","documentation":"optionalString (refresh.ts:512-516) is a typed accessor used by selectJsonPath (for input.path/file/name) and selectGithubRepositoryApiUrl (for input.url). It returns undefined when the field is absent but throws when the field is present and not a string, so callers cannot mistake a wrong-typed field for a missing one.","triggerScenarios":"A refresh source.input provides path/file/name (for project_files.read_json) or url (for public_github_repository_metric) as a number, boolean, object, or array instead of a string.","commonSituations":"A connector/tool input built programmatically that passes a numeric path or a parsed-JSON object where a string was expected; a migration that changed a field's type; user input not coerced to string.","solutions":["Pass the field as a string (e.g. input.path: 'data.json', input.url: 'https://...').","Coerce known-safe values to string before constructing the input.","Validate the input shape against the tool's input schema before refresh."],"exampleFix":"// before\nconst input = { path: 42 };\n// throws `input.path must be a string`\n\n// after\nconst input = { path: 'data.json' };","handlingStrategy":"type-guard","validationCode":"function optionalString(v: unknown, field: string): string | undefined {\n  if (v === undefined) return undefined;\n  if (typeof v !== 'string') throw new Error(`${field} must be a string`);\n  return v;\n}","typeGuard":"function isOptionalString(v: unknown): v is string | undefined {\n  return v === undefined || typeof v === 'string';\n}","tryCatchPattern":null,"preventionTips":["Type the source.input against the tool's input schema (ProjectFilesReadJsonInput, PublicgithubRepositoryMetricInput).","Coerce values to string at the boundary, or reject non-strings with a clear message.","Run typecheck (`pnpm --filter @open-design/daemon typecheck`) after changing input shapes."],"tags":["live-artifacts","refresh","input-validation","typescript"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}