diff --git a/docs/development/agent-workflow.md b/docs/development/agent-workflow.md
index 6cb9ad710..0fef67919 100644
--- a/docs/development/agent-workflow.md
+++ b/docs/development/agent-workflow.md
@@ -60,6 +60,8 @@ as Markdown links, including decoded attributes and fragment validation.
URL attributes remove ASCII tabs/newlines throughout and discard surrounding
ASCII control/space characters before resolution.
Iframe/embed sources and object data attributes are document references and retain Markdown-target anchor checks.
+Inline iframe srcdoc documents are traversed too, with their own fragment anchors.
+Explicit srcset attributes must contain at least one parsed candidate.
Image and media references require nonempty targets that resolve to files, not directories.
Image references check file existence without interpreting image fragments as
Markdown headings; document links keep anchor checks even when sharing a target.
diff --git a/tools/skills/agent-guidance-markdown.mjs b/tools/skills/agent-guidance-markdown.mjs
index 9d7460273..a13e84f18 100644
--- a/tools/skills/agent-guidance-markdown.mjs
+++ b/tools/skills/agent-guidance-markdown.mjs
@@ -92,12 +92,29 @@ function htmlNavigation(html, inspect = () => {}) {
node.tagName
),
});
+ if (node.tagName === 'iframe' && attribute.name === 'srcdoc') {
+ const embedded = htmlNavigation(attribute.value);
+ for (const reference of embedded.references)
+ references.push(
+ reference.target.startsWith('#') &&
+ !reference.embeddedAnchors
+ ? {
+ ...reference,
+ embeddedAnchors: embedded.anchors,
+ }
+ : reference
+ );
+ }
if (
['img', 'source'].includes(node.tagName) &&
attribute.name === 'srcset'
- )
- for (const candidate of parseSrcset(attribute.value))
+ ) {
+ const candidates = parseSrcset(attribute.value);
+ if (!candidates.length)
+ references.push({ target: '', image: true });
+ for (const candidate of candidates)
references.push({ target: candidate.url, image: true });
+ }
}
for (const child of node.childNodes ?? []) visit(child);
}
diff --git a/tools/skills/validate-agent-guidance.mjs b/tools/skills/validate-agent-guidance.mjs
index 6d4288654..3381d30d4 100644
--- a/tools/skills/validate-agent-guidance.mjs
+++ b/tools/skills/validate-agent-guidance.mjs
@@ -36,7 +36,7 @@ function within(root, path) {
async function validateReference(
rootDir,
source,
- { target, literal, image, unresolvedReference }
+ { target, literal, image, unresolvedReference, embeddedAnchors }
) {
if (unresolvedReference !== undefined)
return `${source}: unresolved Markdown reference "${unresolvedReference}"`;
@@ -53,6 +53,10 @@ async function validateReference(
} catch {
return `${source}: malformed local link: ${target}`;
}
+ if (embeddedAnchors && !path && !image)
+ return !anchor || embeddedAnchors.includes(anchor)
+ ? undefined
+ : `${source}: missing anchor "${anchor}" in iframe srcdoc`;
const absolute = path
? resolve(literal ? rootDir : dirname(resolve(rootDir, source)), path)
: resolve(rootDir, source);
diff --git a/tools/skills/validate-agent-guidance.test.mjs b/tools/skills/validate-agent-guidance.test.mjs
index e5ca63685..9292ab84b 100644
--- a/tools/skills/validate-agent-guidance.test.mjs
+++ b/tools/skills/validate-agent-guidance.test.mjs
@@ -1399,3 +1399,36 @@ for (const url of [
);
});
}
+
+for (const value of ['', ' ']) {
+ test(`empty srcset is rejected: ${JSON.stringify(value)}`, async (t) => {
+ assert.ok(
+ (await diagnostics(t, { 'AGENTS.md': `` }))
+ .length > 0
+ );
+ });
+}
+for (const content of [
+ "Guide",
+ "
",
+ "Guide",
+]) {
+ test(`srcdoc references are validated: ${content}`, async (t) => {
+ assert.ok(
+ (
+ await diagnostics(t, {
+ 'AGENTS.md': ``,
+ })
+ ).length > 0
+ );
+ });
+}
+test('srcdoc anchors are scoped and templates inert', async (t) => {
+ assert.deepEqual(
+ await diagnostics(t, {
+ 'AGENTS.md':
+ "
\">",
+ }),
+ []
+ );
+});