Files
hyperframes/packages/cli/src/utils/feedbackLint.ts
T
Via 490642b78a 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.
2026-07-17 06:22:00 +00:00

128 lines
5.0 KiB
TypeScript

import { FEEDBACK_RATING_SCALE } from "./feedbackRating.js";
/**
* Keywords that suggest the reporter is describing a visual defect (as opposed
* to a build failure, missing feature, or plain workflow friction). When these
* 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, word-bounded against the raw comment (so "black" fires
* on "black frame" but not "blackboard" / "no black frame at all").
*
* `"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",
"flicker",
"corrupt",
"wrong frame",
"blank",
"visual",
] as const;
/**
* Ratings that should mandate `COMPOSITION_STRUCTURE:` when the comment
* contains a visual-defect keyword. 7 and below covers "clearly broken" —
* 8-9 are usually "worked, but noticed a nit" which doesn't need the full
* structural anatomy.
*/
export const COMPOSITION_STRUCTURE_RATING_CEILING = 7;
const REPRO_MARKER = "REPRO COMMAND:";
const STRUCTURE_MARKER = "COMPOSITION_STRUCTURE:";
export interface FeedbackLintInput {
rating: number;
comment: string | undefined;
}
export interface FeedbackLintWarning {
code: "missing-repro-command" | "missing-composition-structure";
message: string;
}
/**
* Soft-warn lint on the `hyperframes feedback` comment body. Never blocks
* submission — some legitimate reports (a one-line "cloudrun quota bumped
* yesterday, fine now") won't fit the mold. The warning is just a nudge and
* a pointer to the auto-census helper.
*
* Rules:
* 1. `rating === 10` — no check. A perfect run doesn't need a repro packet.
* 2. Comment missing / empty — no check. `feedback --rating 6` with no
* comment is a valid quick vote; the maintainer sees rating drift without
* the reporter having to synthesize a fake repro.
* 3. Comment present + rating < 10 + no `REPRO COMMAND:` — warn.
* 4. Comment present + rating ≤ 7 + visual-defect keyword + no
* `COMPOSITION_STRUCTURE:` — warn (in addition to any #3 warning).
*/
export function lintFeedbackComment(input: FeedbackLintInput): FeedbackLintWarning[] {
const { rating, comment } = input;
if (rating === FEEDBACK_RATING_SCALE) return [];
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 (!upperTrimmed.includes(REPRO_MARKER)) {
warnings.push({
code: "missing-repro-command",
message: [
`Comment on a ${rating}/${FEEDBACK_RATING_SCALE} report is missing a "${REPRO_MARKER}" block —`,
"maintainers can't rerun the failure from a symptom summary alone.",
"See `references/preview-render.md` → feedback for the required packet shape.",
].join(" "),
});
}
if (
rating <= COMPOSITION_STRUCTURE_RATING_CEILING &&
mentionsVisualDefect(trimmed) &&
!upperTrimmed.includes(STRUCTURE_MARKER)
) {
warnings.push({
code: "missing-composition-structure",
message: [
`Comment describes a visual defect at ${rating}/${FEEDBACK_RATING_SCALE} but omits a`,
`"${STRUCTURE_MARKER}" block. Agents can auto-fill this via the composition-census helper`,
"(`buildCompositionCensus`/`renderCompositionCensusBlock` in `packages/cli/src/utils/compositionCensus.ts`)",
"so maintainers can pattern-match against known bug families without the composition ZIP.",
].join(" "),
});
}
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 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 {
return VISUAL_DEFECT_REGEX.test(comment);
}