diff --git a/packages/lint/src/rules/composition.test.ts b/packages/lint/src/rules/composition.test.ts
index 68881f4d0..dbf6eecf7 100644
--- a/packages/lint/src/rules/composition.test.ts
+++ b/packages/lint/src/rules/composition.test.ts
@@ -315,6 +315,50 @@ describe("composition rules", () => {
expect(finding).toBeUndefined();
});
+ it("does not flag one sub-composition mounted repeatedly with per-instance values", async () => {
+ // Regression: sub-compositions.md "Per-Instance Variables" documents
+ // mounting one source several times with different data-variable-values.
+ // That necessarily repeats the id, and the runtime rewrites repeated
+ // mounts to `id__hf1`/`id__hf2` so they coexist. Flagging it made the
+ // documented pattern an error with no correct way to satisfy it.
+ const html = `
+
+
+
+
+`;
+
+ const result = await lintHyperframeHtml(html);
+ expect(result.findings.find((f) => f.code === "duplicate_composition_id")).toBeUndefined();
+ });
+
+ it("still flags a real collision between a root and a non-mount element", async () => {
+ // The guard that keeps the exemption honest: skipping mounts must not
+ // blind the rule to the meta-versus-root collision it exists for, even
+ // when a legitimately repeated mount is present in the same file.
+ const html = `
+
+
+
+
+
+
+
+`;
+
+ const result = await lintHyperframeHtml(html);
+ const finding = result.findings.find((f) => f.code === "duplicate_composition_id");
+ expect(finding).toBeDefined();
+ expect(finding?.message).toContain("main");
+ });
+
it("ignores composition ids inside inert template content", async () => {
const html = `
diff --git a/packages/lint/src/rules/composition.ts b/packages/lint/src/rules/composition.ts
index 021826864..c2902f4c7 100644
--- a/packages/lint/src/rules/composition.ts
+++ b/packages/lint/src/rules/composition.ts
@@ -262,6 +262,16 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
const tagsByCompositionId = new Map();
for (const tag of tags) {
if (isInsideInertTemplate(tag, tags)) continue;
+ // A `data-composition-src` element is a MOUNT of a sub-composition, not a
+ // composition root, and sub-compositions.md documents mounting one source
+ // repeatedly with different `data-variable-values` to get per-instance
+ // variations. Those mounts legitimately share an id: the runtime rewrites
+ // repeated ones to `id__hf1`, `id__hf2` so they coexist. Counting them
+ // here made the documented pattern an error with no correct way to
+ // satisfy it. The collision this rule exists for -- a tag carrying
+ // the root's id, per its own fixHint -- is unaffected, since that tag has
+ // no `data-composition-src`.
+ if (readAttr(tag.raw, "data-composition-src")) continue;
const compositionId = readDecodedAttr(tag.raw, "data-composition-id");
if (!compositionId || compositionId.trim().length === 0) continue;