From cee31000a72f05f44bcc988958bd2aa3b5f55da6 Mon Sep 17 00:00:00 2001 From: DavidBabinec Date: Fri, 31 Jul 2026 15:00:45 +0200 Subject: [PATCH] fix(import): report loop references the importer cannot resolve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `` — a table slug where a source id belongs — imported cleanly. The id was copied verbatim into props, `site_insert_html` reported success, `verify-docs` found the document intact, and at publish `loopPrefetch` turned the unregistered source into a well-formed `{ items: [], totalItems: 0 }`, indistinguishable from "this table has no rows". The section rendered as nothing and every gate passed, because the route still returned 200 with a unique title and one h1. `loopSourceRegistry` has had `has()`/`getOrThrow()` the whole time and nothing under `htmlImport` ever called either, so the shape `site_list_loop_sources` documents and the shape the importer accepts were checked against nothing in common. They now share the registry: the walk reports an unregistered source, a missing source, and a `data.rows` loop with no table — the second silent path, where `filters: {}` makes `dataRows` return empty just as quietly. These are warnings, not rejections. A paste of thirty elements should not be thrown away over one mistyped attribute, and the importer also serves arbitrary user-pasted HTML. What matters is that the caller is told at author time rather than finding an empty section in a screenshot later, so `site_insert_html` and `site_replace_node_html` return them alongside the ids they already report. Warnings live on the walk result rather than on `ImportFragment`, which is the structural shape callers hand back to the store. Also corrected two descriptions that promised behaviour the composer does not implement. `` is spliced away on page routes — the page's content replaces it — so `data-tag` only takes effect on entry routes, and a template that wants pages to have a `
` has to wrap the outlet itself. The module's own doc comment describes this polymorphism correctly; the tool description and system prompt did not, which is how a layout ends up with no main landmark on any public route. Test: a table slug, a missing id, and a missing table each warn with the fix in the message; a well-formed loop is silent; the node still imports; several bad loops in one payload all report. --- server/ai/tools/site/systemPrompt.ts | 2 +- server/ai/tools/site/writeTools.ts | 2 +- .../htmlImport/loopReferenceWarnings.test.ts | 65 +++++++++++ src/admin/pages/site/agent/executor.ts | 20 +++- src/core/htmlImport/importWarnings.ts | 103 ++++++++++++++++++ src/core/htmlImport/walkAndMap.ts | 28 ++++- 6 files changed, 209 insertions(+), 11 deletions(-) create mode 100644 src/__tests__/htmlImport/loopReferenceWarnings.test.ts create mode 100644 src/core/htmlImport/importWarnings.ts diff --git a/server/ai/tools/site/systemPrompt.ts b/server/ai/tools/site/systemPrompt.ts index ce1b61315..f2aabed72 100644 --- a/server/ai/tools/site/systemPrompt.ts +++ b/server/ai/tools/site/systemPrompt.ts @@ -63,7 +63,7 @@ Loops (repeated CMS/data lists): Templates (CMS layouts): - A template is a document/page that WRAPS other content. Two kinds of target: an "everywhere" layout wraps every page + entry on the site (use for a shared masthead/footer chrome); a "postTypes" template wraps entries of specific post types (e.g. each blog post). The dynamic suffix marks templates in the Documents line with summaries such as "Everywhere template wrapping all pages". -- The wrapped content flows into a single \`\` you place inside the template's HTML (via site_insert_html) — put it where the page/entry body should appear, with the template's chrome (header/nav/footer) around it. Use the neutral div form when the shared shell already owns \`
\`; omit data-tag only when this outlet itself should own the page's main landmark. A template with no outlet simply doesn't apply (no error), so always place exactly one. +- The wrapped content flows into a single \`\` you place inside the template's HTML (via site_insert_html) — put it where the page/entry body should appear, with the template's chrome (header/nav/footer) around it. On PAGE routes the outlet element itself is not rendered — the page's content is spliced in at that position — so \`data-tag\` there only matters for entry routes, where the outlet stays and wraps the entry body. Author the landmark yourself: wrap the outlet in \`
\` in the template's own HTML, or no public page route will have one. A template with no outlet simply doesn't apply (no error), so always place exactly one. - Create flow: build the chrome on a page with site_insert_html (including one \`\`), then call site_set_page_template(pageId, target, priority?). For a postTypes target, get valid slugs from site_list_post_types first. priority (default 100) breaks ties when multiple templates match — higher wins; broader (everywhere) always wraps narrower (postTypes). - site_clear_page_template(pageId) reverts a template to an ordinary page. Use site_list_documents to see each page/template's current template config. diff --git a/server/ai/tools/site/writeTools.ts b/server/ai/tools/site/writeTools.ts index c2a0910f3..ac5d1101a 100644 --- a/server/ai/tools/site/writeTools.ts +++ b/server/ai/tools/site/writeTools.ts @@ -85,7 +85,7 @@ const insertHtmlTool: AiTool = { execution: 'browser', requiredCapabilities: SITE_STRUCTURE_CAPS, description: - 'Insert semantic HTML as a subtree of editable nodes under an existing parent. Write structure as HTML (
,

, ,