{"record":{"id":"7f6215b2efc60e53","repo":"nexu-io/open-design","slug":"unterminated-tag-in-live-artifact-template","errorCode":null,"errorMessage":"unterminated tag in live artifact template","messagePattern":"unterminated tag in live artifact template","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/live-artifacts/render.ts","lineNumber":118,"sourceCode":"}\n\nfunction interpolateScalars(fragment: string, resolve: BindingResolver): string {\n  return fragment.replace(TEMPLATE_INTERPOLATION, (_match, rawBinding: string) => resolve(rawBinding.trim()));\n}\n\n/** Index of the `>` that closes the tag opening at `start`, respecting quotes. */\nfunction findTagEnd(html: string, start: number): number {\n  let quote: string | null = null;\n  for (let i = start; i < html.length; i++) {\n    const ch = html[i];\n    if (quote) {\n      if (ch === quote) quote = null;\n      continue;\n    }\n    if (ch === '\"' || ch === \"'\") quote = ch;\n    else if (ch === '>') return i;\n  }\n  throw new Error('unterminated tag in live artifact template');\n}\n\n/** Index just past the `</tagName>` that matches the element opened at `openTagEnd`. */\n/**\n * Whether `index` falls inside an HTML comment. Comment text is authored\n * content: neither repeat directives nor same-tag tokens inside `<!-- -->`\n * count as structure anywhere the renderer scans. Returns the position just\n * past the comment so scanners can resume after it.\n */\nfunction insideComment(html: string, index: number): { resumeAt: number } | null {\n  const start = html.lastIndexOf('<!--', index);\n  if (start === -1) return null;\n  const end = html.indexOf('-->', start);\n  if (end !== -1 && end < index) return null;\n  return { resumeAt: end === -1 ? html.length : end + 3 };\n}\n\nfunction findElementEnd(html: string, tagName: string, openTagEnd: number): number {","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/live-artifacts/render.ts#L100-L136","documentation":"Thrown by findTagEnd() while scanning an HTML tag opening in a live artifact template (html_template_v1). The scanner walks character-by-character from the '<' at `start`, tracking single/double quote state, and expects to find a '>' that closes the opening tag. It throws when the string ends first — either because the tag literally never closes, or because an attribute's quote is never balanced so every subsequent '>' is swallowed as attribute content. This is a hard parse failure: the renderer cannot locate the element boundary, so it refuses to emit anything.","triggerScenarios":"A template.html containing an opening tag with no closing '>', e.g. `<div data-od-repeat=\"item in data.items\"` (truncated). Or an attribute whose quote is never closed, e.g. `<div title='oops>` — the quote stays open and the loop runs off the end of the string. Also triggered by a directive-bearing tag whose opening tag spans a malformed region the scanner cannot exit.","commonSituations":"Agent-authored template.html gets truncated mid-tag during streaming; an LLM emits a tag with an unbalanced quote when filling in attributes; hand-edited templates where the closing '>' was accidentally deleted; copy-pasting a fragment that ends inside a tag.","solutions":["Open the artifact's template.html and find the tag reported by the surrounding renderFragment scan; add the missing '>' to close the opening tag.","Balance every attribute quote — verify each opening '\"' or \"'\" has a matching closing quote before the tag's '>'.","If the template was produced by an agent run, regenerate the artifact so the streaming output is not truncated.","Run the template through an HTML linter/validator before persisting it as a live artifact."],"exampleFix":"// before (template.html)\n<ul data-od-repeat=\"item in data.items\" class=\"list\n  <li>{{item.label}}</li>\n</ul>\n// after\n<ul data-od-repeat=\"item in data.items\" class=\"list\">\n  <li>{{item.label}}</li>\n</ul>","handlingStrategy":"validation","validationCode":"// Reject malformed templates before persisting/rendering.\nfunction assertTagsClosed(html: string): void {\n  let quote: string | null = null;\n  let inTag = false;\n  for (let i = 0; i < html.length; i++) {\n    const ch = html[i];\n    if (inTag) {\n      if (quote) {\n        if (ch === quote) quote = null;\n      } else if (ch === '\"' || ch === \"'\") {\n        quote = ch;\n      } else if (ch === '>') {\n        inTag = false;\n      }\n      continue;\n    }\n    if (ch === '<') inTag = true;\n  }\n  if (inTag) throw new Error('template has an unterminated tag');\n}\n\nassertTagsClosed(input.templateHtml);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Validate template.html structure before writing the artifact (run a tag-balance check or an HTML parser).","Never truncate streamed template output mid-tag; finalize the stream before persisting.","Lint agent-authored templates in a review step before they reach the renderer."],"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"}