mit einem Klick
gerrit-frontend-engineering
// Provides guidance and best practices on Polygerrit UI development, frontend architecture, and TypeScript/JS coding standards in Gerrit.
// Provides guidance and best practices on Polygerrit UI development, frontend architecture, and TypeScript/JS coding standards in Gerrit.
Proofreads and suggests structural improvements for Git commit messages to ensure style guide compliance, completeness, and accuracy.
Provides rules, patterns, and best practices for code hygiene, formatting, downstream plugin dependencies, and release operations in Gerrit.
Provides rules, patterns, and best practices for Gerrit backend system logic, Java APIs, performance, and correctness.
| name | gerrit-frontend-engineering |
| description | Provides guidance and best practices on Polygerrit UI development, frontend architecture, and TypeScript/JS coding standards in Gerrit. |
Welcome to the Frontend Engineering & UI Development guide. This repository serves as the authoritative source of tribal knowledge for our UI architecture, born from historical refactoring efforts, critical performance optimizations, and the ongoing necessity to prevent recurrent regression of known failure modes. By codifying these engineering standards, we ensure that incoming engineers can confidently navigate the complexities of our frontend ecosystem without falling into legacy traps, introducing unverified UI states, or triggering silent runtime failures.
This guide enforces strict architectural boundaries across the entire UI development lifecycle. It mandates rigorous state encapsulation within the Lit framework, uncompromising TypeScript type safety, and highly resilient client-server integrations. It further standardizes hermetic UI testing methodologies, unifies our CSS design systems, and outlines strict strategies for client-side performance profiling.
Adherence to these principles guarantees structural consistency and system reliability. Whether you are building dynamic web components, integrating complex REST API endpoints, or optimizing data payloads for AI contexts and telemetry, this documentation establishes the foundational constraints required to ship a performant, predictable, and scalable user interface.
| Chapter Theme / Title | Scope & Objective |
|---|---|
| **Lit Framework Idioms & State | Enforce strict Lit framework idioms |
| : Encapsulation** : by leveraging declarative rendering, : | |
| : : reactive property encapsulation, and : | |
| : : native lifecycle hooks. Avoid : | |
| : : imperative DOM manipulation and : | |
| : : properly isolate transient UI status : | |
| : : from the core data models. : | |
| **TypeScript Strictness & Type | This domain governs the strict |
| : Safety** : enforcement of TypeScript type safety : | |
| : : by explicitly forbidding unsafe : | |
| : : casting and blanket compiler : | |
| : : suppressions. It mandates precise : | |
| : : component modeling, strict interface : | |
| : : adherence, and proper access : | |
| : : modifiers to eliminate silent runtime : | |
| : : failures and unverified states. : | |
| **Hermetic Testing & Visual | This chapter mandates the strict |
| : Regression** : isolation of unit tests, : | |
| : : comprehensive visual validation using : | |
| : : full shadow DOM snapshots, and the : | |
| : : centralization of test data : | |
| : : generation to guarantee hermetic : | |
| : : execution and prevent false-positive : | |
| : : assertions. : | |
| **Client-Side Performance & | This theme governs the optimization |
| : Telemetry** : of client-side performance by : | |
| : : enforcing strict telemetry on : | |
| : : synchronous CPU-bound operations and : | |
| : : minimizing network latency. It : | |
| : : mandates the caching of asynchronous : | |
| : : API promises, the derivation of state : | |
| : : from existing payloads, and the rigid : | |
| : : bounding of background data requests. : | |
| **CSS Architecture & Design System | This domain governs the consistent |
| : Consistency** : application of styles across the UI, : | |
| : : mandating the use of content-driven : | |
| : : layout techniques, externalized : | |
| : : custom property configurations for : | |
| : : visual assets, and declarative Lit : | |
| : : directives over imperative inline : | |
| : : styling. : | |
| API Integration & Error Handling | This chapter governs the resilient |
| : : integration of frontend logic with : | |
| : : REST APIs. It establishes strict : | |
| : : constraints for maintaining backend : | |
| : : payload parity, explicitly modeling : | |
| : : structural variances, and enforcing : | |
| : : centralized error handling over : | |
: : localized try...catch blocks. : | |
| **AI Context & Telemetry Payload | This domain governs the client-side |
| : Optimization** : lifecycle and optimization of data : | |
| : : structures sent to AI agents and : | |
| : : telemetry pipelines. It strictly : | |
| : : dictates payload deduplication : | |
| : : strategies, transparent AI token : | |
| : : constraint surfacing, and rigorous : | |
| : : parsing validation to prevent silent : | |
| : : context loss. : |
Context: Enforce strict Lit framework idioms by leveraging declarative rendering, reactive property encapsulation, and native lifecycle hooks. Avoid imperative DOM manipulation and properly isolate transient UI status from the core data models.
| Rule ID | Principle / | Priority | Primary Symptom / Trap |
: : Constraint : : :
| :-------- | :----------------- | :------- | :------------------------------- |
| T1-01 | Separation of Data | High | Reusing a data property to hold |
: : and UI State : : UI loading or error text, which :
: : Variables : : corrupts the data being passed :
: : : : to child components or the :
: : : : clipboard. :
| T1-02 | Explicit Boolean | High | Inspecting the DOM via |
: : Properties over : : this.children in Lit element :
: : Dynamic Slot : : methods to determine if a named :
: : Detection : : slot element was provided by the :
: : : : parent. :
| T1-03 | Declarative Event | Medium | Attaching DOM event listeners |
: : Listeners in Lit : : imperatively within the :
: : Templates : : component constructor. :
| T1-04 | Declarative | Medium | Chaining ternary operators |
: : Conditional CSS : : inside a template string to :
: : via classMap : : build a class list. :
| T1-05 | Centralized | Medium | Defining a custom manager object |
: : Periodic : : or singleton strictly tied to :
: : LitElement Updates : : one specific UI component type :
: : via Generic : : for periodic updates. :
: : Utility : : :
| T1-06 | Declarative | Medium | Manually invoking setAttribute |
: : Attribute : : or removeAttribute within a Lit :
: : Reflection in Lit : : lifecycle method to sync DOM :
: : Components : : properties. :
| T1-07 | Idempotent | Medium | Firing a tracking event inside |
: : Telemetry : : updated purely based on :
: : Reporting in Lit : : conditional presence, without a :
: : Component : : state flag acknowledging it :
: : Lifecycle Hooks : : fired. :
| T1-08 | Strict Truthiness | High | Relying on optional chaining |
: : Checks for : : length checks to represent empty :
: : Asynchronous Array : : or missing data. :
: : Data : : :
| T1-09 | Pre-computing | Medium | A helper method called in |
: : Derived State in : : render() that parses raw :
: : Lit Lifecycle : : strings into arrays on every :
: : Methods : : invocation. :
| T1-10 | Returning | Medium | Using an empty template literal |
: : nothing for : : or omitting a return when a :
: : Empty Templates in : : condition fails in the template. :
: : Lit : : :
| T1-11 | Single-Pass | Medium | Assigning values to @state() |
: : Initialization of : : or @property() fields inside :
: : Reactive : : the firstUpdated hook. :
: : Properties : : :
| T1-12 | Guarding | High | Executing timeout handlers or |
: : Asynchronous State : : asynchronous DOM updates without :
: : Updates : : confirming the component remains :
: : Post-Disconnection : : in the DOM structure. :
| T1-13 | Use Lit classMap | Medium | Concatenating ternary operators |
: : Directive for : : inside the class attribute :
: : Conditional CSS : : string of a Lit HTML template. :
: : Classes : : :
| T1-14 | Strict Lit State | High | Using the @property decorator |
: : Encapsulation : : for variables that represent :
: : : : internal component state (like :
: : : : data lists or loading spinners). :
| T1-15 | Declarative | Medium | Controlling element visibility |
: : Component : : using CSS class conditionals. :
: : Visibility : : :
: : Toggling : : :
| T1-16 | Immutable Lit Form | High | Mutating form state fields |
: : State Resets : : directly without triggering a :
: : : : reference change for Lit to :
: : : : detect. :
Rule: Never overload core data variables with transient UI status strings. You must use dedicated status properties to manage UI state separately from the underlying data model.
What: Do not overload core data variables with transient UI status strings (e.g., 'Loading...', 'Error'). Use dedicated status properties to manage UI state separately from the underlying data model.
Applies To: Lit components managing asynchronous operations and displaying resulting data alongside loading or error states.
Why: A
generatedPasswordproperty was overloaded to temporarily hold 'Generating...' or 'Failed to generate'. This caused the associated 'Copy to clipboard' component to copy the error or status text instead of a valid password. Failing to adhere to this typically results in Invalid Data Copied.
Trap 1: Reusing a data property to hold UI loading or error text, which corrupts the data being passed to child components or the clipboard.
Don't:
this._generatedPassword = 'Generating...';
this.restApiService.generatePassword().then(newPassword => {
this._generatedPassword = newPassword ?? 'Failed to generate';
});
Do:
this.status = 'Generating...';
this.restApiService.generatePassword().then(newPassword => {
if (newPassword) {
this.generatedPassword = newPassword;
this.status = undefined;
} else {
this.status = 'Failed to generate';
}
});
Rule: Always control conditional layout wrappers using explicit boolean properties mapped to the component API. Never dynamically query the DOM for the presence of assigned slots.
What: Control the rendering of conditional layout wrappers using explicit boolean properties mapped to the component's API, rather than dynamically querying the DOM for the presence of assigned slots.
Applies To: Lit components featuring conditional layout containers that wrap
<slot>elements (e.g., search bars with optional leading icons).Why: Dynamically checking
hasNamedSlotby iterating overthis.childrenwas computationally brittle and caused rendering bugs where structural elements (like search icons) were accidentally removed or created unintended 'ghost spacing'. Failing to adhere to this typically results in Layout Regression / Missing Elements.
Trap 1: Inspecting the DOM via this.children in Lit element methods to
determine if a named slot element was provided by the parent.
Don't:
private hasNamedSlot(name: string): boolean {
return Array.from(this.children).some(
el => el.getAttribute('slot') === name
);
}
render() {
return this.hasNamedSlot('leading-icon') ? html`<div><slot name="leading-icon"></slot></div>` : nothing;
}
Do:
@property({type: Boolean})
showLeadingIcon = false;
render() {
return this.showLeadingIcon ? html`<div><slot name="leading-icon"></slot></div>` : nothing;
}
Rule: Always bind event listeners declaratively directly within the component's
render()template. Never imperatively attach listeners usingthis.addEventListenerin the constructor.What: Bind event listeners declaratively directly within the component's
render()template using@eventsyntax, rather than imperatively attaching them usingthis.addEventListenerin the constructor.Applies To: LitElement initialization and user interaction event handling.
Why: Imperatively adding listeners via the constructor disconnects the logic from the declarative template, risks memory leaks if not cleaned up, leaves inline documentation orphaned, and triggers automated code health warnings. Failing to adhere to this typically results in Structural Anti-Pattern / Orphaned Context.
Trap 1: Attaching DOM event listeners imperatively within the component constructor.
Don't:
constructor() {
super();
// BAD: Imperative binding
this.addEventListener('mousedown', e => this.handleMouseDown(e));
}
Do:
override render() {
// GOOD: Declarative binding directly in the Lit template
return html`
<div class="menu" @mousedown=${this.handleMenuMouseDown}>
<div class="menu-item">${this.hoverCardText}</div>
</div>
`;
}
Rule: Must use the Lit
classMapdirective for applying conditional CSS classes in templates. Avoid manual string interpolation with ternary operators.What: Use the Lit
classMapdirective for applying conditional CSS classes in templates instead of manual string interpolation with ternary operators.Applies To: Lit Framework templates (
render()methods).Why: Manual string interpolation for classes is difficult to read and prone to whitespace concatenation errors, which leads to incorrectly applied or missed CSS selectors during dynamic state changes. Failing to adhere to this typically results in Malformed Class Strings / UI Bugs.
Trap 1: Chaining ternary operators inside a template string to build a class list.
Don't:
<div class="diffContainer ${this.shownSidebar ? 'sidebarOpen' : ''} ${this.file?.diffs_too_expensive_to_compute ? 'hidden' : ''}">
Do:
<div class=${classMap({
diffContainer: true,
sidebarOpen: this.shownSidebar,
hidden: !!this.file?.diffs_too_expensive_to_compute
})}>
Rule: Always register components requiring timer-based periodic re-rendering with a centralized update manager. Never implement individual
setIntervalloops inside isolated components.What: Components requiring timer-based periodic re-rendering must register with a centralized
PeriodicUpdateManagerrather than implementing individualsetIntervaland lifecycle cleanup logic inside the component.Applies To: LitElements displaying time-sensitive data (e.g., relative dates, "time ago" formatters).
Why: Individual date formatter components initially implemented their own object literal manager or interval timers. This violated the separation of concerns and led to memory leaks if components failed to clean up their specific timers upon disconnection. Failing to adhere to this typically results in Memory Leaks / Duplicated Timer Logic.
Trap 1: Defining a custom manager object or singleton strictly tied to one specific UI component type for periodic updates.
Don't:
export const dateFormatterManager = {
formatters: new Set<GrDateFormatter>(),
register(formatter) { /* set interval to call requestUpdate */ }
}
Do:
// In periodic-update-util.ts
export class PeriodicUpdateManager<T extends LitElement> {
constructor(private readonly refreshIntervalMs: number) {}
register(component: T) { /* generic interval logic */ }
}
// In component
override connectedCallback() {
super.connectedCallback();
dateFormatterManager.register(this);
}
Rule: Must use Lit's
@property({reflect: true})decorator to synchronize a component's property state with DOM attributes. Never manually manipulate attributes viasetAttributewithin lifecycle methods.What: Use Lit's @property({reflect: true}) decorator to automatically synchronize a component's property state with its corresponding DOM attributes, replacing manual DOM manipulation calls.
Applies To: Lit web components, styling hooks, and state management.
Why: A custom icon wrapper was manually setting and removing an attribute inside the
willUpdatelifecycle method to apply CSS selectors, violating declarative state management principles. Failing to adhere to this typically results in Imperative DOM Manipulation.
Trap 1: Manually invoking setAttribute or removeAttribute within a Lit lifecycle method to sync DOM properties.
Don't:
override willUpdate() {
if (this.icon) {
this.setAttribute('custom', '');
} else {
this.removeAttribute('custom');
}
}
Do:
@property({type: Boolean, reflect: true})
custom = false;
override willUpdate() {
this.custom = this.icon ? true : false;
}
Rule: Must guard telemetry interactions fired during the
updated()lifecycle hook with a dedicated state flag. Never allow subsequent, unrelated property changes to trigger duplicate reporting.What: Telemetry interactions fired during the
updated()lifecycle hook must be guarded by a dedicated state flag to prevent duplicate reporting triggered by subsequent, unrelated property changes.Applies To: Lit components executing side effects (like analytics or impression tracking) within the
updated()loop.Why: When streaming AI responses, the component's state rapidly updated. Without an idempotent guard flag, the system generated multiple telemetry events for the exact same interaction every time the state re-rendered. Failing to adhere to this typically results in Duplicate Impression Reporting.
Trap 1: Firing a tracking event inside updated purely based on conditional
presence, without a state flag acknowledging it fired.
Don't:
override updated(changedProperties: PropertyValues) {
if (this.message()?.responseComplete) {
this.reportSuggestionsShown();
}
}
Do:
private reportedSuggestionsShown = false;
override updated(changedProperties: PropertyValues) {
if (!this.reportedSuggestionsShown && this.message()?.responseComplete) {
this.reportSuggestionsShown();
this.reportedSuggestionsShown = true;
}
}
Rule: Always explicitly check for null or undefined before evaluating the
.lengthproperty of asynchronous array data. Never rely solely on optional chaining length checks for truthiness.What: When determining the fallback UI state based on asynchronous array data (like API responses), explicitly check for null/undefined before checking the
.lengthproperty.Applies To: Lit components conditionally rendering UI elements based on the loaded state of arrays.
Why: Checking
array?.length === 0fails when the array is stillundefined, becauseundefined === 0resolves tofalse, causing the application to skip rendering the fallback UI during the loading or uninitialized state. Failing to adhere to this typically results in Missing Fallback UI.
Trap 1: Relying on optional chaining length checks to represent empty or missing data.
Don't:
if (this.repoLabels?.length === 0) {
return this.renderDefaultParameterInputField();
}
Do:
if (!this.repoLabels || this.repoLabels.length === 0) {
return this.renderDefaultParameterInputField();
}
Rule: Always pre-compute derived state within the
willUpdatelifecycle method or a dedicated observer. Never process strings or execute heavy array computations directly inside therender()loop.What: Avoid processing strings or doing heavy array computations directly inside the
render()method or helper template methods. Instead, reactively compute derived state (e.g., parsing a string into an array) within thewillUpdatelifecycle method or a dedicated observer.Applies To: Lit components dealing with data transformation before rendering.
Why: Dynamic computation inside render cycles degrades performance and muddles template readability. Helper methods that executed
.split()and regex operations on strings were running repeatedly on every re-render. Failing to adhere to this typically results in Redundant Computations.
Trap 1: A helper method called in render() that parses raw strings into
arrays on every invocation.
Don't:
private getParameters(): string[] {
if (this.parameters) return this.parameters;
if (this.parameterStr?.trim()) {
return this.parameterStr.trim().split(/\s+/);
}
return [];
}
render() {
const params = this.getParameters();
// ...
}
Do:
// Handle the calculation inside `willUpdate` reacting to changes in `this.parameterStr`
willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has('parameterStr')) {
this.parameters = this.parameterStr?.trim() ? this.parameterStr.trim().split(/\s+/) : [];
}
}
nothing for Empty Templates in LitRule: Must explicitly return the
nothingsentinel value instead of an empty template literal when rendering conditionally empty elements.What: In Lit templates, explicitly return the
nothingsentinel value instead of an empty template literal when rendering conditionally empty elements.Applies To: Lit components, specifically conditional rendering blocks.
Why: Historically, empty template instances (html``) were returned for false conditions, which created unnecessary markers in the DOM, slightly degrading rendering efficiency and DOM cleanliness. Failing to adhere to this typically results in DOM Clutter.
Trap 1: Using an empty template literal or omitting a return when a condition fails in the template.
Don't:
// BAD: Returning empty template
render() {
if (!this.show) return html``;
return html`<div>Content</div>`;
}
Do:
// GOOD: Returning nothing
import {nothing} from 'lit';
render() {
if (!this.show) return nothing;
return html`<div>Content</div>`;
}
Rule: Always initialize base reactive properties during construction or via bound property updates. Never use the
firstUpdatedlifecycle hook for initial assignment.What: Base reactive properties must be initialized during construction or via bound property updates rather than using the
firstUpdatedlifecycle hook, which triggers a redundant second render cycle.Applies To: Lit components, specifically lifecycle hooks (
constructor,willUpdate,firstUpdated).Why: Assigning derived URL states inside
firstUpdatedtriggered immediate, unnecessary re-renders, hurting initial paint performance. Failing to adhere to this typically results in Redundant Re-rendering.
Trap 1: Assigning values to @state() or @property() fields inside the
firstUpdated hook.
Don't:
// BAD: Triggers a second render cycle immediately after the first
override firstUpdated() {
this.hostUrl = window.location.origin;
}
Do:
// GOOD: Reactively bound to updates before render
override willUpdate(changedProperties: PropertyValues) {
if (!this.hostUrl) {
this.hostUrl = window.location.origin;
}
}
Exceptions: Properties that strictly depend on measuring DOM dimensions or child element readiness after layout.
Rule: Always explicitly verify
this.isConnectedbefore executing asynchronous tasks or debounced updates. Never execute callbacks that mutate state during DOM teardown.What: Components that schedule asynchronous tasks or debounced updates must explicitly verify
this.isConnectedbefore executing work to prevent mutations during DOM teardown.Applies To: Event handlers, async callbacks, and debounced routines in Lit web components.
Why: Property updates triggered
willUpdatehooks even afterdisconnectedCallbackhad run, scheduling new background tasks for components that were no longer attached to the document. Failing to adhere to this typically results in Memory Leaks.
Trap 1: Executing timeout handlers or asynchronous DOM updates without confirming the component remains in the DOM structure.
Don't:
// BAD: Running debounced task without verifying connection
updateSuggestions() {
this.scheduleDebounceTask();
}
Do:
// GOOD: Short-circuiting if disconnected
updateSuggestions() {
if (!this.isConnected) return;
this.scheduleDebounceTask();
}
Rule: Must use Lit's
classMapdirective for managing multiple conditional CSS classes. Never use manual string concatenation or ternary chaining within class attributes.What: Use Lit's
classMapdirective for managing multiple conditional CSS classes rather than manual string interpolation.Applies To: Lit component templates rendering conditional classes based on component state.
Why: Historically, manual string concatenation for multiple conditional CSS classes led to messy, error-prone template structures that were difficult to read and maintain. Failing to adhere to this typically results in Unreadable/Error-Prone Templates.
Trap 1: Concatenating ternary operators inside the class attribute string of
a Lit HTML template.
Don't:
class="context-chip ${this.isSuggestion ? 'suggested-chip' : ''} ${this.isCustomAction ? 'custom-action-chip' : ''}"
Do:
class=${classMap({'context-chip': true, 'suggested-chip': this.isSuggestion, 'custom-action-chip': this.isCustomAction})}
Rule: Always isolate internal component variables that drive UI re-renders using the
@statedecorator. Never use@propertyfor internal state that is not intended to be configured via HTML attributes.What: Internal component variables that drive UI re-renders but are not intended to be configured via HTML attributes must use the
@statedecorator instead of@property.Applies To: All Lit-based Web Components.
Why: Component logic was exposing internal data retrieval statuses (like loading state or fetched lists) as public
@propertyattributes. This polluted the component's public API surface and allowed external DOM manipulation to improperly overwrite internal component state. Failing to adhere to this typically results in State Leakage.
Trap 1: Using the @property decorator for variables that represent internal
component state (like data lists or loading spinners).
Don't:
// BAD: Internal state exposed as an attribute
@property({type: Boolean})
_loading = true;
@property({type: Array})
submitRequirements?: SubmitRequirementInfo[];
Do:
// GOOD: Internal state isolated using @state
@state()
loading = true;
@state()
submitRequirements?: SubmitRequirementInfo[];
Rule: Always handle conditional rendering of DOM elements using Lit's declarative
whenornothingdirectives. Never dynamically apply CSS classes that setdisplay: noneto toggle visibility.What: Conditional rendering of DOM elements must be handled using Lit's declarative
whenornothingdirectives within the HTML template, rather than dynamically applying CSS classes that setdisplay: none.Applies To: All Lit templates, particularly for loading states and conditional sections.
Why: Loading states were previously managed by dynamically assigning a
.loadingCSS class to an element, which relied on external stylesheet rules to hide/show the node. This made the UI state harder to reason about and bypassed Lit's native DOM reconciliation. Failing to adhere to this typically results in Layout Shifts / DOM Bloat.
Trap 1: Controlling element visibility using CSS class conditionals.
Don't:
// BAD: CSS-driven visibility
render() {
return html`
<table class="${this.loading ? 'loading' : ''}">
<!-- rows -->
</table>
`;
}
// Relying on: .loading #target { display: none; }
Do:
// GOOD: Declarative Lit rendering directives
render() {
return html`
<tbody>
${when(
this.loading,
() => html`<tr><td>Loading...</td></tr>`,
() => html`<!-- Render data rows -->`
)}
</tbody>
`;
}
Rule: Always assign a newly constructed object reference to the state property when resetting form state or clearing dialog inputs. Never mutate the existing object's properties in-place.
What: When resetting form state or clearing dialog inputs in Lit, assign a newly constructed object reference to the state property rather than mutating the existing object's properties in-place.
Applies To: Lit form components and dialogs with complex object state (
@state).Why: Form dialogs failed to clear properly after creating a new item because the state object was mutated in place or partially reset, resulting in stale UI data where fields appeared populated but submitted empty values. Failing to adhere to this typically results in Stale UI Data.
Trap 1: Mutating form state fields directly without triggering a reference change for Lit to detect.
Don't:
// BAD: In-place mutation does not consistently trigger a full re-render
handleCreateCancel() {
this.newRequirement.name = '';
this.newRequirement.description = '';
this.dialog.close();
}
Do:
// GOOD: Provide a new object reference to trigger full reactive updates
private getEmptyRequirement() {
return { name: '', description: '' };
}
handleCreateCancel() {
this.newRequirement = this.getEmptyRequirement();
this.dialog.close();
}
classMap dynamically consume centralized
structural CSS classes and shared UI styles.updated() natively trigger telemetry interaction
payloads that require idempotent guards to prevent data bloat.Context: This domain governs the strict enforcement of TypeScript type safety by explicitly forbidding unsafe casting and blanket compiler suppressions. It mandates precise component modeling, strict interface adherence, and proper access modifiers to eliminate silent runtime failures and unverified states.
| Rule ID | Principle / Constraint | Priority | Primary Symptom / |
: : : : Trap :
| :-------- | :------------------------------- | :------- | :----------------- |
| T2-01 | Targeted Type Casting over Broad | High | Suppressing all |
: : Error Suppression : : TypeScript :
: : : : compiler errors on :
: : : : a line just to :
: : : : bypass a private :
: : : : visibility check :
: : : : in a unit test. :
| T2-02 | Removal of Underscore Prefixes | Medium | Prefixing reactive |
: : for Reactive Properties : : Lit properties :
: : : : with an underscore :
: : : : to denote private :
: : : : state. :
| T2-03 | Elimination of TypeScript | High | Suppressing the |
: : Compiler Suppressions in Unit : : compiler error to :
: : Tests : : mutate a private :
: : : : property in the :
: : : : test setup. :
| T2-04 | Utilizing TypeScript Utility | High | Constructing an |
: : Types over Unsafe Casting : : object with :
: : : : missing properties :
: : : : and masking the :
: : : : error by casting :
: : : : it as the full :
: : : : interface type. :
| T2-05 | Prototype Methods over Arrow | Medium | Using an arrow |
: : Function Properties : : function assigned :
: : : : to a variable :
: : : : inside the class :
: : : : body. :
| T2-06 | Elimination of Unsafe any Type | High | Suppressing |
: : Casts : : TypeScript errors :
: : : : or forcibly :
: : : : casting objects to :
: : : : any when dynamic :
: : : : types don't :
: : : : strictly align. :
| T2-07 | Testing Private State | Medium | Forcibly accessing |
: : Encapsulation Rules : : class internals :
: : : : using string-keyed :
: : : : bracket notation :
: : : : in test files. :
| T2-08 | Eliminate Unsafe 'any' Type | Medium | Casting function |
: : Casting in Test Stubs : : arguments to any :
: : : : within a mock's :
: : : : custom callback :
: : : : function. :
| T2-09 | Suppression of Private Member | Medium | Casting the class |
: : Access in Tests via : : reference or :
: : @ts-expect-error : : global object to :
: : : : any to bypass :
: : : : TypeScript's :
: : : : visibility and :
: : : : presence checks. :
| T2-10 | Enforce Type Contracts Over | Medium | Adding |
: : Redundant Runtime Checks : : .filter(Boolean) :
: : : : or explicit :
: : : : undefined checks :
: : : : on an array that :
: : : : is strictly typed :
: : : : as containing only :
: : : : defined objects. :
| T2-11 | Explicit TypeScript Access | Medium | Prefixing internal |
: : Modifiers : : class methods or :
: : : : properties with an :
: : : : underscore while :
: : : : leaving them :
: : : : functionally :
: : : : public. :
| T2-12 | Idiomatic Boolean Casting | Medium | Casting a |
: : : : potentially :
: : : : undefined boolean :
: : : : to a strict :
: : : : boolean using the :
: : : : nullish coalescing :
: : : : operator. :
Rule: Always use targeted type casts (e.g.,
as any) to bypass specific visibility constraints in unit tests rather than silencing the entire line with@ts-expect-error.What: Replace blanket
@ts-expect-errordirectives with targeted type casts (e.g.,(element as any)) when attempting to access private component methods or properties in unit tests.Applies To: TypeScript unit tests interacting with encapsulated component logic.
Why: Using
// @ts-expect-errorsuppresses all TypeScript errors on the subsequent line. Historically, this masked actual bugs like typos in test assertions (e.g., a typo inassert.isFalse), rendering the tests unreliable. Failing to adhere to this typically results in Masked Bugs / Silent Failures.
Trap 1: Suppressing all TypeScript compiler errors on a line just to bypass a private visibility check in a unit test.
Don't:
// BAD: Masks all TS errors, including typos in the assert statement.
// @ts-expect-error
assert.isFalse(element.hasAiComments());
Do:
// GOOD: Bypasses visibility selectively while maintaining strict type checking on the assertion.
assert.isFalse((element as any).hasAiComments());
Rule: Never use leading underscores for Lit element properties; strictly enforce private state using TypeScript access modifiers.
What: Do not use leading underscores for Lit element properties. Rely on TypeScript
privateaccess modifiers to encapsulate internal state instead of naming conventions.Applies To: All LitElement
@propertyand@statedeclarations.Why: Leading underscores were heavily used in the legacy Polymer implementation to denote privacy, but they violate current TypeScript strictness standards and style guidelines for the modernized PolyGerrit UI. Failing to adhere to this typically results in Style Guide Violation.
Trap 1: Prefixing reactive Lit properties with an underscore to denote private state.
Don't:
@property({type: String})
_passwordUrl: string | null = null;
Do:
@property({type: String})
passwordUrl: string | null = null;
Rule: Must never bypass compiler checks to modify test internals; explicitly elevate visibility of the required property and document the exception.
What: Unit tests must not bypass compiler checks using @ts-expect-error to test internal logic. Instead, widen the visibility of the target property or method from private to public/internal, and document it with a comment.
Applies To: TypeScript unit test suites and component class files (e.g., Lit components).
Why: Developers were using @ts-expect-error directives to suppress compiler errors when assigning or calling private component members in tests. This masked actual type regressions from the TS compiler. Failing to adhere to this typically results in Type Safety Bypass.
Trap 1: Suppressing the compiler error to mutate a private property in the test setup.
Don't:
// In test file:
// @ts-expect-error
element.docsBaseUrl = 'https://docs.com/';
// @ts-expect-error
assert.equal(element.computeHelpUrl(), '...');
Do:
// In component file:
// private but used in test
public docsBaseUrl = '';
// In test file (no suppressions):
element.docsBaseUrl = 'https://docs.com/';
assert.equal(element.computeHelpUrl(), '...');
Rule: Always construct precise subsets of interfaces using utility types like
Pick<T, K>rather than forcibly blinding the compiler viaas Type.What: When creating objects that fulfill only a specific subset of an interface's requirements, construct the correct type signature using TypeScript utility types (e.g., Pick<T, K>) rather than using 'as Type' type assertions to blind the compiler.
Applies To: TypeScript data transformations, API response mapping, and component state variables.
Why: A subset of a LabelDefinitionInfo object was generated and aggressively typed using
as LabelDefinitionInfo. This misled consumers of the data about which properties were actually populated, creating potential runtime hazards. Failing to adhere to this typically results in Unsafe Type Assertion.
Trap 1: Constructing an object with missing properties and masking the error by casting it as the full interface type.
Don't:
const partial = {
name: 'LabelName',
values: { '+1': '' }
} as LabelDefinitionInfo;
Do:
const partial: Pick<LabelDefinitionInfo, 'name'> & Pick<LabelDefinitionInfo, 'values'> = {
name: 'LabelName',
values: { '+1': '' }
};
Rule: Always define class behaviors as standard prototype methods to avoid the excess instantiation overhead caused by arrow function properties.
What: Define class behaviors using standard class methods rather than assigning arrow functions as class properties to optimize memory usage.
Applies To: TypeScript class definitions across the application (Providers, Services, Models).
Why: Assigning arrow functions directly as class properties caused a new instance of the function to be created in memory for every instantiation of the class, unnecessarily increasing memory overhead. Failing to adhere to this typically results in Excessive Memory Overhead.
Trap 1: Using an arrow function assigned to a variable inside the class body.
Don't:
export class LabelSuggestionsProvider {
getSuggestions = (
predicate: string,
expression: string
): Promise<AutocompleteSuggestion[]> => {
// logic
};
}
Do:
export class LabelSuggestionsProvider {
getSuggestions(
predicate: string,
expression: string
): Promise<AutocompleteSuggestion[]> {
// logic
}
}
Exceptions: Arrow functions are acceptable if the method is passed around as
a callback and strictly requires preserving the this lexical binding without
manual .bind(this).
any Type CastsRule: Never use the
anytype; enforce strict bounds using concrete interfaces or rely onunknownfor safe downcasting.What: The
anytype must be strictly avoided. Use explicit interfaces, strict types, orunknownfor downcasting, eliminating@ts-expect-errorand@typescript-eslint/no-explicit-any.Applies To: Global TypeScript codebase, particularly component state assignments, plugin configurations, and test setups.
Why: Developer convenience led to pervasive use of
anytype casting, bypassing the compiler and masking structural mismatches that caused uncaught runtime exceptions when interfaces evolved. Failing to adhere to this typically results in Type Erasure.
Trap 1: Suppressing TypeScript errors or forcibly casting objects to any
when dynamic types don't strictly align.
Don't:
// BAD: Bypassing type safety
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.then((element: any) => {
assert.strictEqual(element, module);
})
Do:
// GOOD: Using unknown or explicit interfaces
.then((element: unknown) => {
assert.strictEqual(element, module);
})
Rule: Must never pierce class boundaries using bracket notation (
['property']) in tests; natively expose and document properties required for testing.What: Do not bypass private property access via bracket notation (
element['privateProp']) in tests. If a property must be exposed for testing, remove theprivatemodifier and document it with a standard comment.Applies To: Component state definitions and their respective test suites.
Why: Engineers were circumventing class lexical scope constraints by using bracket notation to set private component states in tests. This violated the TypeScript style guide and evaded static analysis tools. Failing to adhere to this typically results in Encapsulation Violation.
Trap 1: Forcibly accessing class internals using string-keyed bracket notation in test files.
Don't:
// BAD: Bypassing private scope
element['stages'] = [{ condition: 'status:open' }];
Do:
// GOOD: Remove private, add explicit documentation, and use dot notation
// In component:
@state() // private but used in tests
stages: Stage[] = [];
// In test:
element.stages = [{ condition: 'status:open' }];
Rule: Never substitute actual types with
anyin mock definitions; enforce accurate mock function signatures or useunknown.What: Avoid using the
anytype when defining function arguments in stub callbacks (e.g., SinoncallsFake). Use the actual mocked type orunknown.Applies To: Sinon stubs and mock definitions within unit tests.
Why: Using the 'any' type bypassed the TypeScript compiler's type checking, allowing signature mismatches between the mock and the actual implementation to silently pass. Failing to adhere to this typically results in Silent Type Mismatches.
Trap 1: Casting function arguments to any within a mock's custom callback
function.
Don't:
sinon.stub(Obj, 'method').callsFake(function (this: Obj, account: any) { ... })
Do:
sinon.stub(Obj, 'method').callsFake(function (this: Obj, account: unknown) { ... })
Rule: Always apply
@ts-expect-errorto suppress targeted visibility issues in mocks rather than annihilating all type validation by casting toany.What: When accessing private or static members in unit tests for mocking purposes, use the
@ts-expect-errordirective instead of casting the entire class or object toany.Applies To: Unit test setup scripts needing to stub private, protected, or unexported properties on classes or global objects.
Why: Casting objects to 'any' completely stripped all type checking for subsequent operations. Utilizing
@ts-expect-errormaintains the type context, ensuring the test fails at compile-time if the underlying property's visibility or type changes in the future. Failing to adhere to this typically results in Complete Type Loss.
Trap 1: Casting the class reference or global object to any to bypass
TypeScript's visibility and presence checks.
Don't:
// BAD: Overrides all type checking
const libLoader = (GrImageViewer as any).libLoader;
(window as any).resemble = sinon.stub();
Do:
// GOOD: Suppresses visibility error but retains underlying type
// @ts-expect-error
const libLoader = GrImageViewer.libLoader;
// @ts-expect-error
window.resemble = sinon.stub();
Rule: Must never litter code with redundant falsy checks for arrays/objects explicitly typed as defined; fix non-compliant test data upstream.
What: Do not add redundant runtime filtering or null-checks for conditions that the TypeScript definitions explicitly forbid. Fix the upstream source or test data instead.
Applies To: Business logic and data processing on strictly typed models.
Why: Adding defensive runtime checks for strictly-typed data degraded readability and masked underlying flaws in test setups that were passing invalid, poorly-mocked data into functions. Failing to adhere to this typically results in Masked Upstream Bugs / Cluttered Logic.
Trap 1: Adding .filter(Boolean) or explicit undefined checks on an array
that is strictly typed as containing only defined objects.
Don't:
// Method strictly accepts: changes: ChangeInfo[]
async sync(changes: ChangeInfo[]) {
// BAD: Redundant runtime check
const validChanges = changes.filter(c => c && isChangeInfo(c));
const basicChanges = new Map(validChanges.map(c => [c.id, c]));
}
Do:
// GOOD: Trust the contract and fix failing tests upstream
async sync(changes: ChangeInfo[]) {
const basicChanges = new Map(changes.map(c => [c.id, c]));
}
Exceptions: External payloads lacking robust typing across integration boundaries.
Rule: Always codify class visibility through strict
private,protected, orpublicmodifiers instead of falling back on leading underscore conventions.What: Class members and methods intended for internal use must be enforced using the TypeScript
privateaccess modifier rather than relying on the legacy_(underscore) naming convention.Applies To: TypeScript classes and Web Component definitions.
Why: The codebase contained legacy properties like
_loadingand methods without explicit access modifiers, which failed to utilize the TypeScript compiler to prevent external dependencies from coupling to internal implementation details. Failing to adhere to this typically results in Broken Encapsulation.
Trap 1: Prefixing internal class methods or properties with an underscore while leaving them functionally public.
Don't:
// BAD: Relying on naming conventions for privacy
@state()
_loading = true;
renderBoolean() { ... }
Do:
// GOOD: Enforcing privacy with TypeScript modifiers
@state()
private loading = true;
private renderCheckmark() { ... }
Rule: Always cast optional or truthy/falsy values using the idiomatic double-negation operator (
!!) rather than the nullish coalescing operator (?? false).What: To cast an optionally undefined or truthy/falsy value to a strict boolean, use the double-negation operator (
!!) instead of the nullish coalescing operator (?? false).Applies To: TypeScript logic handling optional object properties or API responses.
Why: During permission evaluation,
access?.is_owner ?? falsewas flagged during review as unidiomatic, and the codebase was aligned to use standard double-negation for boolean casts. Failing to adhere to this typically results in Unidiomatic Code.
Trap 1: Casting a potentially undefined boolean to a strict boolean using the nullish coalescing operator.
Don't:
// BAD: Verbose and unidiomatic casting
this.isProjectOwner = access?.is_owner ?? false;
Do:
// GOOD: Concise, standard boolean cast
this.isProjectOwner = !!access?.is_owner;
Context: This chapter mandates the strict isolation of unit tests, comprehensive visual validation using full shadow DOM snapshots, and the centralization of test data generation to guarantee hermetic execution and prevent false-positive assertions.
| Rule ID | Principle / | Priority | Primary Symptom / Trap |
: : Constraint : : :
| :-------- | :----------------- | :------- | :------------------------------- |
| T3-01 | Strict DOM Query | High | Querying for an element and |
: : Assertions in : : manually asserting its existence :
: : Testing : : using an assertion library. :
| T3-02 | Shadow DOM Event | Medium | Relying exclusively on the |
: : Property Fallbacks : : element property which may :
: : for Tests : : remain empty in test fixtures. :
| T3-03 | Comprehensive | High | Checking the length of |
: : Shadow DOM : : elements matching a query :
: : Snapshot : : selector to verify UI rendering. :
: : Assertions : : :
| T3-04 | Centralized Test | Medium | Manually creating partial data |
: : Data Generation : : structures in tests and using :
: : via Factory : : type casting to satisfy the :
: : Helpers : : compiler. :
| T3-05 | Isolation of | Medium | Adding a visualDiff snapshot |
: : Visual Regression : : assertion inside standard :
: : Tests : : logical unit tests. :
| T3-06 | Awaiting | High | Triggering a UI update and |
: : Asynchronous : : immediately asserting on the :
: : Render Cycles : : resulting DOM without waiting :
: : Before Assertions : : for the Lit rendering engine :
: : : : lifecycle. :
| T3-07 | DRY Centralization | Medium | Duplicating 5+ lines of UI |
: : of Shared Test : : interaction setup across :
: : Setup Logic : : multiple test cases. :
| T3-08 | Avoid | Medium | Assigning a random, dynamic |
: : Monkey-Patching : : property to the component being :
: : Test-Only : : tested to track mock responses :
: : Properties on : : or internal states. :
: : Component : : :
: : Instances : : :
| T3-09 | Explicit Mocking | High | Allowing the component to |
: : of External : : natively load its third-party :
: : Network Assets in : : script dependencies during a :
: : Tests : : basic unit test fixture setup. :
| T3-10 | Strict Limits on | High | Increasing the Mocha test suite |
: : Test Timeouts for : : timeout inside a failing :
: : Visual Regressions : : screenshot test to force it to :
: : : : pass. :
| T3-11 | Realistic | High | Rendering a generic template |
: : Component : : without required inputs and :
: : Initialization in : : asserting the shadow DOM is :
: : Tests : : empty. :
| T3-12 | Descriptive and | Medium | Naming a test based on an |
: : Contextual Test : : abstract concept rather than the :
: : Case Naming : : behavioral condition. :
| T3-13 | Production-Aligned | Medium | Defining static mock data for |
: : Visual Regression : : visual tests that includes :
: : Mock Data : : properties normally filtered out :
: : : : by the production environment. :
Rule: Always use the
queryAndAssertutility to locate and verify DOM elements simultaneously in unit tests.What: Use the
queryAndAssertutility to locate DOM elements in unit tests instead of usingquerycoupled with manual truthiness assertions or optional chaining.Applies To: Frontend Web Component testing; specifically LitElement test fixtures interacting with the DOM.
Why: Using a standard
querythat returnsnullwhen an element is missing causes crypticTypeErrors later in the test execution. Asserting immediately provides clear, fail-fast test reporting. Failing to adhere to this typically results in Test TypeErrors / Cryptic Failures.
Trap 1: Querying for an element and manually asserting its existence using an assertion library.
Don't:
const warning = query(element, '.expensiveDiff');
assert.isOk(warning);
assert.include(warning.textContent, 'Diff too expensive');
Do:
const warning = queryAndAssert(element, '.expensiveDiff');
assert.include(warning.textContent, 'Diff too expensive');
Trap 2: Using optional chaining to silently swallow null elements when checking class lists.
Don't:
const diffContainer = query(element, '.diffContainer');
assert.isTrue(diffContainer?.classList.contains('hidden'));
Do:
const diffContainer = queryAndAssert(element, '.diffContainer');
assert.isTrue(diffContainer.classList.contains('hidden'));
Rule: Must implement explicit attribute fallbacks when reading custom event properties that fail to synchronize in hermetic test environments.
What: When handling custom event properties (like
valuefrom custommd-outlined-selectcomponents) that fail to reliably synchronize to the target's property in the testing environment, safely fallback to reading thevalueattribute.Applies To: Lit element event handlers dealing with custom web components and executed in hermetic testing environments.
Why: Automated tests simulating user selections in
md-outlined-selectwere failing because the testing framework failed to read the value from the event target property, requiring an explicit fallback to the attribute. Failing to adhere to this typically results in False Negative Test Failures.
Trap 1: Relying exclusively on the element property which may remain empty in test fixtures.
Don't:
@change=${(e: Event) => {
this.selectedLabelForVote = (e.target as HTMLSelectElement).value;
}}
Do:
@change=${(e: Event) => {
// TODO: Remove reading from attribute once test env issue is fixed
this.selectedLabelForVote =
((e.target as HTMLSelectElement).value ||
(e.target as HTMLSelectElement).getAttribute('value')) ?? '';
}}
Rule: Never use shallow DOM assertions; always validate UI structures using
assert.shadowDom.equal.What: UI component tests must use
assert.shadowDom.equalto validate the entire rendered structure against an expected HTML snapshot, rather than asserting shallow DOM properties.Applies To: Frontend UI unit testing of Lit web components.
Why: During refactoring, comprehensive DOM checks were accidentally replaced with shallow element counts, creating a blind spot where regressions in structural layout, attributes, and text content could slip past CI. Failing to adhere to this typically results in Missed UI Regressions.
Trap 1: Checking the length of elements matching a query selector to verify
UI rendering.
Don't:
// BAD: Shallow DOM assertion
const flowElements = element.shadowRoot!.querySelectorAll('.flow');
assert.equal(flowElements.length, 2);
Do:
// GOOD: Full Shadow DOM snapshot assertion
assert.shadowDom.equal(
element,
/* HTML */ `
<div class="container">
<div class="flow">...</div>
<div class="flow">...</div>
</div>
`
);
Rule: Always construct mock data structures using centralized factory helpers configured with
Partial<T>overrides.What: Avoid manually instantiating large mock data payloads. Utilize centralized factory functions that accept
Partial<T>and provide sensible defaults, eliminating manual type assertions.Applies To: Frontend unit tests and mock data setup blocks.
Why: Tests repeatedly hardcoded large data objects, making the test suite brittle to schema changes and requiring explicit type casting (
as FlowInfo) to bypass compiler complaints about missing fields. Failing to adhere to this typically results in Brittle Tests.
Trap 1: Manually creating partial data structures in tests and using type casting to satisfy the compiler.
Don't:
// BAD: Manual object creation with casting
const flow = {
uuid: 'flow1',
owner: {name: 'owner1'},
created: '2025-01-01T10:00:00.000Z' as Timestamp,
} as FlowInfo;
Do:
// GOOD: Using a test data generator with Partial overrides
const flow = createFlow({
uuid: 'flow1',
owner: {name: 'owner1', _account_id: 1 as AccountId},
});
Rule: Must isolate DOM snapshot and screenshot operations into parallel
_screenshot_test.tsfiles.What: Visual regression (screenshot) tests utilizing DOM snapshots must reside in a separate
_screenshot_test.tsfile rather than being appended to standard unit test files.Applies To: Test directory structure and files utilizing
visualDiff.Why: Combining fast unit tests with slow visual diffing tests bloated unit execution times and complicated testing step separation in the CI pipeline. Failing to adhere to this typically results in CI Bottleneck.
Trap 1: Adding a visualDiff snapshot assertion inside standard logical unit tests.
Don't:
visualDiff test blocks into the standard
gr-[component]_test.ts file alongside business logic tests.Do:
gr-[component]_screenshot_test.ts dedicated
exclusively to visual assertions.Rule: Always
await element.updateCompleteprior to evaluating DOM nodes following a UI state change.What: Tests asserting UI state changes must strictly
await element.updateCompletebefore querying DOM elements or their properties, avoiding false positive evaluations.Applies To: Lit web component unit tests.
Why: Tests were erroneously passing because asynchronous state updates lacked
await, causing assertions to execute and pass before the updated component render cycle actually fired. Failing to adhere to this typically results in False Positive Tests.
Trap 1: Triggering a UI update and immediately asserting on the resulting DOM without waiting for the Lit rendering engine lifecycle.
Don't:
// BAD: Synchronous assertion after state change
element.renderInOrder([{path: 'p2'}]);
assert.equal(reviewStub.callCount, 1);
Do:
// GOOD: Awaiting update completion
await element.renderInOrder([{path: 'p2'}]);
await element.updateComplete;
assert.equal(reviewStub.callCount, 1);
Rule: Must extract repeated setup mechanisms and multi-step UI sequences into centralized, shared asynchronous helpers.
What: Repeated interaction sequences required to establish a test's initial state (e.g., clicking menus, awaiting cycles, querying elements) must be extracted into asynchronous helper functions.
Applies To: Component test suites with repetitive, multi-step UI interaction setups.
Why: Tests were repeatedly copying and pasting the multi-step DOM interaction needed to open a modal and fetch an input reference, inflating the codebase and making structural changes painful. Failing to adhere to this typically results in High Maintenance Burden.
Trap 1: Duplicating 5+ lines of UI interaction setup across multiple test cases.
Don't:
await element.updateComplete; lines into every test block.Do:
async function openLinkDialogAndGetInput(): Promise<HTMLInputElement>) and
calling it in each test.Rule: Never mutate component instances with custom, untyped tracking properties; track test states using locally scoped variables.
What: Avoid mutating component instances with test-only custom properties to track state during tests. Utilize locally scoped variables inside the test setup or perform proper stub verification instead.
Applies To: Unit testing, specifically when stubbing instance methods or external calls.
Why: Attaching custom test-only properties (like tracking URLs directly on the element) polluted the component object model, bypassed encapsulation, and risked state leakage between unit tests. Failing to adhere to this typically results in Test State Leakage.
Trap 1: Assigning a random, dynamic property to the component being tested to track mock responses or internal states.
Don't:
.callsFake(function (this: MyComponent, arg: any) {
// BAD: Modifying the instance for testing purposes
(this as any).__test_url = arg;
return 'data:image...';
});
// ... later in the test ...
assert.equal((element as any).__test_url, expectedUrl);
Do:
let generatedUrl: string;
.callsFake(function (this: MyComponent, arg: unknown) {
// GOOD: Using a scoped variable for tracking
generatedUrl = arg;
return 'data:image...';
});
// ... later in the test ...
assert.equal(generatedUrl, expectedUrl);
Rule: Must completely stub remote library calls or image-fetching mechanisms to execute entirely in isolation.
What: External libraries that trigger network requests (e.g., fetching scripts or image diffing workers) must be fully mocked in local test setups to prevent 404 network errors and ensure hermetic execution.
Applies To: Unit testing for components integrating with third-party, dynamically loaded libraries.
Why: Failure to mock external dependencies caused the test runner to attempt fetching remote or non-existent local assets, resulting in 404 console errors, flaky tests, and significantly slower execution times. Failing to adhere to this typically results in 404 Network Errors.
Trap 1: Allowing the component to natively load its third-party script dependencies during a basic unit test fixture setup.
Don't:
setup(async () => {
// BAD: Component attempts to download external libraries over the network
element = await fixture(`<my-viewer></my-viewer>`);
});
Do:
setup(async () => {
// GOOD: Mock external library loaders to resolve instantly
// @ts-expect-error
const libLoader = MyViewer.libLoader;
sinon.stub(libLoader, 'getLibrary').resolves();
// @ts-expect-error
window.externalLib = sinon.stub().returns({ ... });
element = await fixture(`<my-viewer></my-viewer>`);
});
Rule: Never augment native test timeouts (
this.timeout()) as a workaround for slow rendering components or unreliable baselines.What: Do not artificially inflate test timeouts (e.g., Mocha
this.timeout()) to mask slow component rendering or flakiness in visual regression tests. Maintain default timeouts.Applies To: Screenshot testing and visual regression test suites.
Why: Artificially extending timeouts allowed significant performance regressions in UI rendering to go unnoticed, as tests would wait excessively long for slow components to stabilize rather than failing fast. Failing to adhere to this typically results in Masked Performance Regressions.
Trap 1: Increasing the Mocha test suite timeout inside a failing screenshot test to force it to pass.
Don't:
suite('component screenshot tests', function () {
this.timeout(4000);
// ... test body ...
});
Do:
suite('component screenshot tests', () => {
// Default timeout ensures fast failure if rendering degrades
// Update screenshots with CLI commands if baseline legitimately changed
// ... test body ...
});
Rule: Must initialize required component properties to functional values prior to asserting against the shadow DOM.
What: Unit tests that assert against a component's shadow DOM must initialize the component with realistic property states, ensuring the DOM is not trivially empty or unrendered.
Applies To: Lit component tests doing Shadow DOM assertions.
Why: Asserting against an empty or completely uninitialized DOM resulted in false-positive test passes that failed to verify any actual template or structural logic. Failing to adhere to this typically results in False Positive Tests.
Trap 1: Rendering a generic template without required inputs and asserting the shadow DOM is empty.
Don't:
test('render', async () => {
await element.updateComplete;
assert.shadowDom.equal(element, '');
});
Do:
test('render', async () => {
element.name = 'Test';
await element.updateComplete;
assert.shadowDom.equal(
element,
`<div><h3 class="title">Test</h3></div>`
);
});
Rule: Always name test cases with strict, functional "renders [state] if [condition]" descriptions.
What: Test cases must use descriptive 'renders X if Y' naming structures rather than relying on abstract, ambiguous, or overly technical jargon that lacks functional context.
Applies To: Unit test descriptions (the string argument in
test()orit()).Why: Ambiguous test names using mathematical or overly specific internal jargon made it difficult for developers to deduce the functional intent of the test upon failure without reading the implementation. Failing to adhere to this typically results in Unclear Test Intent.
Trap 1: Naming a test based on an abstract concept rather than the behavioral condition.
Don't:
Do:
Rule: Must guarantee that mock datasets used in visual regressions bypass data points filtered out by production pipeline logic.
What: Mock data structures used to drive screenshot/visual regression tests must precisely mirror the data shapes and filtering logic deployed in the production frontend.
Applies To: Frontend visual regression tests (
*_screenshot_test.ts) and mock data fixtures.Why: Visual tests previously hardcoded reference types that were intentionally stripped out by frontend plugins in production. This resulted in visual tests asserting on UI components that users would never actually see. Failing to adhere to this typically results in False Positive Tests.
Trap 1: Defining static mock data for visual tests that includes properties normally filtered out by the production environment.
Don't:
Do:
updateComplete chains mapped here.Partial<T> and eliminating any casting rely
completely on the strictness mandates of this domain.Context: This theme governs the optimization of client-side performance by enforcing strict telemetry on synchronous CPU-bound operations and minimizing network latency. It mandates the caching of asynchronous API promises, the derivation of state from existing payloads, and the rigid bounding of background data requests.
| Rule ID | Principle / Constraint | Priority | Primary Symptom / Trap |
|---|---|---|---|
| T4-01 | Synchronous Execution | High | Assuming a save operation |
| : : Telemetry Profiling : : is only slow due to : | |||
| : : : : network requests and : | |||
| : : : : leaving synchronous data : | |||
| : : : : preparation unmeasured. : | |||
| T4-02 | Elimination of Redundant | High | Executing an asynchronous |
| : : API Data Fetches : : API call to pull data : | |||
| : : : : that already exists as a : | |||
| : : : : property of a currently : | |||
| : : : : loaded state object. : | |||
| T4-03 | Promise-Based API Request | High | Calling the API directly |
| : : Caching : : on every query invocation : | |||
| : : : : without storing the : | |||
| : : : : ongoing or resolved : | |||
| : : : : request. : | |||
| T4-04 | Capped Payloads for | High | Passing undefined or |
| : : Autocomplete Suggestion : : leaving limit parameters : | |||
| : : Requests : : empty for data : | |||
| : : : : aggregation endpoints : | |||
| : : : : used merely for : | |||
| : : : : suggestion dropdowns. : |
Rule: Always instrument synchronous, CPU-bound logic with telemetry timers to surface main-thread blocking operations. Never assume UI latency is solely caused by asynchronous network requests.
What: Instrument synchronous, CPU-bound logic (e.g., object comparison, distance calculations) with telemetry timers to identify main-thread blocking operations, rather than solely profiling asynchronous network calls.
Applies To: Frontend Performance Profiling; particularly in high-frequency or data-heavy UI actions like draft comment saving and text matching.
Why: Historically, performance metrics focused heavily on network request times. This hid UI freezes caused by expensive O(n^2) synchronous calculations on the main thread, such as running deep equality checks against large AI-generated patch suggestions. Failing to adhere to this typically results in Main Thread UI Freezes.
Trap 1: Assuming a save operation is only slow due to network requests and leaving synchronous data preparation unmeasured.
Don't:
// BAD: Only timing the network request
const result = await this.restApiService.saveDraft(draft);
Do:
// GOOD: Timing CPU-bound data preparation independently
const fixTimer = this.reporting.getTimer('UpdateDraftComment - isFixSuggestionChanged');
if (this.isFixSuggestionChanged()) {
draft.fix_suggestions = this.getFixSuggestions();
}
fixTimer.end();
const networkTimer = this.reporting.getTimer('UpdateDraftComment - network');
const result = await this.restApiService.saveDraft(draft);
networkTimer.end();
Rule: Always derive state directly from globally hydrated context objects instead of executing redundant REST API requests.
What: Derive required state directly from existing context payloads (such as the globally hydrated Change object) rather than making redundant network requests to fetch subsets of identical data.
Applies To: Frontend components querying repository metadata, permissions, or labels.
Why: The UI was performing a dedicated REST API call to fetch repository labels, adding UI latency, even though the allowed labels were already hydrated within the
change.permitted_labelsproperty on the global change object. Failing to adhere to this typically results in Redundant Network Latency.
Trap 1: Executing an asynchronous API call to pull data that already exists as a property of a currently loaded state object.
Don't:
// Anti-pattern: Fetching when the data is already there
this.repoLabels = await this.restApiService.getRepoLabels(change.project);
Do:
// Preferred: Mapping from existing context
const permittedLabels = change.permitted_labels ?? {};
this.repoLabels = Object.entries(permittedLabels).map(...);
Exceptions: If the existing payload is known to be stale or intentionally truncated for performance reasons in that specific context.
Rule: Must cache the Promise of an API request to prevent redundant network calls during rapid UI events, ensuring errors resolve safely to avoid cache poisoning.
What: Cache the Promise of an API request to prevent redundant network calls during rapid UI events (e.g., autocomplete typing), and ensure failed requests resolve to a safe fallback (like undefined) so the cache is not poisoned permanently.
Applies To: Frontend services making API calls based on user input, specifically Autocomplete and Suggestion Providers.
Why: Fetching data on every keystroke without caching led to spamming the backend API, especially when the data subset being searched was static (e.g., repository labels) and could be filtered purely on the client side. Failing to adhere to this typically results in Backend API Spam / High Latency.
Trap 1: Calling the API directly on every query invocation without storing the ongoing or resolved request.
Don't:
getSuggestions(expression: string) {
if (!this.repoName) return Promise.resolve([]);
return this.restApiService.getRepoLabels(this.repoName).then(labels => {
// filter logic
});
}
Do:
getSuggestions(expression: string) {
if (!this.repoName) return Promise.resolve([]);
if (!this.cachedLabelsPromise) {
this.cachedLabelsPromise = this.restApiService
.getRepoLabels(this.repoName)
.catch(err => {
reportingService.error('Provider', err);
return undefined; // Ensure caught errors resolve safely
});
}
return this.cachedLabelsPromise.then(labels => {
// filter logic
});
}
Exceptions: Queries that rely on server-side filtering (e.g., passing the user's keystroke to the backend endpoint directly) cannot be cached in this manner.
Rule: Never execute unbounded background API queries; always enforce hard payload limits for components like autocomplete and dialogs to prevent UI freezes.
What: Backend API queries triggered by background UI components (like autocomplete dropdowns or dialog filling) must be strictly bounded to a hard limit to prevent downloading excessive payloads.
Applies To: API query parameters, specifically REST API requests fetching background data for UI presentation.
Why: An unbounded query for open changes downloaded over 600kB of data from the network, causing a severe UX lag and freezing the user interface on slower internet connections. Failing to adhere to this typically results in Network Bottleneck.
Trap 1: Passing undefined or leaving limit parameters empty for data
aggregation endpoints used merely for suggestion dropdowns.
Don't:
// BAD: Unbounded fetching for suggestions
await this.restApiService.getChanges(
undefined, // no limit
'is:open -age:90d'
);
Do:
// GOOD: Hardcoded maximum to prevent bandwidth exhaustion
await this.restApiService.getChanges(
/* changeNumber=*/ 450,
'is:open -age:90d'
);
Context: This domain governs the consistent application of styles across the UI, mandating the use of content-driven layout techniques, externalized custom property configurations for visual assets, and declarative Lit directives over imperative inline styling.
| Rule ID | Principle / Constraint | Priority | Primary Symptom / Trap |
|---|---|---|---|
| T5-01 | Dynamic Sizing for Custom | Medium | Embedding static pixel |
| : : SVG Icons via CSS : : values for width and : | |||
| : : : : height inside the raw SVG : | |||
| : : : : literal. : | |||
| T5-02 | Dynamic Width via | Medium | Hardcoding width in |
| : : Content-Driven CSS Sizing : : pixels or percentages to : | |||
| : : : : achieve visual uniformity : | |||
| : : : : despite unpredictable : | |||
| : : : : content strings. : | |||
| T5-03 | Declarative Conditional | Medium | Controlling visibility by |
| : : Visibility using Class : : binding ternary operators : | |||
: : Maps : : to the inline style : | |||
| : : : : attribute. : |
Rule: Always omit hardcoded dimensional attributes from custom SVG markup. Must control icon sizing dynamically using CSS custom properties to ensure cross-context scalability.
What: Custom SVG icon definitions must not contain hardcoded
widthorheightattributes within the markup. Dimensions should be omitted from the SVG template and controlled dynamically via CSS custom properties.Applies To: Lit icon wrapper components (
gr-icon) and centralized custom SVG asset files.Why: Hardcoded dimensions within SVG source strings forced icons into rigid sizes, preventing them from scaling properly when nested inside generic buttons, dense lists, or varying display contexts. Failing to adhere to this typically results in Layout Clipping / Unresponsive UI.
Trap 1: Embedding static pixel values for width and height inside the raw SVG literal.
Don't:
const spark = svg`<svg width="24px" height="24px" viewBox="0 0 960 960">...</svg>`;
Do:
const spark = svg`<svg viewBox="0 0 960 960">...</svg>`;
// Within gr-icon CSS:
// svg {
// width: var(--gr-icon-size, 20px);
// height: var(--gr-icon-size, 20px);
// }
Rule: Always prefer
width: fit-contentover arbitrary fixed pixel limits for dynamically generated UI cards. Never enforce visual uniformity at the expense of content readability.What: Use
width: fit-contentfor dynamically generated UI cards rather than uniform fixed widths when content length is variable and unpredictable.Applies To: UI component styling, specifically CSS rules for lists of cards or informational blocks.
Why: Fixed-width cards were initially suggested for uniform alignment, but variable-length rules caused clipping or excessive whitespace.
fit-contentprovided a more flexible baseline layout. Failing to adhere to this typically results in Visual Clipping.
Trap 1: Hardcoding width in pixels or percentages to achieve visual uniformity despite unpredictable content strings.
Don't:
/* BAD: Fixed width that might clip */
.flow-card {
width: 300px;
}
Do:
/* GOOD: Width determined by content */
.flow-card {
width: fit-content;
}
Exceptions: Cases where uniform width is strictly mandated by UX mocks and content is truncated gracefully with ellipses.
Rule: Must use standard CSS utility classes via Lit's
classMapdirective to toggle element visibility. Never bind ternary operators to inject inline style strings.What: Conditional visibility logic must rely on a standard CSS
.hiddenutility class applied via Lit'sclassMapdirective, rather than injecting inline style strings.Applies To: Component rendering templates (
htmlstrings) handling dynamically hidden UI states.Why: Visibility was frequently controlled via verbose ternary operators injecting hardcoded CSS text into
styleattributes, making code harder to read and reuse. Failing to adhere to this typically results in Brittle Inline Styling.
Trap 1: Controlling visibility by binding ternary operators to the inline
style attribute.
Don't:
// BAD: Inline conditional styles
<md-icon-button
style=${this.supportsHistory ? '' : 'visibility:hidden; pointer-events:none;'}
>
Do:
// GOOD: Using Lit classMap with a dedicated CSS rule
<md-icon-button
class=${classMap({
'history-button': true,
'hidden': !this.supportsHistory
})}
>
Exceptions: Truly dynamic, mathematically computed layout styles (e.g., precise pixel offsets or user-defined color themes).
classMap provide the declarative mechanism required to
apply standard CSS utility classes effectively.Context: This chapter governs the resilient integration of frontend logic
with REST APIs. It establishes strict constraints for maintaining backend
payload parity, explicitly modeling structural variances, and enforcing
centralized error handling over localized try...catch blocks.
| Rule ID | Principle / Constraint | Priority | Primary Symptom / Trap |
|---|---|---|---|
| T6-01 | Explicit Server Error | High | Firing mutative API |
| : : Reporting in REST API : : requests without : | |||
| : : Calls : : explicitly configuring : | |||
| : : : : the fetch client to : | |||
| : : : : report server-side HTTP : | |||
| : : : : errors. : | |||
| T6-02 | Strict Frontend-Backend | Critical | Using a descriptive but |
| : : API Payload Parity : : incorrect property name : | |||
| : : : : in the UI that does not : | |||
| : : : : match the exact backend : | |||
| : : : : JSON key. : | |||
| T6-03 | Delegation to Centralized | Medium | Wrapping API calls in |
| : : REST API Error Handlers : : verbose catch blocks : | |||
| : : : : solely to surface generic : | |||
| : : : : network failure messages. : | |||
| T6-04 | Centralized Error | Medium | Wrapping an API call in a |
| : : Callbacks for API Calls : : try/catch block and : | |||
| : : : : firing a custom error : | |||
| : : : : event on catch alongside : | |||
| : : : : null checks. : | |||
| T6-05 | Modeling Inconsistent | Medium | Assuming a single payload |
| : : Backend API Payloads : : structure across : | |||
| : : : : disparate endpoints and : | |||
| : : : : experiencing undefined : | |||
| : : : : runtime property access. : |
Rule: Always enable explicit server error reporting for mutating API requests (PUT, POST, DELETE) to guarantee backend failures reach the UI.
What: State-mutating REST API calls (PUT, POST, DELETE) must explicitly enable server error reporting in their configuration to ensure backend failures are caught and surfaced to the UI.
Applies To: REST API service layer (
gr-rest-api-impl.ts), specifically when migrating to the newfetchhelper structure.Why: During the migration to a modern REST API helper module, API failures for mutating operations could be silently swallowed unless
reportServerError: truewas explicitly passed in the request configuration. Failing to adhere to this typically results in Silent API Failures.
Trap 1: Firing mutative API requests without explicitly configuring the fetch client to report server-side HTTP errors.
Don't:
this._restApiHelperNew.fetch({
fetchOptions: getFetchOptions({
method: HttpMethod.PUT,
body: config,
}),
url,
});
Do:
this._restApiHelperNew.fetch({
fetchOptions: getFetchOptions({
method: HttpMethod.PUT,
body: config,
}),
url,
reportServerError: true,
});
Rule: Must perfectly mirror the exact naming conventions of backend JSON response fields within frontend TypeScript interfaces; never substitute assumed names.
What: Frontend TypeScript interfaces must perfectly mirror the naming conventions of the backend JSON response fields; UI logic cannot substitute similar or assumed names.
Applies To: REST API models, data layer mappings, and UI conditionals consuming backend payloads.
Why: A UI feature designed to hide a component when a diff was too large failed silently in production because the frontend referenced
diffs_too_expensive_to_computewhile a previous implementation or documentation referred totoo_expensive_to_compute, resulting inundefined. Failing to adhere to this typically results in Silent UI State Failures.
Trap 1: Using a descriptive but incorrect property name in the UI that does not match the exact backend JSON key.
Don't:
// BAD: Backend sends 'diffs_too_expensive_to_compute'
if (this.file?.too_expensive_to_compute) {
renderWarning();
}
Do:
// GOOD: Exact matching property name
if (this.file?.diffs_too_expensive_to_compute) {
renderWarning();
}
Rule: Never wrap standard REST API calls in explicit
try...catchblocks to handle generic network failures.What: Do not wrap standard REST API calls in explicit try...catch blocks to handle generalized network failures, as the global framework centrally intercepts these and dispatches user-visible network errors.
Applies To: API service integrations and asynchronous handlers making network requests.
Why: A reviewer expressed concern about unhandled promise rejections if a network call failed inside a finally block. The framework is designed specifically not to throw standard API errors back to the caller unless explicitly opted into via a custom errFn, handling generic network error alerting natively. Failing to adhere to this typically results in Redundant Error Boilerplate.
Trap 1: Wrapping API calls in verbose catch blocks solely to surface generic network failure messages.
Don't:
try {
await this.restApiService.createFlow(changeNum);
} catch (e) {
fireNetworkError(this, e);
}
Do:
// Fire and let the framework's centralized errFn handle generic failures.
await this.restApiService.createFlow(changeNum);
Exceptions: When a specific component logic must intercept an error
silently, or when a custom errFn is explicitly provided to bypass default
handling.
Rule: Always use built-in centralized error callback mechanisms (
errFn) provided by the REST API service instead of manualtry...catchblocks.What: Use built-in centralized error callback mechanisms (
errFn) provided by the REST API service instead of wrapping calls in manualtry...catchblocks that result in redundant local error dispatching.Applies To: Asynchronous REST API calls within frontend components.
Why: Manual
try...catchblocks often resulted in duplicate error notifications being fired to the user—once for the native fetch rejection by the global handler, and once for the custom component fallback. Failing to adhere to this typically results in Duplicate Error Notifications.
Trap 1: Wrapping an API call in a try/catch block and firing a custom error event on catch alongside null checks.
Don't:
try {
response = await this.restApiService.getPatchContent();
} catch (e) {
fireError(this, 'Failed');
return;
}
if (!response) { fireError(this, 'Failed'); return; }
Do:
const response = await this.restApiService.getPatchContent(errFn);
if (!response) return;
Rule: Must explicitly document and type structural variances with optional fields when backend endpoints return disparate keys for the same entity.
What: When backend endpoints return structurally differing payload keys for the same conceptual entity, the frontend interface must explicitly type the variance and track the technical debt, rather than forcing a singular, inaccurate type.
Applies To: Frontend API interface definitions and REST service layers.
Why: Two related backend chat endpoints returned data under different keys (
responsevschat_response). Forcing the frontend to expect onlyresponsebroke conversation loading, necessitating explicit optional types. Failing to adhere to this typically results in Type Safety Violation.
Trap 1: Assuming a single payload structure across disparate endpoints and experiencing undefined runtime property access.
Don't:
// BAD: Forces all endpoints to return 'response'
interface ConversationTurn {
response: ChatResponse;
}
Do:
// GOOD: Accurately model the backend variance and track the tech debt
interface ConversationTurn {
response: ChatResponse;
// TODO: Clean this up - when loadConversation is used we get chat_response instead of response
chat_response?: ChatResponse;
}
Context: This domain governs the client-side lifecycle and optimization of data structures sent to AI agents and telemetry pipelines. It strictly dictates payload deduplication strategies, transparent AI token constraint surfacing, and rigorous parsing validation to prevent silent context loss.
| Rule ID | Principle / Constraint | Priority | Primary Symptom / Trap |
|---|---|---|---|
| T7-01 | Elimination of Implicit | Medium | Manually injecting the |
| : : Telemetry Payload Fields : : current domain/host into : | |||
| : : : : the analytics event : | |||
| : : : : detail object. : | |||
| T7-02 | Deduplication of | Medium | Defining telemetry |
| : : Telemetry Payload : : interfaces that manually : | |||
| : : Attributes : : require context variables : | |||
| : : : : easily derivable from the : | |||
| : : : : current session or : | |||
| : : : : backend data warehouse. : | |||
| T7-03 | Surface AI Context Prompt | Medium | Rendering an empty |
| : : Sizes in the UI : : container or entirely : | |||
| : : : : omitting the size : | |||
| : : : : calculation for AI prompt : | |||
| : : : : payloads. : | |||
| T7-04 | Validating AI Context | High | Filtering out undefined |
| : : Data Parsing : : parsed items before : | |||
| : : : : validating if the parsing : | |||
| : : : : step encountered : | |||
| : : : : failures. : |
Rule: Never include implicit environmental data like domain or host origin in frontend telemetry payloads. Always rely entirely on backend log enrichment to capture network request origins.
What: Exclude implicit environmental data (e.g., the browser's host or domain origin) from explicit frontend telemetry interaction payloads, relying instead on backend log enrichment.
Applies To: Frontend telemetry and metrics reporting services.
Why: Sending fields like
window.location.hostin interaction reporting payloads creates redundant data bloat, as the logging infrastructure automatically captures the origin of the network request. Failing to adhere to this typically results in Telemetry Payload Bloat.
Trap 1: Manually injecting the current domain/host into the analytics event detail object.
Don't:
const details: AiAgentEventDetails = {
host: window.location.host,
agentId,
conversationId: this.conversationId,
};
this.reportingService.reportInteraction(Interaction.AI_AGENT, details);
Do:
const details: Pick<AiAgentEventDetails, 'agentId' | 'conversationId'> = {
agentId,
conversationId: this.conversationId,
};
this.reportingService.reportInteraction(Interaction.AI_AGENT, details);
Rule: Must omit redundant context variables from telemetry payloads if they can be derived via backend SQL joins. Delegate session identity and role resolution to the downstream data pipeline.
What: Telemetry payloads sent from frontend components must not include redundant context variables (like changeId or userRole) if they can be attached via global session context or derived via SQL joins on the backend.
Applies To: Frontend reporting services, constants defining EventDetails, and UI components firing interaction events.
Why: The telemetry payloads for AI interactions manually calculated and passed user roles and change IDs, unnecessarily bloating the payload and duplicating data already captured globally by the reporting service. Failing to adhere to this typically results in Redundant Payload Bloat.
Trap 1: Defining telemetry interfaces that manually require context variables easily derivable from the current session or backend data warehouse.
Don't:
export type AiAgentEventDetails = {
changeId: string;
userRole: 'author' | 'reviewer';
// ... other fields
};
Do:
export type AiAgentEventDetails = {
// Rely on global session_id attached by reporting service
agentId: string;
turnIndex: number;
};
Trap 2: Performing complex logic on the client to derive a metric that can be natively handled using a SQL join during data analysis.
Don't:
const userRole = this.account?._account_id === this.change.owner._account_id ? 'author' : 'reviewer';
this.reportingService.reportInteraction('action', { userRole });
Do:
Rule: Always calculate and display prompt capacities (such as word or token counts) directly in the UI to make context limits explicit to the user.
What: Dialogs or interfaces handling AI prompt generation must calculate and explicitly display the prompt size (word or token count) to provide the user with clear context limits.
Applies To: AI prompt dialogue components and context builders.
Why: Failing to display prompt sizes left users blind to context limitations, leading to rejected backend payloads or silently truncated context when the payload exceeded token bounds. Failing to adhere to this typically results in Unpredictable AI Truncation.
Trap 1: Rendering an empty container or entirely omitting the size calculation for AI prompt payloads.
Don't:
<div class="actions">
<div class="size"></div>
<gr-button>Copy Prompt</gr-button>
</div>
Do:
<div class="actions">
<div class="size">
${this.calculatePromptWordCount(this.promptText)} words
</div>
<gr-button>Copy Prompt</gr-button>
</div>
Rule: Must assert the full structural integrity of parsed AI context datasets before executing any filtering logic. Never silently drop undefined items without evaluating parsing failures.
What: When parsing unstructured or semi-structured data for AI context payloads, validation logic must assert the integrity of the entire dataset before filtering out invalid entries.
Applies To: Frontend chat panels and AI context item parsers.
Why: Historically, the application filtered out undefined context items immediately after parsing. This swallowed partial parsing failures, meaning users were not alerted when some of their context links failed to load, leading to degraded AI prompts. Failing to adhere to this typically results in Silent Data Loss.
Trap 1: Filtering out undefined parsed items before validating if the parsing step encountered failures.
Don't:
// BAD: The check evaluates the array after undefined items are already removed.
const contextItems = (links ?? [])
.map(link => parseLink(link))
.filter(isDefined);
if (contextItems.some(item => !item)) {
fireAlert('Failed to parse links.');
}
Do:
// GOOD: Count items before and after filtering to detect failures, or check the mapped array prior to filtering.
const parsedItems = (links ?? []).map(link => parseLink(link));
const validItems = parsedItems.filter(isDefined);
if (links.length > 0 && validItems.length === 0) {
fireAlert('Failed to parse one or more context item links.');
}