skills/code-quality-analysis/SKILL.md
Analyze Angular / Cumulocity Web SDK code for anti-patterns, bugs, and quality issues. Use when reviewing components, services, or modules for code quality, maintainability, performance, and correctness. Covers TypeScript best practices, Angular idioms, C8Y SDK usage patterns, and project-specific conventions. Triggers: code review, anti-pattern, quality check, refactor suggestion, style guide, bug analysis.
npx skillsauth add cumulocity-iot/cumulocity-skills code-quality-analysisInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill performs comprehensive code quality analysis on Angular + Cumulocity Web SDK codebases. It combines guidance from:
mastering-typescript skill (from https://github.com/SpillwaveSolutions/mastering-typescript-skill/tree/main/mastering-typescript)https://angular.dev/assets/context/llms-full.txtmcp_c8y-docs_* MCP tools to retrieve component and API referencesMCP server required: The
mcp_c8y-docs_*tools are served byhttps://c8y-codex-mcp.schplitt.workers.dev/(server name:c8y-docs, transport: HTTP). Register it once with your agent:claude mcp add --transport http c8y-docs https://c8y-codex-mcp.schplitt.workers.dev/See
AGENTS.mdfor full setup instructions and alternative config formats.
Before analyzing any file, load the following resources in parallel:
skills/mastering-typescript/SKILL.mdhttps://angular.dev/assets/context/llms-full.txt (use the fetch_webpage tool)mcp_c8y-docs_get-codex-structure to get the full Codex mapmcp_c8y-docs_query-codex with queries relevant to the features used in the
file under review (e.g. ["css utility classes spacing", "color tokens", "widgets lazy loading"])mcp_c8y-docs_query-codex with queries for the C8Y SDK services in scope
(e.g. ["InventoryService", "AlarmService", "MeasurementService client"]) to validate API usageIf no specific file was provided, search for:
*.component.ts files*.service.ts files*.module.ts files*.component.html filesRun each check below against the target file(s). For every finding:
*ngIf instead of @if (Angular Control Flow)Rule: Never use the structural directive *ngIf. Use the built-in @if / @else block
syntax introduced in Angular 17+.
Also applies to: *ngFor → @for, *ngSwitch → @switch.
<!-- BAD -->
<div *ngIf="loading">Loading…</div>
<li *ngFor="let item of items">{{ item.name }}</li>
<!-- GOOD -->
@if (loading) {
<div>Loading…</div>
}
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
}
Rule: Components are responsible for the view only. Business logic, data transformation, and orchestration belong in dedicated services.
Signals of violation:
Refactor pattern: Extract logic into an @Injectable() service. The component calls
the service and binds the result.
any EverywhereRule: Explicit any is forbidden except at genuine boundaries (e.g. 3rd-party library
interop with no types). Prefer unknown, proper interfaces, or generic type parameters.
Detection:
: anyas any: anyFix examples:
// BAD
function process(data: any): any { … }
// GOOD
function process<T extends Record<string, unknown>>(data: T): ProcessedResult { … }
Cross-reference the mastering-typescript skill (section "Type Guards and Narrowing") for
patterns that eliminate the need for any.
Rule: Every component that does not mutate shared mutable state should use
ChangeDetectionStrategy.OnPush. Default change detection causes unnecessary re-renders
across the entire component tree.
// BAD
@Component({ selector: 'app-sensor', templateUrl: '…' })
export class SensorComponent { … }
// GOOD
@Component({
selector: 'app-sensor',
templateUrl: '…',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SensorComponent { … }
Note: With OnPush, use async pipe or markForCheck() / signals to trigger updates.
trackBy / track in LoopsRule: Every @for loop (or legacy *ngFor) that renders a list of objects must
specify a track expression so Angular can diff items by identity rather than re-rendering
the entire list.
<!-- BAD -->
@for (sensor of sensors) {
<app-sensor [sensor]="sensor" />
}
<!-- GOOD -->
@for (sensor of sensors; track sensor.id) {
<app-sensor [sensor]="sensor" />
}
standalone: false)Rule: All newly created components, directives, and pipes must be standalone
(standalone: true). Do not create new NgModules for features. Existing NgModules can
be kept only when wrapping third-party code that requires it. standalone: false should
never appear in new code.
// BAD
@Component({ selector: 'app-foo', templateUrl: '…', standalone: false })
export class FooComponent { … }
// GOOD
@Component({ selector: 'app-foo', templateUrl: '…', standalone: true, imports: [CommonModule] })
export class FooComponent { … }
Rule: Do not create an Observable or Subject just to hold a synchronous or
non-reactive value. Use plain variables, signal(), or computed() instead.
Signals of violation:
BehaviorSubject<boolean>(false) used as a simple flag with no subscribers outside
the same classSubject that is immediately .next()-ed once and never reusedPromise result in an Observable without a good reason// BAD — isLoading is never observed reactively outside this class
private isLoading$ = new BehaviorSubject<boolean>(false);
// GOOD
isLoading = signal(false);
Rule: Angular templates re-evaluate every expression on each change-detection cycle. Method calls in templates are called every cycle, even if their inputs have not changed.
Signals of violation:
{{ formatDate(item.timestamp) }} — function call in interpolation[class]="getClass(item)" — function call in binding*ngIf="shouldShow(item)" / @if (shouldShow(item)) — function call in conditionFix: Move transformation logic to a pure Pipe or pre-compute in the component
class (e.g. derived signal() or computed()).
// BAD (template): {{ formatValue(measurement) }}
// GOOD — create a pipe
@Pipe({ name: 'formatValue', pure: true, standalone: true })
export class FormatValuePipe implements PipeTransform {
transform(measurement: IMeasurement): string { … }
}
Rule: Any Observable.subscribe() call made inside a component or service that is
not providedIn: 'root' must be cleaned up to avoid memory leaks.
Preferred patterns (in order):
async pipe in the template — Angular unsubscribes automaticallytakeUntilDestroyed(this.destroyRef) (Angular 16+)Subscription and call subscription.unsubscribe() in ngOnDestroy// BAD
ngOnInit(): void {
this.service.data$.subscribe(d => this.data = d);
}
// GOOD
private destroyRef = inject(DestroyRef);
ngOnInit(): void {
this.service.data$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(d => this.data = d);
}
FetchClient for Calls Covered by @c8y/clientRule: Never use Angular's HttpClient or FetchClient for Cumulocity REST API calls
that are already wrapped by a service in @c8y/client. Only ok if used for microservice queries (baseUrl contains /service).
Use mcp_c8y-docs_query-codex to look up the relevant @c8y/client service for the domain in
question (e.g. ["InventoryService REST"], ["AlarmService client"]). Covered domains include:
inventory, alarms, events, measurements, operations, binary, users, and more.
// BAD — HttpClient used for inventory
constructor(private http: HttpClient) {}
getDevice(id: string) {
return this.http.get(`/inventory/managedObjects/${id}`);
}
// GOOD — use the typed SDK service
constructor(private inventory: InventoryService) {}
async getDevice(id: string) {
const { data } = await this.inventory.detail(id);
return data;
}
Exception: Raw HTTP is acceptable only for external APIs or endpoints not covered by
@c8y/client.
Rule: Widget plugin modules that are registered via the C8Y hook mechanism must use lazy loading so they are only downloaded when needed. Implies configuring components as standalone: true.
loadComponent instead of componentloadConfigComponent instead of configComponent// BAD
{
component: MyWidgetComponent,
configComponent: MyWidgetConfigComponent,
}
// GOOD
{
loadComponent: () => import('./my-widget/my-widget.component').then(m => m.MyWidgetComponent),
loadConfigComponent: () => import('./my-widget/my-widget-config.component').then(m => m.MyWidgetConfigComponent),
}
Rule: Do not introduce custom CSS/LESS rules for spacing, typography, or color when the Cumulocity Design System already provides utility classes or CSS custom properties. Hard-coded hex/rgb color values break tenant branding.
Action: Before writing any custom style, query the Codex:
mcp_c8y-docs_query-codex(["css utility classes spacing padding margin", "color tokens design system", "typography font size"])
Signals of violation:
margin, padding, or font-size with raw pixel values when a
utility class existscolor: #1776BF, background: rgb(23,118,191))--c8y-* design tokens with fixed valuesFix: Use the Cumulocity utility classes (m-t-8, p-l-16, text-muted, etc.) or
CSS custom properties (var(--c8y-brand-primary)) documented in the Codex.
Rule: If the same logical block or template pattern appears more than twice across different files, extract it into:
src/modules/shared/ (for templates)Signals of violation:
filter + map chain repeated in multiple servicesRule: Do not define TypeScript interfaces or types inside a component, directive, or
service file if those types are also used by other files. Define them in a dedicated
model file (e.g. src/models/sensor.model.ts or adjacent *.model.ts).
// BAD — inside sensor-list.component.ts
export interface SensorFilter { type: string; active: boolean; }
// GOOD — src/models/sensor-filter.model.ts
export interface SensorFilter { type: string; active: boolean; }
providedIn: 'root'Rule: Only services that genuinely need application-wide singleton state should use
providedIn: 'root'. If a service is tightly coupled to a single component (e.g. it
holds local UI state), remove providedIn and provide it in the component's providers
array instead — this also ensures it is destroyed along with the component.
// BAD — global singleton for a component-local concern
@Injectable({ providedIn: 'root' })
export class SensorTableStateService { … }
// GOOD — provided by the component that owns it
@Injectable()
export class SensorTableStateService { … }
@Component({
…
providers: [SensorTableStateService],
})
export class SensorTableComponent { … }
Rule: Components are tied to the Angular view lifecycle and can be destroyed at any time. Do not perform long-running async operations (polling loops, large data fetches, chained network requests) directly inside a component.
Fix: Delegate long-running work to a root-level service that is not bound to a component's lifetime. The component subscribes to the service's output observable or signal and unsubscribes cleanly on destroy (see AP-09).
// BAD — long poll inside a component
ngOnInit(): void {
this.pollFirmwareStatus(); // may run forever if component is destroyed mid-flight
}
// GOOD — root service drives the async work
@Injectable({ providedIn: 'root' })
export class FirmwareStatusService {
readonly status$ = timer(0, 5000).pipe(
switchMap(() => this.fetchStatus()),
shareReplay(1),
);
}
IManagedObject Extra AttributesRule: IManagedObject is a generic model with an open index signature ([key: string]: any).
Any fragment or custom attribute on are not guaranteed to be present or correctly shaped at
runtime. Every access to a non-standard IManagedObject property must be guarded with a
type predicate function or a Zod schema so that the code is both type-safe and validated at
runtime.
Reference: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
Signals of violation:
device['c8y_Hardware'].serialNumberas MyDeviceType without runtime validation(obj as any)?.nested?.propFix — Type Predicate:
// BAD
function getSerialNumber(mo: IManagedObject): string {
return mo['c8y_Hardware'].serialNumber; // no guarantee the fragment or field exists
}
// GOOD — define the expected shape and a type predicate
interface C8yHardware {
model: string;
revision: string;
serialNumber: string;
}
interface DeviceWithHardware extends IManagedObject {
c8y_Hardware: C8yHardware;
}
function hasHardwareInfo(mo: IManagedObject): mo is DeviceWithHardware {
const hw = (mo as DeviceWithHardware).c8y_Hardware;
return (
typeof hw === 'object' &&
hw !== null &&
typeof hw.serialNumber === 'string'
);
}
// Usage
if (hasHardwareInfo(device)) {
// device.c8y_Hardware is now fully typed and validated
return device.c8y_Hardware.serialNumber;
}
Fix — Zod Schema (preferred for complex/nested shapes):
import { z } from 'zod';
const C8yHardwareSchema = z.object({
model: z.string(),
revision: z.string(),
serialNumber: z.string(),
});
// Parse at the boundary (e.g. when receiving the MO from the API)
const parseHardwareInfo = (mo: IManagedObject) => C8yHardwareSchema.safeParse(mo['c8y_Hardware']);
const result = parseHardwareInfo(device);
if (result.success) {
const { model, revision, serialNumber } = result.data; // fully typed
}
Cross-reference the mastering-typescript skill (section "Zod Validation") for schema composition patterns.
| translate)Rule: Every user-visible string in a template must be piped through | translate
so it can be localized via the .po translation files. Hard-coded English strings in
templates are not translatable and will break non-English deployments.
Also applies to:
AlertService, ModalService, toast notifications, etc. — use
TranslateService.instant() or pass keys.Signals of violation:
<span>Loading…</span> — no | translate[placeholder]="'Enter a value'" — hard-coded string in bindingtitle="Device details" — hard-coded HTML attributethis.alertService.add({ text: 'Saved successfully' }) — un-translated stringFix:
<!-- BAD -->
<button>Save</button>
<span>No devices found.</span>
<!-- GOOD -->
<button>{{ 'Save' | translate }}</button>
<span>{{ 'No devices found.' | translate }}</span>
// BAD
this.alert.add({ text: 'Operation failed', type: 'danger' });
// GOOD
this.alert.add({ text: this.translate.instant('Operation failed'), type: 'danger' });
After adding a new key, add translations to every .po file in src/locales/
(de.po, fr.po, es.po, it.po, ja_JP.po, ko.po, pt_BR.po, ru.po, zh_CN.po).
At minimum, copy the English string as a placeholder so the build does not produce
empty translation entries.
Severity: High (Security)
Rule: No passwords, API keys, bearer tokens, or tenant-specific URLs must ever be committed to source control. This includes:
package.json scripts (e.g. --user admin:password)package.json proxy/server configs (e.g. https://mytenant.cumulocity.com)*.ts, *.json, or *.env file that is tracked by git.env files containing real credentials that are not in .gitignoreSignals of violation:
package.json scripts containing --user, --password, --bearer, or credential-looking stringspackage.json devProxy / c8ycli server config with a hard-coded https://<tenant>.cumulocity.com URL (tenant-specific; should live in a local env file)*.env or .env.* file tracked by git that contains non-example values// token: eyJhbGc…)Fix:
// BAD — package.json
{
"scripts": {
"start": "c8ycli server --user admin:mypassword123 https://mytenant.eu-latest.cumulocity.com"
}
}
// GOOD — package.json references an env var or local config file
{
"scripts": {
"start": "c8ycli server" // tenant + credentials read from .env (gitignored)
}
}
# .gitignore — ensure these are always excluded
.env
.env.local
.env.*.local
*.credentials.json
Action items when this pattern is found:
.gitignore..env file (excluded from git) for
tenant URLs and credentials.HOOK_* Injection Tokens Instead of Hook FunctionsRule: The uppercase HOOK_* injection token pattern (e.g. HOOK_TABS, HOOK_ROUTE,
HOOK_ACTION_BAR, HOOK_COMPONENTS) is deprecated. Use the equivalent hook function
from @c8y/ngx-components instead. Hook functions are tree-shakeable, produce cleaner
provider arrays, and are the officially supported API going forward.
Also applies to: Using hookComponent to register a widget — widgets must use
hookWidget instead. This distinction matters because hookWidget registers the
component in the widget gallery and wires up additional widget-specific configuration.
Mapping:
| Deprecated token | Replacement function | Import path |
|---|---|---|
| HOOK_TABS | hookTab | @c8y/ngx-components |
| HOOK_ROUTE | hookRoute | @c8y/ngx-components |
| HOOK_ACTION_BAR | hookActionBar | @c8y/ngx-components |
| HOOK_COMPONENTS | hookWidget | @c8y/ngx-components |
| hookComponent({…}) for a widget | hookWidget({…}) | @c8y/ngx-components |
Note: Custom project-level InjectionTokens named HOOK_* (defined with new InjectionToken(…)) are not affected — only tokens imported from @c8y/ngx-components.
// BAD — deprecated token-based pattern
import { HOOK_TABS, HOOK_ACTION_BAR } from '@c8y/ngx-components';
providers: [
{ provide: HOOK_TABS, useClass: MyTabFactory, multi: true },
{ provide: HOOK_ACTION_BAR, useClass: MyActionFactory, multi: true },
]
// GOOD — hook function pattern
import { hookTab, hookActionBar } from '@c8y/ngx-components';
providers: [
hookTab(MyTabFactory),
hookActionBar(MyActionFactory),
]
// BAD — hookComponent used for a widget registration
import { hookComponent } from '@c8y/ngx-components';
providers: [
hookComponent({
id: 'my-widget',
label: 'My Widget',
component: MyWidgetComponent,
}),
]
// GOOD — hookWidget for widget gallery registration
import { hookWidget } from '@c8y/ngx-components';
providers: [
hookWidget({
id: 'my-widget',
label: 'My Widget',
component: MyWidgetComponent,
}),
]
Rule: Every public method or property on a component must be referenced in its
template (HTML). All other methods/properties must be private (or protected
if used by subclasses).
Signals of violation:
public method(): void { … } that is never called in the templatepublic isLoading = false; that is never read in the template (should be private)public because they were not explicitly marked privateDetection strategy:
.component.html) for references to each method/property nameprivateprivate// BAD — public method never called from the template
@Component({ … })
export class SensorComponent {
public title = 'Sensors';
public formatValue(val: number): string { return val.toFixed(2); } // template uses the pipe instead
public calculateThreshold(): number { … } // internal logic, not in template
}
// GOOD — only public if used in template; otherwise private
@Component({ … })
export class SensorComponent {
public title = 'Sensors'; // referenced in template: {{ title }}
private formatValue(val: number): string { … } // not in template; use a pipe instead or call from a template-bound method
private calculateThreshold(): number { … } // internal helper, never called from template
// Template-bound event handler — public because it's called from template (click)="onUpdate()"
public onUpdate(): void { … }
}
<!-- Corresponding template -->
<h1>{{ title }}</h1>
<button (click)="onUpdate()">Update</button>
Refactoring guide:
| async, interpolation) → keep publicprivatepublicprivateSeverity: High (Technical Debt)
Rule: Do not access or override private methods, properties, or internal APIs from
@c8y/ngx-components, @c8y/client, or any other third-party library. Private APIs are
not guaranteed to remain stable across patch versions and can break your code without
notice.
Exception: If extending a component or service from @c8y/ngx-components genuinely
requires accessing a private member, this is only acceptable as a last resort when:
// TODO: and assert() to detect
future breakageSignals of violation:
_ (e.g. this._internalState, component._cache)any to bypass TypeScript's access checks for private membersObject.defineProperty() or reflection to access/modify private stateprivate or protected method from a parent class without documented justificationCode patterns to avoid:
// BAD — accessing private property
export class MyDetailsComponent extends DetailsComponent {
ngOnInit(): void {
this._internalData = { … }; // DetailsComponent._internalData is private
}
}
// BAD — casting to any to bypass type safety
const cache = (this.service as any)._cache; // _cache is private
// BAD — no documentation about fragility
export class MyComponent extends BaseTabsComponent {
override protected getSelectedTabIndex(): number {
return this._selectedIndex; // relying on internal property
}
}
Correct pattern — document, assert, and plan to remove:
export class MyDetailsComponent extends DetailsComponent {
// TODO: Extract this logic to public API in @c8y/ngx-components
// Issue: https://github.com/SoftwareAG/cumulocity-app-builder/issues/XXXXX
// Risk: This overrides private method '_initializeForm()'. Patch upgrades may break this.
override protected _initializeForm(): void {
super._initializeForm();
// Assert that the internal structure hasn't changed
console.assert(
typeof (this as any)._formBuilder === 'function',
'Expected private _formBuilder method to exist on DetailsComponent'
);
// Custom initialization
this._applyCustomValidation();
}
private _applyCustomValidation(): void { … }
}
Why this matters:
@c8y/ngx-components is upgraded, the team must
review and test overridden private methods to ensure they still work.Action items:
// TODO: comment including:
console.assert()) to catch breaking changes earlyChecking for violations:
_ (underscore)as any casts combined with property accessObject.defineProperty(), Object.getOwnPropertyDescriptor(), or reflection APIsoverride keywords on protected or private methods in extended classes@c8y/client Service ResponsesRule: Do not check the res.status (or res.res.status) of a resolved response from
any @c8y/client service (e.g. InventoryService, AlarmService, EventService,
MeasurementService, OperationService, etc.) to verify it equals 200/201/ok. If the promise
resolves, the request was successful — non-2xx responses cause the promise to reject, so
a status check in the success path is always vacuously true and adds noise.
Signals of violation:
if (res.res.status === 200) after await inventoryService.detail(id)if (response.status === 200) after any @c8y/client service call=== 200 / !== 200 inside a .then() handlerFix:
// BAD — status check is redundant; promise already rejected on non-200
const res = await this.inventoryService.detail(id);
if (res.res.status === 200) {
this.device = res.data;
}
// GOOD — if we got here, the call succeeded
const { data } = await this.inventoryService.detail(id);
this.device = data;
// BAD — conditional on status inside .then()
this.alarmService.list(filter).then(res => {
if (res.res.status === 200) {
this.alarms = res.data;
}
});
// GOOD
this.alarmService.list(filter).then(({ data }) => {
this.alarms = data;
});
Why: @c8y/client service methods throw (reject the promise) on any non-successful
HTTP response. The resolved value's status is always a success code, making the check
both redundant and misleading — it implies a failure branch that can never be reached.
For each file analyzed, produce a structured report:
## Analysis: <file path>
### Summary
<one-paragraph summary>
### Findings
| # | Anti-Pattern | Severity | Line(s) |
|---|---|---|---|
| 1 | AP-06 Non-Standalone Component | High | 12 |
| 2 | AP-09 Missing Unsubscribe | Medium | 45–52 |
### Details
#### Finding 1 — AP-06 Non-Standalone Component (line 12)
**Code:**
```ts
// problematic snippet
Recommendation:
// fixed snippet
### Severity Levels
- **High** — likely bug, memory leak, or security concern
- **Medium** — maintainability or performance problem
- **Low** — style / convention violation
---
## Contextual Cross-References
When the analysis involves:
- **TypeScript types / generics** → consult [mastering-typescript](https://github.com/SpillwaveSolutions/mastering-typescript-skill/tree/main/mastering-typescript)
- **Angular lifecycle / control flow / change detection** → fetch `https://angular.dev/assets/context/llms-full.txt`
- **Angular style guide and conventions** → reference the version-specific style guide: `https://vMAJOR.angular.dev/style-guide` (e.g., `https://v20.angular.dev/style-guide` for Angular 20, `https://v19.angular.dev/style-guide` for Angular 19, or `https://angular.dev/style-guide` for the latest version)
- **CSS utilities, design tokens, component APIs** → call `mcp_c8y-docs_query-codex` with relevant keywords
- **C8Y REST API wrappers** → call `mcp_c8y-docs_query-codex` with the service name (e.g. `["InventoryService"]`)
tools
Scaffold a new Cumulocity application using the @c8y/websdk Angular schematic without human interaction. Covers Angular CLI installation, app generation, schematic setup, AI tools configuration, dev server, and build commands. Triggers: new app, scaffold, create application, ng add websdk, setup cumulocity app, new cumulocity project.
tools
Step-by-step guide to migrate a Cumulocity Web SDK application to a target version. Detects breaking changes with the ui-breaking-changes-cli, scaffolds a reference app at the target version with the new-app skill, compares key configuration files (app.ts, bootstrap.ts, angular.json, etc.), and finishes with a code-quality-analysis review. Triggers: migrate app, upgrade version, breaking changes, sdk upgrade, migrate cumulocity, upgrade websdk.
development
Complete guide to internationalizing a Cumulocity Web SDK application. Covers all approaches to annotating and translating text (gettext, translate pipe, translate directive, TranslateService), extracting strings, creating and updating .po files, overriding existing translations, and adding brand-new languages. Triggers: i18n, internationalization, add language, translate, translation, localization, l10n, new language, po file, gettext, TranslateService, language switcher.
development
Recommends and support the best approach for migrating PKI certificates when moving from another platform or PKI to Cumulocity. Use when a developer or architect asks about how to migrate existing PKI certificates to Cumulocity, how to handle certificate rotation during migration, or best practices for PKI management in Cumulocity.