diff --git a/docs/AUDIT_PLATFORM.md b/docs/AUDIT_PLATFORM.md new file mode 100644 index 00000000..77c0829c --- /dev/null +++ b/docs/AUDIT_PLATFORM.md @@ -0,0 +1,97 @@ +# Platform โ€” Systematic Audit (cross-workspace) + +Date: 2026-05-04 +Tooling-backed audit (with `GITEA_NPM_TOKEN` available): full `pnpm install`, +typecheck, test, and lint run successfully across all 69 workspace packages +(`packages/`, `services/`, `dashboards/`). + +Legend: ๐Ÿ”ด critical ยท ๐ŸŸ  high ยท ๐ŸŸก medium ยท ๐ŸŸข low ยท +โฌœ open ยท ๐ŸŸฆ in PR ยท โœ… fixed (commit hash on the right). + +--- + +## 0. Health snapshot + +| Check | Result | Notes | +| ----------------- | ---------- | ------------------------------------------------------------------------------------------------------ | +| `pnpm install -r` | โœ… pass | 4 peer warnings โ€” `@azure/cosmos` wants `@azure/core-client@^1.10.0`. Cosmetic. | +| `pnpm typecheck` | โœ… pass | All TS sources compile (`tsc --noEmit`). | +| `pnpm test` | โœ… pass | ~2,200 tests across 18+ test suites, all green. | +| `pnpm lint` | ๐ŸŸก partial | After this audit's structural fixes: still 85 pre-existing errors in code, 96 warnings. See section P. | + +## A. Lint pipeline blockers (fixed by this audit) + +| # | Issue | Severity | Status | Fix | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | :----: | ----------- | +| A1 | `packages/design-tokens/scripts/validate-tokens.cjs` โ€” 45 `no-undef` errors for `process` / `console`. Root eslint config didn't declare a Node-script env, and inline `/* eslint-env node */` is ignored by flat-config. | ๐ŸŸ  | โœ… | this commit | +| A2 | `packages/design-tokens/scripts/token-coverage.cjs` โ€” same root cause: 1 unused `e` in catch. | ๐ŸŸข | โœ… | this commit | +| A3 | `packages/ui/eslint.config.js` was a _complete override_ (flat config doesn't merge with the root). It declared no parser, so 38 parsing errors fired on `interface`, `import {โ€ฆ}`, and other TS syntax in `src/index.ts` and `src/components/*`. | ๐ŸŸ  | โœ… | this commit | +| A4 | `packages/ui` lint also re-included `dist/**/*.d.ts` because the root's `ignores: ['dist/**']` isn't inherited by the package-local override. | ๐ŸŸก | โœ… | this commit | +| A5 | `packages/ui/.storybook/preview.ts` not covered by any TS-parser block. | ๐ŸŸก | โœ… | this commit | +| A6 | `packages/feedback-client/src/index.ts` โ€” 2 `no-undef` for browser globals `XMLHttpRequest` / `ProgressEvent` (not in root globals list). | ๐ŸŸข | โœ… | this commit | +| A7 | `packages/feedback-client/src/index.ts` โ€” `preserve-caught-error` violation in `captureScreen()` (re-throwing without `cause`). | ๐ŸŸก | โœ… | this commit | +| A8 | `packages/feedback-client/src/index.ts` โ€” `captureElement()` declares unused params `mimeType`, `quality`. Renamed with `_` prefix and documented why. | ๐ŸŸข | โœ… | this commit | +| A9 | `packages/logger/src/__tests__/logger.test.ts` โ€” unused type import `LoggerConfig`. | ๐ŸŸข | โœ… | this commit | +| A10 | `services/extraction-service/src/lib/circuit-breaker.test.ts` โ€” unused vitest import `afterEach`. | ๐ŸŸข | โœ… | this commit | +| A11 | `services/extraction-service/src/modules/extract/sidecar-monitor.test.ts` โ€” unused type import `HealthCheck`. | ๐ŸŸข | โœ… | this commit | +| A12 | `services/extraction-service/src/modules/extract/usage.test.ts` โ€” unused vitest import `beforeEach`. | ๐ŸŸข | โœ… | this commit | +| A13 | `dashboards/tracker-web/src/__tests__/tracker-proxy.test.ts` โ€” unused local `url` (renamed to `_url`). | ๐ŸŸข | โœ… | this commit | + +These all matter because `pnpm -r exec eslint` bails on the first package +failure, so the 45-error design-tokens issue was hiding everything below it. +Now the pipeline runs to completion and **surfaces the real lint debt** (next +section). + +## P. Pre-existing lint debt now visible + +After section A's structural unblocks the workspace-wide lint reports: + +``` +โœ– 181 problems (85 errors, 96 warnings) +``` + +Errors break down by rule (top 4): + +| Rule | Count | Notes | +| ----------------------------------- | ----: | ------------------------------------------------------------------ | +| `@typescript-eslint/no-unused-vars` | 67 | Mostly unused imports / unused destructured params in tests + code | +| `prefer-const` | 7 | `let` declarations never reassigned | +| `no-redeclare` | 7 | Likely identifiers shadowing globals / re-imports | +| `no-useless-escape` | 4 | Regex / string escapes that are no-ops | + +All of these are autofixable for the most part (`pnpm lint:fix`) **except** +where renaming an unused var changes a public API surface. They span multiple +packages I don't have working knowledge of (auth, llm, cosmos, billing-service, +tracker-service, growth-service, etc.). I'm not fixing them blind in this pass +โ€” each one is a one-liner but a wrong rename can break a downstream consumer. + +**Recommendation**: a follow-up sweep where the package owner runs +`pnpm --filter lint:fix`, eyeballs the diff, and commits per package. +Should be 6โ€“10 small commits. + +## W. Pre-existing lint warnings (96) + +Mostly `no-console` in CLI tools (`create-app`, `keyvault`, `sidecar-monitor`, +`generate.ts`, `roadmap/page.tsx`) and one `no-explicit-any` in `api-client`. +These are intentional in CLI scripts (we already disabled `no-console` for +`**/scripts/**` in this audit's eslint config change), and the rest are case-by- +case judgment calls. Not blocking. + +## R. Repo-state observations (not fixed) + +| # | Observation | Severity | Status | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------: | :----------------: | +| R1 | Working tree had 3 uncommitted edits when the audit started: `docker-compose.ecosystem.yml` (removes `nomgap-web` from Docker), `products/nomgap/product.json` (replaces flag set + adds containers), `services/platform-service/src/modules/flags/seed.ts` (+14 lines of flags). These look like an in-progress nomgap-on-Vercel migration. **Not touched** โ€” out of audit scope and missing context. | โ€” | โฌœ | +| R2 | Local `main` was 17 commits behind `origin/main` at the start of the session. Backup branch `backup/main-20260504-062733` was taken from `origin/main` (the source of truth) โ€” local stale main was _not_ backed up. | ๐ŸŸข | โœ… (backup exists) | +| R3 | `.npmrc` references `${GITEA_NPM_TOKEN}` โ€” pnpm prints a noisy WARN if the env var is unset. Cosmetic; the install still resolves the public packages it can. | ๐ŸŸข | โฌœ | +| R4 | `pnpm install -r` reports 4 missing peer warnings for `@azure/core-client@^1.10.0`. Adding it as an explicit dep on the two services that use `@azure/cosmos` would silence the warning. | ๐ŸŸข | โฌœ | + +--- + +## Ordering of fixes + +1. **Section A** (this commit) โ€” structural unblocks so `pnpm lint` runs end-to-end again. +2. **Section P** โ€” per-package `lint:fix` sweeps owned by each package's + maintainer. Each package = one commit. ~6โ€“10 commits. +3. **Section W** โ€” case-by-case `no-console` review (defer; warnings only). +4. **Section R** โ€” chore-level housekeeping (peer deps, .npmrc). diff --git a/eslint.config.js b/eslint.config.js index 244807b0..40fd2b28 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -106,6 +106,8 @@ export default [ NodeList: 'readonly', DOMRect: 'readonly', SVGElement: 'readonly', + XMLHttpRequest: 'readonly', + ProgressEvent: 'readonly', }, }, plugins: { @@ -137,4 +139,32 @@ export default [ // 'sort-imports': ['error', { ignoreDeclarationSort: true }], }, }, + // Node CJS scripts (build helpers, validators) and `**/scripts/**/*.js` โ€” + // these run on Node directly and need the Node globals (process, console, + // require, module, __dirname). Without this block they trip 40+ no-undef + // errors and break `pnpm lint` for the whole workspace. + { + files: ['**/*.cjs', '**/scripts/**/*.js', '**/scripts/**/*.cjs'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'commonjs', + globals: { + process: 'readonly', + Buffer: 'readonly', + console: 'readonly', + require: 'readonly', + module: 'readonly', + exports: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + setTimeout: 'readonly', + clearTimeout: 'readonly', + setInterval: 'readonly', + clearInterval: 'readonly', + }, + }, + rules: { + 'no-console': 'off', // CLI scripts legitimately print to stdout + }, + }, ]; diff --git a/packages/design-tokens/scripts/token-coverage.cjs b/packages/design-tokens/scripts/token-coverage.cjs index e021612a..447852e7 100755 --- a/packages/design-tokens/scripts/token-coverage.cjs +++ b/packages/design-tokens/scripts/token-coverage.cjs @@ -39,7 +39,7 @@ function findFiles(dir, files = []) { files.push(fullPath); } } - } catch (e) { + } catch { // Directory might not exist } return files; diff --git a/packages/design-tokens/scripts/validate-tokens.cjs b/packages/design-tokens/scripts/validate-tokens.cjs index 851bb99c..13e22b9f 100755 --- a/packages/design-tokens/scripts/validate-tokens.cjs +++ b/packages/design-tokens/scripts/validate-tokens.cjs @@ -27,7 +27,7 @@ function findFiles(dir, files = []) { files.push(fullPath); } } - } catch (e) { + } catch { // Directory might not exist or be accessible } return files; diff --git a/packages/feedback-client/src/index.ts b/packages/feedback-client/src/index.ts index 531a0d2a..52f1876d 100644 --- a/packages/feedback-client/src/index.ts +++ b/packages/feedback-client/src/index.ts @@ -1,6 +1,6 @@ /** * Feedback Client โ€” TypeScript SDK for user feedback with screenshots - * + * * @module @bytelyst/feedback-client */ @@ -90,7 +90,7 @@ export class FeedbackClient { /** * Submit feedback with optional screenshot - * + * * Flow: * 1. If screenshot provided, get SAS URL * 2. Upload screenshot to blob storage @@ -105,12 +105,8 @@ export class FeedbackClient { // Step 1 & 2: Handle screenshot upload if provided if (params.screenshot) { const sas = await this.generateSasUrl(params.screenshot.contentType); - - await this.uploadScreenshot( - sas.uploadUrl, - params.screenshot.blob, - onProgress - ); + + await this.uploadScreenshot(sas.uploadUrl, params.screenshot.blob, onProgress); screenshotMeta = { blobPath: sas.blobPath, @@ -132,7 +128,10 @@ export class FeedbackClient { appVersion: params.appVersion, platform: params.platform, screenshotBlobPath: screenshotMeta?.blobPath, - screenshotContentType: screenshotMeta?.contentType as 'image/png' | 'image/jpeg' | 'image/webp', + screenshotContentType: screenshotMeta?.contentType as + | 'image/png' + | 'image/jpeg' + | 'image/webp', screenshotSizeBytes: screenshotMeta?.sizeBytes, deviceContext: params.deviceContext, }), @@ -221,7 +220,7 @@ export class FeedbackClient { /** * Capture screenshot of current page or element (Web only) - * + * * Uses native getDisplayMedia for screen capture or html2canvas-style * DOM serialization for element capture. */ @@ -232,7 +231,8 @@ export class FeedbackClient { } const format = options.format || 'png'; - const mimeType = format === 'png' ? 'image/png' : format === 'jpeg' ? 'image/jpeg' : 'image/webp'; + const mimeType = + format === 'png' ? 'image/png' : format === 'jpeg' ? 'image/jpeg' : 'image/webp'; // If selector provided, capture specific element if (options.selector) { @@ -257,7 +257,7 @@ export class FeedbackClient { // Create video element to capture frame const video = document.createElement('video'); video.srcObject = stream; - + // Wait for video to load await new Promise((resolve, reject) => { video.onloadedmetadata = () => { @@ -275,7 +275,7 @@ export class FeedbackClient { canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('Failed to get canvas context'); - + ctx.drawImage(video, 0, 0); // Stop all tracks @@ -285,7 +285,7 @@ export class FeedbackClient { const quality = mimeType === 'image/jpeg' ? 0.9 : undefined; const blob = await new Promise((resolve, reject) => { canvas.toBlob( - (b) => b ? resolve(b) : reject(new Error('Canvas toBlob failed')), + b => (b ? resolve(b) : reject(new Error('Canvas toBlob failed'))), mimeType, quality ); @@ -298,17 +298,22 @@ export class FeedbackClient { height: canvas.height, }; } catch (err) { - throw new Error(`Screen capture failed: ${err instanceof Error ? err.message : String(err)}`); + throw new Error( + `Screen capture failed: ${err instanceof Error ? err.message : String(err)}`, + { cause: err } + ); } } /** - * Capture specific DOM element using html-to-image approach + * Capture specific DOM element using html-to-image approach. + * `mimeType` and `quality` are accepted for API parity with captureScreen() + * but are not yet honoured here (this path always returns the fallback PNG). */ private async captureElement( selector: string, - mimeType: string, - quality?: number + _mimeType: string, + _quality?: number ): Promise { const element = document.querySelector(selector); if (!element) { @@ -318,7 +323,7 @@ export class FeedbackClient { // Use html-to-image or similar approach // For now, we'll use a simple canvas-based approach for visible elements const rect = element.getBoundingClientRect(); - + // Create canvas const canvas = document.createElement('canvas'); canvas.width = rect.width; @@ -330,14 +335,14 @@ export class FeedbackClient { // This is a simplified implementation throw new Error( 'Element capture requires html2canvas library. ' + - 'Please install: npm install html2canvas ' + - 'Then use: html2canvas(element).then(canvas => canvas.toBlob(...))' + 'Please install: npm install html2canvas ' + + 'Then use: html2canvas(element).then(canvas => canvas.toBlob(...))' ); } /** * Capture and submit feedback in one operation - * + * * @example * // Capture full screen and submit * const result = await client.captureAndSubmit({ @@ -345,7 +350,7 @@ export class FeedbackClient { * title: 'Something is broken', * body: 'Description of the issue' * }); - * + * * @example * // Capture specific element * const result = await client.captureAndSubmit({ @@ -365,12 +370,15 @@ export class FeedbackClient { const capture = await this.captureScreenshot(screenshotOptions); // Submit with captured screenshot - return this.submitWithScreenshot({ - ...params, - screenshot: { - blob: capture.blob, - contentType: capture.contentType, + return this.submitWithScreenshot( + { + ...params, + screenshot: { + blob: capture.blob, + contentType: capture.contentType, + }, }, - }, onProgress); + onProgress + ); } } diff --git a/packages/logger/src/__tests__/logger.test.ts b/packages/logger/src/__tests__/logger.test.ts index b6ec896d..4d44a779 100644 --- a/packages/logger/src/__tests__/logger.test.ts +++ b/packages/logger/src/__tests__/logger.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { createLogger } from '../logger.js'; -import type { Logger, LoggerConfig } from '../types.js'; +import type { Logger } from '../types.js'; describe('createLogger', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/ui/eslint.config.js b/packages/ui/eslint.config.js index fc017d43..5835cb45 100644 --- a/packages/ui/eslint.config.js +++ b/packages/ui/eslint.config.js @@ -1,7 +1,23 @@ +// Package-local override for @bytelyst/ui. +// Flat config does NOT inherit the root file when ESLint discovers a config +// here, so the parser must be declared explicitly or ESLint falls back to +// the espree default which can't parse TypeScript syntax (`interface`, +// type-only imports, etc.) and emits 38 parse errors. +import tsparser from '@typescript-eslint/parser'; + export default [ + // Ignore build artifacts (root config's ignores aren't inherited). { - files: ['src/**/*.{ts,tsx}'], + ignores: ['dist/**', 'node_modules/**', 'coverage/**'], + }, + { + files: ['src/**/*.{ts,tsx}', '.storybook/**/*.{ts,tsx}'], languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, globals: { HTMLButtonElement: 'readonly', HTMLSpanElement: 'readonly',