{"record":{"id":"4312c6cebc3631fa","repo":"nexu-io/open-design","slug":"unbalanced-data-od-repeat-element-tagname","errorCode":null,"errorMessage":"unbalanced data-od-repeat element <${tagName}>","messagePattern":"unbalanced data-od-repeat element <(.+?)>","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/live-artifacts/render.ts","lineNumber":156,"sourceCode":"  re.lastIndex = openTagEnd;\n  let depth = 1;\n  let m: RegExpExecArray | null;\n  while ((m = re.exec(html))) {\n    const comment = insideComment(html, m.index);\n    if (comment) {\n      re.lastIndex = comment.resumeAt;\n      continue;\n    }\n    const tagEnd = findTagEnd(html, m.index);\n    if (m[1] === '/') {\n      depth -= 1;\n      if (depth === 0) return tagEnd + 1;\n    } else if (html[tagEnd - 1] !== '/') {\n      depth += 1;\n    }\n    re.lastIndex = tagEnd + 1;\n  }\n  throw new Error(`unbalanced data-od-repeat element <${tagName}>`);\n}\n\n/** Reads a `data-od-repeat` source path to its array, always from the data root. */\ntype ArrayReader = (arrayPath: string) => unknown[];\n\n/**\n * Render one template fragment against a binding scope, expanding\n * `data-od-repeat` elements left to right. Each repeat element is emitted\n * fully rendered (all its bindings resolved) so substituted data values are\n * never re-scanned by a later interpolation pass — a single-pass invariant\n * that keeps data-supplied `{{...}}`-looking text inert. One level only:\n * a `data-od-repeat` nested inside another is rejected, matching the\n * documented html_template_v1 contract.\n */\n/**\n * Locate the next REAL `data-od-repeat` directive at or after `from`. A match\n * is a directive only when it sits inside an element's open tag: the nearest\n * `<` to its left starts a named tag that has not closed yet. Literal","sourceCodeStart":138,"sourceCodeEnd":174,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/live-artifacts/render.ts#L138-L174","documentation":"Thrown by findElementEnd() when it cannot match the closing tag for a data-od-repeat element. The function tracks open/close depth of same-named tags (skipping comments) starting just after the opening tag; if depth never returns to zero before the string ends, the repeat element is considered unbalanced. The renderer needs an exact element boundary to slice the repeated body, so a missing or mismatched closing tag is fatal.","triggerScenarios":"A repeat element whose closing tag is missing: `<li data-od-repeat=\"item in data.items\">{{item.label}}</ul>` (closed with </ul> instead of </li>). Also when the closing tag name has different casing/spelling, or when a nested same-named element is itself unclosed and consumes the outer closer.","commonSituations":"Agent emits a repeat over <li> or <tr> but closes the wrong parent; refactoring a template and forgetting to rename the closing tag; copy-paste that drops a closing tag; mismatched casing like `<TR ...>` closed by `</tr>` is tolerated (case-insensitive regex) but a renamed tag like `<row>` closed by `</tr>` is not.","solutions":["Locate the data-od-repeat element for the reported tagName and add/fix its matching </tagName> closing tag.","Ensure the closing tag name exactly matches the opening tag name (spelling identical; dashes included).","If the body intentionally contains same-named children, confirm each child is itself self-closed or properly closed.","Wrap the body in a different tag name if you need repeat-over-repeat semantics (nested repeats are unsupported — see error 343)."],"exampleFix":"// before\n<tr data-od-repeat=\"row in data.rows\">\n  <td>{{row.a}}</td>\n</div>\n// after\n<tr data-od-repeat=\"row in data.rows\">\n  <td>{{row.a}}</td>\n</tr>","handlingStrategy":"validation","validationCode":"// Lightweight depth check: for each repeat tagName, ensure open/close counts match (ignoring self-closing).\nimport { REPEAT_DIRECTIVE } from './render'; // or inline the regex\n\nfunction assertRepeatElementsBalanced(html: string): void {\n  const re = /\\s*\\bdata-od-repeat\\s*=\\s*\"[^\"]*\"/gi;\n  let m: RegExpExecArray | null;\n  while ((m = re.exec(html))) {\n    const openIdx = html.lastIndexOf('<', m.index);\n    const name = /^<([A-Za-z][A-Za-z0-9_-]*)/.exec(html.slice(openIdx))?.[1];\n    if (!name) continue;\n    const open = (html.match(new RegExp(`<${name}\\\\b`, 'gi')) || []).length;\n    const close = (html.match(new RegExp(`</${name}\\\\s*>`, 'gi')) || []).length;\n    if (open !== close) throw new Error(`unbalanced repeat element <${name}>: ${open} open vs ${close} close`);\n  }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always pair a data-od-repeat opening tag with a matching </tagName> of the exact same name.","Validate tag balance on the repeat element specifically before persisting the template.","Avoid nesting same-named elements inside a repeat unless each is properly closed."],"tags":["live-artifacts","html-template","template-rendering","validation"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}