ocpipe 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/parsing.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * DSTS SDK response parsing.
2
+ * ocpipe response parsing.
3
3
  *
4
4
  * Extracts and validates LLM responses using JSON or field marker formats.
5
5
  */
@@ -79,6 +79,7 @@ export function tryParseJson<T>(
79
79
  ok: false,
80
80
  errors: [
81
81
  {
82
+ code: 'no_json_found',
82
83
  path: '',
83
84
  message: 'No JSON found in response',
84
85
  expectedType: 'object',
@@ -96,6 +97,7 @@ export function tryParseJson<T>(
96
97
  ok: false,
97
98
  errors: [
98
99
  {
100
+ code: 'json_parse_failed',
99
101
  path: '',
100
102
  message: `JSON parse failed: ${parseErr.message}`,
101
103
  expectedType: 'object',
@@ -157,6 +159,7 @@ function zodErrorsToFieldErrors(
157
159
  }
158
160
 
159
161
  errors.push({
162
+ code: 'schema_validation_failed',
160
163
  path,
161
164
  message: issue.message,
162
165
  expectedType,
@@ -507,13 +510,13 @@ export function applyJqPatch(
507
510
  /\binput\b/,
508
511
  /\binputs\b/,
509
512
  /\bsystem\b/,
510
- /\@base64d/,
511
- /\@uri/,
512
- /\@csv/,
513
- /\@tsv/,
514
- /\@json/,
515
- /\@text/,
516
- /\@sh/,
513
+ /@base64d/,
514
+ /@uri/,
515
+ /@csv/,
516
+ /@tsv/,
517
+ /@json/,
518
+ /@text/,
519
+ /@sh/,
517
520
  /`[^`]*`/, // Backtick string interpolation
518
521
  /\bimport\b/,
519
522
  /\binclude\b/,
@@ -532,7 +535,7 @@ export function applyJqPatch(
532
535
 
533
536
  // Only allow patches that look like field operations
534
537
  // Valid: .foo = "bar", .items[0].name = .items[0].title, del(.foo) | .bar = 1
535
- const safePattern = /^[\s\w\[\]."'=|,:\-{}]*$/
538
+ const safePattern = /^[\s\w[\]."'=|,:\-{}]*$/
536
539
  if (!safePattern.test(patch)) {
537
540
  console.error(` Invalid characters in patch, skipping: ${patch}`)
538
541
  return obj
@@ -648,44 +651,63 @@ export function buildBatchJsonPatchPrompt(
648
651
  return lines.join('\n')
649
652
  }
650
653
 
651
- /** extractJsonPatch extracts a JSON Patch array from an LLM response. */
652
- export function extractJsonPatch(response: string): JsonPatchOperation[] {
653
- // Try to find JSON array in code blocks first
654
- const codeBlockMatch = response.match(
655
- /```(?:json)?\s*(\[[\s\S]*?\])[\s\S]*?```/,
656
- )
657
- if (codeBlockMatch?.[1]) {
658
- try {
659
- return JSON.parse(codeBlockMatch[1]) as JsonPatchOperation[]
660
- } catch {
661
- // Continue to try other methods
654
+ /**
655
+ * extractBalancedArray extracts a balanced JSON array from a string starting at startIdx.
656
+ * Returns the array substring or null if not found/unbalanced.
657
+ */
658
+ function extractBalancedArray(text: string, startIdx: number): string | null {
659
+ if (startIdx === -1 || startIdx >= text.length) return null
660
+
661
+ let bracketCount = 0
662
+ let endIdx = startIdx
663
+ for (let i = startIdx; i < text.length; i++) {
664
+ if (text[i] === '[') bracketCount++
665
+ else if (text[i] === ']') {
666
+ bracketCount--
667
+ if (bracketCount === 0) {
668
+ endIdx = i + 1
669
+ break
670
+ }
662
671
  }
663
672
  }
664
673
 
665
- // Try to find raw JSON array by counting brackets
666
- const startIdx = response.indexOf('[')
667
- if (startIdx !== -1) {
668
- let bracketCount = 0
669
- let endIdx = startIdx
670
- for (let i = startIdx; i < response.length; i++) {
671
- if (response[i] === '[') bracketCount++
672
- else if (response[i] === ']') {
673
- bracketCount--
674
- if (bracketCount === 0) {
675
- endIdx = i + 1
676
- break
674
+ if (endIdx > startIdx && bracketCount === 0) {
675
+ return text.slice(startIdx, endIdx)
676
+ }
677
+ return null
678
+ }
679
+
680
+ /** extractJsonPatch extracts a JSON Patch array from an LLM response. */
681
+ export function extractJsonPatch(response: string): JsonPatchOperation[] {
682
+ // Try to find JSON array in code blocks first
683
+ // Use indexOf to find code block boundaries to avoid ReDoS vulnerabilities
684
+ const codeBlockStart = response.indexOf('```')
685
+ if (codeBlockStart !== -1) {
686
+ const codeBlockEnd = response.indexOf('```', codeBlockStart + 3)
687
+ if (codeBlockEnd !== -1) {
688
+ const codeBlockContent = response.slice(codeBlockStart + 3, codeBlockEnd)
689
+ // Skip optional "json" language identifier and whitespace
690
+ const arrayStart = codeBlockContent.indexOf('[')
691
+ if (arrayStart !== -1) {
692
+ const arrayJson = extractBalancedArray(codeBlockContent, arrayStart)
693
+ if (arrayJson) {
694
+ try {
695
+ return JSON.parse(arrayJson) as JsonPatchOperation[]
696
+ } catch {
697
+ // Continue to try other methods
698
+ }
677
699
  }
678
700
  }
679
701
  }
702
+ }
680
703
 
681
- if (endIdx > startIdx) {
682
- try {
683
- return JSON.parse(
684
- response.slice(startIdx, endIdx),
685
- ) as JsonPatchOperation[]
686
- } catch {
687
- // Fall through to empty array
688
- }
704
+ // Try to find raw JSON array by counting brackets
705
+ const arrayJson = extractBalancedArray(response, response.indexOf('['))
706
+ if (arrayJson) {
707
+ try {
708
+ return JSON.parse(arrayJson) as JsonPatchOperation[]
709
+ } catch {
710
+ // Fall through to empty array
689
711
  }
690
712
  }
691
713
 
@@ -775,6 +797,37 @@ export function applyJsonPatch(
775
797
  return result
776
798
  }
777
799
 
800
+ /** Keys that could be used for prototype pollution attacks. */
801
+ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
802
+
803
+ function isUnsafeKey(key: string): boolean {
804
+ return UNSAFE_KEYS.has(key)
805
+ }
806
+
807
+ /**
808
+ * Unescape a single JSON Pointer path segment according to RFC 6901.
809
+ * This ensures that checks for dangerous keys are applied to the
810
+ * effective property name, not the escaped form.
811
+ */
812
+ function unescapeJsonPointerSegment(segment: string): string {
813
+ return segment.replace(/~1/g, '/').replace(/~0/g, '~')
814
+ }
815
+
816
+ /**
817
+ * isSafePathSegment determines whether a JSON Pointer path segment is safe to use
818
+ * as a property key on an object. It rejects keys that are known to enable
819
+ * prototype pollution or that contain characters commonly used in special
820
+ * property notations.
821
+ */
822
+ function isSafePathSegment(segment: string): boolean {
823
+ // Normalize the segment as it will appear as a property key.
824
+ const normalized = unescapeJsonPointerSegment(String(segment))
825
+ if (isUnsafeKey(normalized)) return false
826
+ // Disallow bracket notation-style segments to avoid unexpected coercions.
827
+ if (normalized.includes('[') || normalized.includes(']')) return false
828
+ return true
829
+ }
830
+
778
831
  /** getValueAtPath retrieves a value at a JSON Pointer path. */
779
832
  function getValueAtPath(
780
833
  obj: Record<string, unknown>,
@@ -782,6 +835,15 @@ function getValueAtPath(
782
835
  ): unknown {
783
836
  let current: unknown = obj
784
837
  for (const part of parts) {
838
+ // Block prototype-pollution: reject __proto__, constructor, prototype
839
+ if (
840
+ part === '__proto__' ||
841
+ part === 'constructor' ||
842
+ part === 'prototype'
843
+ ) {
844
+ return undefined
845
+ }
846
+ if (!isSafePathSegment(part)) return undefined
785
847
  if (current === null || current === undefined) return undefined
786
848
  if (Array.isArray(current)) {
787
849
  const idx = parseInt(part, 10)
@@ -806,25 +868,49 @@ function setValueAtPath(
806
868
  let current: unknown = obj
807
869
  for (let i = 0; i < parts.length - 1; i++) {
808
870
  const part = parts[i]!
871
+ // Block prototype-pollution: reject __proto__, constructor, prototype
872
+ if (
873
+ part === '__proto__' ||
874
+ part === 'constructor' ||
875
+ part === 'prototype'
876
+ ) {
877
+ return
878
+ }
879
+ if (!isSafePathSegment(part)) {
880
+ // Avoid writing to dangerous or malformed prototype-related properties
881
+ return
882
+ }
809
883
  if (Array.isArray(current)) {
810
884
  const idx = parseInt(part, 10)
811
885
  if (current[idx] === undefined) {
812
886
  // Create intermediate object or array
813
887
  const nextPart = parts[i + 1]!
814
- current[idx] = /^\d+$/.test(nextPart) ? [] : {}
888
+ current[idx] = /^\d+$/.test(nextPart) ? [] : Object.create(null)
815
889
  }
816
890
  current = current[idx]
817
891
  } else if (typeof current === 'object' && current !== null) {
818
892
  const rec = current as Record<string, unknown>
819
893
  if (rec[part] === undefined) {
820
894
  const nextPart = parts[i + 1]!
821
- rec[part] = /^\d+$/.test(nextPart) ? [] : {}
895
+ rec[part] = /^\d+$/.test(nextPart) ? [] : Object.create(null)
822
896
  }
823
897
  current = rec[part]
824
898
  }
825
899
  }
826
900
 
827
901
  const lastPart = parts[parts.length - 1]!
902
+ // Block prototype-pollution: reject __proto__, constructor, prototype
903
+ if (
904
+ lastPart === '__proto__' ||
905
+ lastPart === 'constructor' ||
906
+ lastPart === 'prototype'
907
+ ) {
908
+ return
909
+ }
910
+ if (!isSafePathSegment(lastPart)) {
911
+ // Avoid writing to dangerous or malformed prototype-related properties
912
+ return
913
+ }
828
914
  if (Array.isArray(current)) {
829
915
  const idx = parseInt(lastPart, 10)
830
916
  current[idx] = value
@@ -843,6 +929,18 @@ function removeValueAtPath(
843
929
  let current: unknown = obj
844
930
  for (let i = 0; i < parts.length - 1; i++) {
845
931
  const part = parts[i]!
932
+ // Block prototype-pollution: reject __proto__, constructor, prototype
933
+ if (
934
+ part === '__proto__' ||
935
+ part === 'constructor' ||
936
+ part === 'prototype'
937
+ ) {
938
+ return
939
+ }
940
+ if (!isSafePathSegment(part)) {
941
+ // Avoid accessing dangerous prototype-related properties
942
+ return
943
+ }
846
944
  if (Array.isArray(current)) {
847
945
  current = current[parseInt(part, 10)]
848
946
  } else if (typeof current === 'object' && current !== null) {
@@ -853,6 +951,18 @@ function removeValueAtPath(
853
951
  }
854
952
 
855
953
  const lastPart = parts[parts.length - 1]!
954
+ // Block prototype-pollution: reject __proto__, constructor, prototype
955
+ if (
956
+ lastPart === '__proto__' ||
957
+ lastPart === 'constructor' ||
958
+ lastPart === 'prototype'
959
+ ) {
960
+ return
961
+ }
962
+ if (!isSafePathSegment(lastPart)) {
963
+ // Avoid deleting dangerous or malformed properties
964
+ return
965
+ }
856
966
  if (Array.isArray(current)) {
857
967
  const idx = parseInt(lastPart, 10)
858
968
  current.splice(idx, 1)
@@ -953,3 +1063,41 @@ export function parseJsonFromResponse<T = Record<string, unknown>>(
953
1063
  response,
954
1064
  )
955
1065
  }
1066
+
1067
+ /** buildJsonRepairPrompt creates a prompt asking the model to fix malformed JSON. */
1068
+ export function buildJsonRepairPrompt(
1069
+ malformedJson: string,
1070
+ errorMessage: string,
1071
+ schema: Record<string, FieldConfig>,
1072
+ ): string {
1073
+ const lines: string[] = []
1074
+
1075
+ lines.push(
1076
+ 'Your previous JSON output has a syntax error and cannot be parsed.',
1077
+ )
1078
+ lines.push('')
1079
+ lines.push(`Error: ${errorMessage}`)
1080
+ lines.push('')
1081
+ lines.push('The malformed JSON (may be truncated):')
1082
+ lines.push('```')
1083
+ lines.push(malformedJson.slice(0, 2000))
1084
+ if (malformedJson.length > 2000) {
1085
+ lines.push('... (truncated)')
1086
+ }
1087
+ lines.push('```')
1088
+ lines.push('')
1089
+ lines.push('Please output the COMPLETE, VALID JSON that matches this schema:')
1090
+ lines.push('```json')
1091
+
1092
+ // Build a simple schema description
1093
+ const schemaDesc: Record<string, string> = {}
1094
+ for (const [name, config] of Object.entries(schema)) {
1095
+ schemaDesc[name] = config.desc ?? zodTypeToString(config.type)
1096
+ }
1097
+ lines.push(JSON.stringify(schemaDesc, null, 2))
1098
+ lines.push('```')
1099
+ lines.push('')
1100
+ lines.push('Respond with ONLY the corrected JSON object, no explanation.')
1101
+
1102
+ return lines.join('\n')
1103
+ }
package/src/pipeline.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * DSTS SDK Pipeline orchestrator.
2
+ * ocpipe Pipeline orchestrator.
3
3
  *
4
4
  * Manages execution context, state, checkpointing, logging, retry logic, and sub-pipelines.
5
5
  */
package/src/predict.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * DSTS SDK Predict class.
2
+ * ocpipe Predict class.
3
3
  *
4
4
  * Executes a signature by generating a prompt, calling OpenCode, and parsing the response.
5
5
  */
@@ -20,6 +20,8 @@ import type {
20
20
  import { runAgent } from './agent.js'
21
21
  import {
22
22
  tryParseResponse,
23
+ extractJsonString,
24
+ buildJsonRepairPrompt,
23
25
  // jq-style patches
24
26
  buildPatchPrompt,
25
27
  buildBatchPatchPrompt,
@@ -47,8 +49,13 @@ export interface PredictConfig {
47
49
  correction?: CorrectionConfig | false
48
50
  }
49
51
 
52
+ type AnySignature = SignatureDef<
53
+ Record<string, FieldConfig>,
54
+ Record<string, FieldConfig>
55
+ >
56
+
50
57
  /** Predict executes a signature by calling an LLM and parsing the response. */
51
- export class Predict<S extends SignatureDef<any, any>> {
58
+ export class Predict<S extends AnySignature> {
52
59
  constructor(
53
60
  public readonly sig: S,
54
61
  public readonly config: PredictConfig = {},
@@ -73,7 +80,7 @@ export class Predict<S extends SignatureDef<any, any>> {
73
80
  // Update context with new session ID for continuity
74
81
  ctx.sessionId = agentResult.sessionId
75
82
 
76
- const parseResult = tryParseResponse<InferOutputs<S>>(
83
+ let parseResult = tryParseResponse<InferOutputs<S>>(
77
84
  agentResult.text,
78
85
  this.sig.outputs,
79
86
  )
@@ -89,7 +96,42 @@ export class Predict<S extends SignatureDef<any, any>> {
89
96
  }
90
97
  }
91
98
 
92
- // Parsing failed - attempt correction if enabled
99
+ // Check if this is a JSON parse error (malformed JSON, not schema validation)
100
+ const isJsonParseError =
101
+ parseResult.errors?.some(
102
+ (e) => e.code === 'json_parse_failed' || e.code === 'no_json_found',
103
+ ) ?? false
104
+
105
+ // Attempt JSON repair if enabled and we have a parse error
106
+ if (this.config.correction !== false && isJsonParseError) {
107
+ const rawJson = extractJsonString(agentResult.text)
108
+ const repairedResult = await this.repairJson(
109
+ rawJson ?? agentResult.text,
110
+ parseResult.errors?.[0]?.message ?? 'JSON parse failed',
111
+ ctx,
112
+ agentResult.sessionId,
113
+ )
114
+
115
+ if (repairedResult) {
116
+ // Re-parse the repaired response
117
+ parseResult = tryParseResponse<InferOutputs<S>>(
118
+ repairedResult,
119
+ this.sig.outputs,
120
+ )
121
+
122
+ if (parseResult.ok && parseResult.data) {
123
+ return {
124
+ data: parseResult.data,
125
+ raw: agentResult.text,
126
+ sessionId: agentResult.sessionId,
127
+ duration: Date.now() - startTime,
128
+ model: this.config.model ?? ctx.defaultModel,
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ // Parsing failed - attempt field correction if enabled and we have parsed JSON
93
135
  if (
94
136
  this.config.correction !== false &&
95
137
  parseResult.errors &&
@@ -130,6 +172,62 @@ export class Predict<S extends SignatureDef<any, any>> {
130
172
  )
131
173
  }
132
174
 
175
+ /** repairJson asks the model to fix malformed JSON. */
176
+ private async repairJson(
177
+ malformedJson: string,
178
+ errorMessage: string,
179
+ ctx: ExecutionContext,
180
+ sessionId: string,
181
+ ): Promise<string | null> {
182
+ const correctionConfig =
183
+ typeof this.config.correction === 'object' ? this.config.correction : {}
184
+ const maxRounds = correctionConfig.maxRounds ?? 3
185
+ const correctionModel = correctionConfig.model
186
+
187
+ for (let round = 1; round <= maxRounds; round++) {
188
+ console.error(
189
+ `\n>>> JSON repair round ${round}/${maxRounds}: fixing malformed JSON...`,
190
+ )
191
+
192
+ const repairPrompt = buildJsonRepairPrompt(
193
+ malformedJson,
194
+ errorMessage,
195
+ this.sig.outputs,
196
+ )
197
+
198
+ // Use same session so the model has context of what it was trying to output
199
+ const repairResult = await runAgent({
200
+ prompt: repairPrompt,
201
+ model: correctionModel ?? ctx.defaultModel,
202
+ sessionId: correctionModel ? undefined : sessionId,
203
+ agent: ctx.defaultAgent,
204
+ timeoutSec: 60,
205
+ })
206
+
207
+ // Try to parse the repaired JSON
208
+ const repairedJson = extractJsonString(repairResult.text)
209
+ if (repairedJson) {
210
+ try {
211
+ JSON.parse(repairedJson)
212
+ console.error(` JSON repair successful after ${round} round(s)!`)
213
+ return repairedJson
214
+ } catch (e) {
215
+ const parseErr = e as SyntaxError
216
+ console.error(` Repair attempt ${round} failed: ${parseErr.message}`)
217
+ malformedJson = repairedJson
218
+ errorMessage = parseErr.message
219
+ }
220
+ } else {
221
+ console.error(
222
+ ` Repair attempt ${round} failed: no JSON found in response`,
223
+ )
224
+ }
225
+ }
226
+
227
+ console.error(` JSON repair failed after ${maxRounds} rounds`)
228
+ return null
229
+ }
230
+
133
231
  /** correctFields attempts to fix field errors using same-session patches with retries. */
134
232
  private async correctFields(
135
233
  json: Record<string, unknown>,
@@ -239,7 +337,7 @@ export class Predict<S extends SignatureDef<any, any>> {
239
337
 
240
338
  // Input fields as JSON
241
339
  const inputsWithDescriptions: Record<string, unknown> = {}
242
- for (const [name, config] of Object.entries(this.sig.inputs) as [
340
+ for (const [name] of Object.entries(this.sig.inputs) as [
243
341
  string,
244
342
  FieldConfig,
245
343
  ][]) {
package/src/signature.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * DSTS SDK signature definition.
2
+ * ocpipe signature definition.
3
3
  *
4
4
  * Signatures declare input/output contracts for LLM interactions using Zod for validation.
5
5
  */
package/src/state.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * DSTS SDK state management.
2
+ * ocpipe state management.
3
3
  *
4
4
  * Provides base state types and helpers for checkpointable workflow state.
5
5
  */
package/src/testing.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * DSTS SDK testing utilities.
2
+ * ocpipe testing utilities.
3
3
  *
4
- * Provides mock backends and test helpers for unit testing DSTS components.
4
+ * Provides mock backends and test helpers for unit testing ocpipe components.
5
5
  */
6
6
 
7
7
  import type { RunAgentOptions, RunAgentResult, FieldConfig } from './types.js'
@@ -175,11 +175,12 @@ export function generateMockOutputs(
175
175
  case 'ZodObject':
176
176
  result[name] = {}
177
177
  break
178
- case 'ZodEnum':
178
+ case 'ZodEnum': {
179
179
  // Get first enum value via options property
180
180
  const enumType = config.type as { options?: readonly string[] }
181
181
  result[name] = enumType.options?.[0] ?? 'unknown'
182
182
  break
183
+ }
183
184
  default:
184
185
  result[name] = null
185
186
  }
package/src/types.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  /**
2
- * DSTS SDK shared types.
3
- *
4
- * Core type definitions for the Declarative Self-Improving TypeScript SDK.
2
+ * ocpipe shared types.
5
3
  */
6
4
 
7
5
  import type { z } from 'zod/v4'
@@ -131,14 +129,24 @@ export interface SignatureDef<
131
129
  }
132
130
 
133
131
  /** Infer the input type from a signature definition. */
134
- export type InferInputs<S extends SignatureDef<any, any>> =
135
- S extends SignatureDef<infer I, any> ?
132
+ export type InferInputs<
133
+ S extends SignatureDef<
134
+ Record<string, FieldConfig>,
135
+ Record<string, FieldConfig>
136
+ >,
137
+ > =
138
+ S extends SignatureDef<infer I, Record<string, FieldConfig>> ?
136
139
  { [K in keyof I]: z.infer<I[K]['type']> }
137
140
  : never
138
141
 
139
142
  /** Infer the output type from a signature definition. */
140
- export type InferOutputs<S extends SignatureDef<any, any>> =
141
- S extends SignatureDef<any, infer O> ?
143
+ export type InferOutputs<
144
+ S extends SignatureDef<
145
+ Record<string, FieldConfig>,
146
+ Record<string, FieldConfig>
147
+ >,
148
+ > =
149
+ S extends SignatureDef<Record<string, FieldConfig>, infer O> ?
142
150
  { [K in keyof O]: z.infer<O[K]['type']> }
143
151
  : never
144
152
 
@@ -165,7 +173,12 @@ export type CorrectionMethod = 'json-patch' | 'jq'
165
173
  export interface CorrectionConfig {
166
174
  /** Correction method to use (default: 'json-patch'). */
167
175
  method?: CorrectionMethod
168
- /** Use a different model for corrections (default: same model, same session). */
176
+ /**
177
+ * Use a different model for corrections.
178
+ * When specified, the correction runs in a new session (no context from the original model).
179
+ * When not specified, corrections reuse the original session so the model has context
180
+ * of what it was trying to output.
181
+ */
169
182
  model?: ModelConfig
170
183
  /** Maximum number of fields to attempt correcting per round (default: 5). */
171
184
  maxFields?: number
@@ -173,8 +186,16 @@ export interface CorrectionConfig {
173
186
  maxRounds?: number
174
187
  }
175
188
 
189
+ /** Error codes for field errors, enabling robust error type detection. */
190
+ export type FieldErrorCode =
191
+ | 'json_parse_failed'
192
+ | 'no_json_found'
193
+ | 'schema_validation_failed'
194
+
176
195
  /** A field-level error from schema validation. */
177
196
  export interface FieldError {
197
+ /** Error code for programmatic detection. */
198
+ code: FieldErrorCode
178
199
  /** The field path that failed (e.g., "issues.0.issue_type"). */
179
200
  path: string
180
201
  /** Human-readable error message. */