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
+32 -16
View File
@@ -6,11 +6,14 @@ import { FEEDBACK_RATING_SCALE } from "./feedbackRating.js";
* appear in a non-10 feedback comment, the reporter should include a
* `COMPOSITION_STRUCTURE:` block so maintainers can pattern-match against
* known bug families without receiving the composition ZIP. Matched
* case-insensitively against the raw comment.
* case-insensitively, word-bounded against the raw comment (so "black" fires
* on "black frame" but not "blackboard" / "no black frame at all").
*
* Keep the list short and unambiguous — false positives are cheap (a soft
* warn), false negatives just mean the reporter skips the structure block on
* a non-visual bug, which is fine.
* `"render"` intentionally omitted: `hyperframes render` is the CLI's primary
* command, so build/perf/hang reports mention it constantly and would drown
* the structure warning in false positives. Rely on the more specific tokens
* ("black", "blank", "flicker", "corrupt", "wrong frame") to identify actual
* visual defects.
*/
export const VISUAL_DEFECT_KEYWORDS: readonly string[] = [
"black",
@@ -19,7 +22,6 @@ export const VISUAL_DEFECT_KEYWORDS: readonly string[] = [
"wrong frame",
"blank",
"visual",
"render",
] as const;
/**
@@ -64,9 +66,13 @@ export function lintFeedbackComment(input: FeedbackLintInput): FeedbackLintWarni
const trimmed = comment?.trim();
if (!trimmed) return [];
// Marker checks are case-insensitive to match `mentionsVisualDefect`'s
// normalization. A reporter who writes `Repro command:` shouldn't get warned
// for compliance just because they lowercased the marker.
const upperTrimmed = trimmed.toUpperCase();
const warnings: FeedbackLintWarning[] = [];
if (!trimmed.includes(REPRO_MARKER)) {
if (!upperTrimmed.includes(REPRO_MARKER)) {
warnings.push({
code: "missing-repro-command",
message: [
@@ -80,7 +86,7 @@ export function lintFeedbackComment(input: FeedbackLintInput): FeedbackLintWarni
if (
rating <= COMPOSITION_STRUCTURE_RATING_CEILING &&
mentionsVisualDefect(trimmed) &&
!trimmed.includes(STRUCTURE_MARKER)
!upperTrimmed.includes(STRUCTURE_MARKER)
) {
warnings.push({
code: "missing-composition-structure",
@@ -96,16 +102,26 @@ export function lintFeedbackComment(input: FeedbackLintInput): FeedbackLintWarni
return warnings;
}
// Compile once. Word-boundary at both sides prevents "black" matching
// "blackboard", "blank" matching "blanket", "visual" matching "visualize".
// Plural forms (e.g. "flickers") won't match — accepted tradeoff for a soft
// warn: false negatives skip the nudge, false positives waste the reporter's
// attention.
function escapeRegex(input: string): string {
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
const VISUAL_DEFECT_REGEX = new RegExp(
`(^|[^A-Za-z0-9_])(?:${VISUAL_DEFECT_KEYWORDS.map(escapeRegex).join("|")})(?![A-Za-z0-9_])`,
"i",
);
/**
* Case-insensitive substring probe against `VISUAL_DEFECT_KEYWORDS`. Exposed
* for tests and reuse; keywords are matched anywhere in the comment (no word
* boundaries) since real reports mix them into free prose. False positives
* cost one soft warn, which is acceptable.
* Case-insensitive word-bounded probe against `VISUAL_DEFECT_KEYWORDS`.
* Exposed for tests and reuse. Word boundaries are enforced on both sides so
* partial-word false positives ("blackboard", "visualize", "corruptible")
* don't trigger the structure-block nudge.
*/
export function mentionsVisualDefect(comment: string): boolean {
const lower = comment.toLowerCase();
for (const kw of VISUAL_DEFECT_KEYWORDS) {
if (lower.includes(kw)) return true;
}
return false;
return VISUAL_DEFECT_REGEX.test(comment);
}