fix(feedback-lint): tighten census value scoping and defect-keyword matching

Addresses C1-C7 from Rames's adversarial review + N2/N3 nits:

- C1 (positionFixed): inline probe now value-scopes to 'fixed' — previously
  fired on any position value (absolute, relative, sticky), producing a
  false-positive anatomy that would mislead maintainers pattern-matching
  the 'sub-comp + position:fixed capture' bug family.

- C2 (overflowHidden): symmetric fix — inline path now catches
  style='overflow: hidden' via the value-scoped probe, matching the
  <style>-tag branch. Also handles overflow-x/overflow-y variants.

- C3 (VISUAL_DEFECT_KEYWORDS): drop 'render' — CLI's primary command is
  'hyperframes render', so build/perf/hang reports were triggering an
  inappropriate COMPOSITION_STRUCTURE: nudge on the most common failure
  mode. Rely on the more specific tokens (black, blank, flicker, corrupt,
  wrong frame) to identify actual visual defects.

- C4 (mentionsVisualDefect): compile keywords into a word-bounded regex.
  'blackboard', 'blanket', 'visualize', 'corruptible' no longer false-
  positive. Accepted tradeoff: plural forms ('flickers') don't match.

- C5 (marker case-normalization): REPRO COMMAND: / COMPOSITION_STRUCTURE:
  checks now case-insensitive, matching mentionsVisualDefect's
  normalization. Reporters using 'Repro command:' or lowercase
  'composition_structure:' get credit for compliance.

- C6 (background/mask shorthand): inline branch previously required the
  longhand 'background-image:' / 'mask-image:' — style='background:
  url(bg.png)' silently returned false. Now checks both longhand AND
  shorthand-with-url() inline forms.

- C7 (usesGsap docstring): trim promise of data-gsap-* attribute scanning
  that detectGsap never implemented — attribute scan lives outside the
  <script>-only detection path.

- N2 (EMPTY_VALUES): include 'inherit', 'revert', 'revert-layer' — a
  style='position: inherit' is authored intent to defer, not authored
  intent to place.

- N3 (input size cap): early-exit to a zero census on HTML > 20 MB
  rather than feeding linkedom a hostile input. Not expected in normal
  usage; guard for future callers that might pass raw user uploads.

Extends the test locks: 6 new census tests (value-scoping, size cap) and
5 new lint tests (word-boundary rejections, render-noise rejections,
lowercase-marker acceptance). All existing tests unchanged in intent —
only the 'flickers' plural in one test updated to 'flicker' to reflect
the new word-boundary rule.

No behavior change to the wire path: lint is still soft-warn, census is
still never called from feedback.ts, no new dependencies.
This commit is contained in:
Via
2026-07-17 06:22:00 +00:00
parent 8f90fd9ec1
commit 490642b78a
4 changed files with 244 additions and 34 deletions
@@ -112,6 +112,60 @@ describe("buildCompositionCensus", () => {
expect(c.timelineShape.nested).toBe(true);
expect(c.timelineShape.subCompositionCount).toBe(2);
});
describe("value-scoped structural probes", () => {
it("positionFixed is false for inline position:absolute (name-vs-value scope)", () => {
const html = `<html><body><div data-composition-id="m" style="position: absolute"></div></body></html>`;
const c = buildCompositionCensus(html);
expect(c.structuralAttributes.positionFixed).toBe(false);
});
it("positionFixed is true for inline position:fixed", () => {
const html = `<html><body><div data-composition-id="m" style="position: fixed"></div></body></html>`;
const c = buildCompositionCensus(html);
expect(c.structuralAttributes.positionFixed).toBe(true);
});
it("overflowHidden catches inline overflow:hidden (symmetric with style-tag path)", () => {
const html = `<html><body><div data-composition-id="m" style="overflow: hidden"></div></body></html>`;
const c = buildCompositionCensus(html);
expect(c.structuralAttributes.overflowHidden).toBe(true);
});
it("overflowHidden is false for inline overflow:visible", () => {
const html = `<html><body><div data-composition-id="m" style="overflow: visible"></div></body></html>`;
const c = buildCompositionCensus(html);
expect(c.structuralAttributes.overflowHidden).toBe(false);
});
it("backgroundImage catches the inline shorthand `background: url(...)`", () => {
const html = `<html><body><div data-composition-id="m" style="background: url('bg.png') center"></div></body></html>`;
const c = buildCompositionCensus(html);
expect(c.structuralAttributes.backgroundImage).toBe(true);
});
it("maskImage catches the inline shorthand `mask: url(...)`", () => {
const html = `<html><body><div data-composition-id="m" style="mask: url('mask.svg')"></div></body></html>`;
const c = buildCompositionCensus(html);
expect(c.structuralAttributes.maskImage).toBe(true);
});
it("does not count `inherit` / `revert` as authored intent", () => {
const html = `<html><body><div data-composition-id="m" style="position: inherit; filter: revert"></div></body></html>`;
const c = buildCompositionCensus(html);
expect(c.structuralAttributes.positionFixed).toBe(false);
expect(c.structuralAttributes.filter).toBe(false);
});
});
it("short-circuits on HTML over the size cap without throwing", () => {
const huge = "a".repeat(21 * 1024 * 1024);
const c = buildCompositionCensus(huge);
// Guard-rail returns a zero census — no OOM, no crash.
expect(c.elementCensus.video).toBe(0);
expect(c.timelineShape.subCompositionCount).toBe(0);
expect(c.structuralAttributes.positionFixed).toBe(false);
});
});
describe("renderCompositionCensusBlock", () => {