{"record":{"id":"4a91358830b0f8ba","repo":"nexu-io/open-design","slug":"invalid-array-segment-in-template-binding-path","errorCode":null,"errorMessage":"invalid array segment in template binding path: ${rawPath}","messagePattern":"invalid array segment in template binding path: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/live-artifacts/render.ts","lineNumber":61,"sourceCode":"    .replaceAll('\"', '&quot;')\n    .replaceAll(\"'\", '&#39;');\n}\n\n/**\n * A binding resolver for one scope. Given a trimmed binding path (e.g.\n * `data.title` or a loop variable path like `item.label`) it returns the\n * already-escaped scalar string to substitute, or throws for an unsupported\n * path. Loop scopes delegate non-matching heads (including `data.*`) to their\n * parent so global bindings keep working inside a repeat.\n */\ntype BindingResolver = (binding: string) => string;\n\nfunction walkPath(root: unknown, segments: string[], rawPath: string): unknown {\n  let current: unknown = root;\n  for (const segment of segments) {\n    if (current === null || current === undefined) return '';\n    if (Array.isArray(current)) {\n      if (!/^\\d+$/.test(segment)) throw new Error(`invalid array segment in template binding path: ${rawPath}`);\n      current = current[Number(segment)];\n      continue;\n    }\n    if (typeof current !== 'object') return '';\n    current = (current as Record<string, unknown>)[segment];\n  }\n  return current ?? '';\n}\n\nfunction readTemplatePath(dataJson: BoundedJsonObject, rawPath: string): unknown {\n  const segments = rawPath.split('.');\n  if (segments.shift() !== 'data') throw new Error(`unsupported template binding path: ${rawPath}`);\n  return walkPath(dataJson, segments, rawPath);\n}\n\nfunction scalarOrThrow(value: unknown, binding: string): string {\n  if (Array.isArray(value) || (value !== null && typeof value === 'object')) {\n    throw new Error(`template binding must resolve to a scalar: ${binding}`);","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/live-artifacts/render.ts#L43-L79","documentation":"Thrown by walkPath during template binding resolution when the current value is an Array but the next path segment is not a decimal integer (not /^\\d+$/). Arrays can only be indexed numerically inside a binding path; any non-numeric segment (e.g. a property name) on an array is a path/type mismatch and is rejected rather than silently returning empty, so the author knows the path disagrees with the data shape.","triggerScenarios":"Binding {{data.items.name}} where data.items is an array (should be data.items.0.name or a repeat loop); {{data.list.length}} on an array; any time a path treats an array element as an object before indexing into it.","commonSituations":"Model writes a property-style path over a list returned by the refresh source (e.g. public_github_repository_metric or git.summary); data shape changed (object became array) and the template wasn't updated; misunderstanding that arrays require numeric indices in this minimal DSL.","solutions":["If the value is a list you want to render, use the repeat directive: data-od-repeat=\"item in data.items\" then bind {{item.name}}.","If you need a single element, index it numerically: {{data.items.0.name}}.","Confirm the data shape in data.json matches the path; if the source switched from object to array, update the template or restructure the data."],"exampleFix":"// before (data.items is an array)\n<div>{{data.items.name}}</div>\n// after\n<div data-od-repeat=\"item in data.items\">{{item.name}}</div>","handlingStrategy":"type-guard","validationCode":"function isArrayIndex(seg: string): boolean { return /^\\d+$/.test(seg); }\nfunction resolveArray(root: unknown[], path: string[]): unknown {\n  let cur: unknown = root;\n  for (const seg of path) {\n    if (!Array.isArray(cur) || !/^\\d+$/.test(seg)) throw new Error('invalid array segment');\n    cur = cur[Number(seg)];\n  }\n  return cur;\n}","typeGuard":"function isNumericSegment(seg: string): boolean { return /^\\d+$/.test(seg); }","tryCatchPattern":null,"preventionTips":["Confirm data shape before authoring paths; arrays need numeric indices.","Use the repeat directive rather than indexing when you want every element.","Keep a fixture of data.json next to the template during authoring."],"tags":["template-binding","path-resolution","live-artifacts","validation"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}