fix(lint): only judge audio-group membership in a file that declares some

Code review caught a false positive in the rule added an hour ago, at severity
error, on the studio's own output. `lintHyperframeHtml` sees ONE file, but
`timelineAudioGroupCreate` deliberately writes the bus into the active
composition and patches `data-audio-group` into each member's own file
("Written to the active composition file rather than beside the members"). So a
bus in index.html with members in compositions/voices.html was reported as
"an audio group no clip belongs to" plus the flatly wrong "No clip carries
`data-audio-group` at all" — about clips in a file the rule cannot see.
Reproduced end to end, then fixed.

The rule now returns early when the file declares no membership at all: absence
of THIS bus's id is only evidence when some other id is present. That keeps the
case it was written for — a typo on a member sitting beside its bus, which is
both the single-file hand-authored shape and what the studio writes when
everything lives in one composition — and the message says "Clips in this file"
so its scope is on the label.

Verified after: the sub-comp shape is silent, the typo shape still errors, and
audio-playground's genuinely orphaned `#narration` bus still reports. lint: 14
files, 536 tests.

The other two claims against these rules I checked and did not act on:
- matching only `audio[data-audio-group]` agrees with core, whose
  `resolveAudioGroups` queries exactly that and documents "a `data-audio-group`
  on a `<video>` is ignored". A video carrying it has no effective membership,
  so the bus really is empty. That the studio timeline nests such a child anyway
  is a studio/core disagreement, not this rule's error.
- `audio_carve_ungrouped_sources` treating an element-less group as a clip id is
  real but pre-existing on this branch, not from these rules.
This commit is contained in:
Vance Ingalls
2026-08-20 16:41:16 -07:00
parent 4f89082caa
commit ee91d3b768
2 changed files with 31 additions and 4 deletions
+20 -2
View File
@@ -566,8 +566,12 @@ describe("audio_group_no_members", () => {
const BUS = `<hf-audio-group id="voiceover" data-label="Voiceover" data-volume="0.4" const BUS = `<hf-audio-group id="voiceover" data-label="Voiceover" data-volume="0.4"
data-fx-chain='{"version":1,"nodes":[{"type":"peaking","id":"n1","params":{"frequency":250,"gain":-3,"q":1.2}}]}'></hf-audio-group>`; data-fx-chain='{"version":1,"nodes":[{"type":"peaking","id":"n1","params":{"frequency":250,"gain":-3,"q":1.2}}]}'></hf-audio-group>`;
it("errors on a bus no clip belongs to", async () => { it("errors on a bus no clip in the file belongs to", async () => {
const res = await lintHyperframeHtml(doc(BUS)); const res = await lintHyperframeHtml(
doc(
`${BUS}<audio id="s-1" src="s.wav" data-start="0" data-duration="2" data-audio-group="sfx"></audio>`,
),
);
const finding = res.findings.find((f) => f.code === "audio_group_no_members"); const finding = res.findings.find((f) => f.code === "audio_group_no_members");
expect(finding?.severity).toBe("error"); expect(finding?.severity).toBe("error");
expect(finding?.elementId).toBe("voiceover"); expect(finding?.elementId).toBe("voiceover");
@@ -597,6 +601,20 @@ describe("audio_group_no_members", () => {
// A bus with no id cannot be joined at all, and `resolveAudioGroups` skips it // A bus with no id cannot be joined at all, and `resolveAudioGroups` skips it
// when building its element map — a different mistake, not this rule's. // when building its element map — a different mistake, not this rule's.
// The rule can only speak about a file it can see all of. `lintHyperframeHtml`
// takes ONE file, and the studio's own group creation writes the bus into the
// active composition while patching `data-audio-group` into each member's own
// file (timelineAudioGroupCreate) — so a file holding a bus and no members at
// all is the normal cross-file shape, not a mistake.
it("stays quiet in a file that declares no members at all", async () => {
const res = await lintHyperframeHtml(
doc(
`${BUS}<div id="host" data-composition-src="compositions/voices.html" data-start="0" data-duration="10"></div>`,
),
);
expect(res.findings.some((f) => f.code === "audio_group_no_members")).toBe(false);
});
it("stays quiet for a bus with no id", async () => { it("stays quiet for a bus with no id", async () => {
const res = await lintHyperframeHtml( const res = await lintHyperframeHtml(
doc(`<hf-audio-group data-label="Nameless"></hf-audio-group>`), doc(`<hf-audio-group data-label="Nameless"></hf-audio-group>`),
+11 -2
View File
@@ -800,6 +800,15 @@ function findAudioGroupNoMembersFindings(ctx: LintContext): HyperframeLintFindin
.filter((id): id is string => Boolean(id)), .filter((id): id is string => Boolean(id)),
); );
// Only a file that declares SOME membership can be judged. `lintHyperframeHtml`
// sees one file, and the studio's own group creation writes the bus into the
// active composition while patching `data-audio-group` into each member's own
// file (`timelineAudioGroupCreate`) — so a file carrying a bus and no members
// at all is the ordinary cross-file shape. Firing there reported the studio's
// own output as an error, and said "No clip carries `data-audio-group` at all"
// about clips it simply could not see.
if (memberGroupIds.size === 0) return [];
const findings: HyperframeLintFinding[] = []; const findings: HyperframeLintFinding[] = [];
for (const tag of ctx.tags) { for (const tag of ctx.tags) {
if (tag.name !== "hf-audio-group") continue; if (tag.name !== "hf-audio-group") continue;
@@ -814,8 +823,8 @@ function findAudioGroupNoMembersFindings(ctx: LintContext): HyperframeLintFindin
const nearby = [...memberGroupIds].filter((id) => id !== elementId); const nearby = [...memberGroupIds].filter((id) => id !== elementId);
const suffix = const suffix =
nearby.length > 0 nearby.length > 0
? ` Clips name ${nearby.map((id) => `"${id}"`).join(", ")} instead.` ? ` Clips in this file name ${nearby.map((id) => `"${id}"`).join(", ")} instead.`
: " No clip carries `data-audio-group` at all."; : "";
findings.push({ findings.push({
code: "audio_group_no_members", code: "audio_group_no_members",
severity: "error", severity: "error",