@zohodesk/codestandard-validator 1.1.4 → 1.2.4-exp-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/bin/cliCI.js +0 -0
  2. package/build/ai/config.example.json +10 -0
  3. package/build/ai/ollama-service.js +403 -0
  4. package/build/ai/prompts/CODE_REVIEW_PROMPT.md +72 -0
  5. package/build/ai/prompts/PROMPT1.MD +70 -0
  6. package/build/ai/prompts/PROMPT2.md +159 -0
  7. package/build/ai/prompts/PROMPT3.md +64 -0
  8. package/build/ai/prompts/PROMPT4.md +64 -0
  9. package/build/ai/provider-factory.js +19 -0
  10. package/build/ai/providers/OllamaProvider.js +106 -0
  11. package/build/ai/render.js +157 -0
  12. package/build/ai/run-review.js +50 -0
  13. package/build/chunk/chunk_Restriction.js +202 -0
  14. package/build/hooks/Precommit/pre-commit-default.js +158 -140
  15. package/build/hooks/hook.js +6 -5
  16. package/build/lib/postinstall.js +6 -10
  17. package/build/mutation/branchDiff.js +178 -0
  18. package/build/mutation/fileResolver.js +170 -0
  19. package/build/mutation/index.js +16 -0
  20. package/build/mutation/mutatePattern.json +3 -0
  21. package/build/mutation/mutationCli.js +111 -0
  22. package/build/mutation/mutationRunner.js +208 -0
  23. package/build/mutation/reportGenerator.js +72 -0
  24. package/build/mutation/strykerWrapper.js +180 -0
  25. package/build/utils/FileAndFolderOperations/filterFiles.js +8 -6
  26. package/build/utils/FileAndFolderOperations/removeFolder.js +2 -2
  27. package/build/utils/General/Config.js +25 -0
  28. package/build/utils/General/SonarQubeUtil.js +1 -1
  29. package/jest.config.js +1 -1
  30. package/package.json +4 -1
  31. package/samples/sample-branch-mode.js +34 -0
  32. package/samples/sample-cli-entry.js +34 -0
  33. package/samples/sample-components.js +63 -0
  34. package/samples/sample-directory-mode.js +30 -0
  35. package/samples/sample-runner-direct.js +32 -0
  36. package/samples/sample-with-api.js +44 -0
package/bin/cliCI.js CHANGED
File without changes
@@ -0,0 +1,10 @@
1
+ {
2
+ "provider": "ollama",
3
+ "baseUrl": "http://127.0.0.1:11434",
4
+ "endpoint": "/api/generate",
5
+ "model": "deepseek-r1:8b",
6
+ "temperature": 0.2,
7
+ "maxTokens": 4096,
8
+ "timeoutMs": 120000,
9
+ "stream": true
10
+ }
@@ -0,0 +1,403 @@
1
+ "use strict";
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const {
6
+ createProvider
7
+ } = require('./provider-factory');
8
+ const {
9
+ printMarkdown,
10
+ formatReviewResult,
11
+ renderMarkdown
12
+ } = require('./render');
13
+ function sanitizeStreamText(text) {
14
+ if (!text) return '';
15
+ return String(text).replace(/<think>[\s\S]*?<\/think>/gi, '').replace(/<\s*think[^>]*>/gi, '').replace(/<\/\s*think\s*>/gi, '').replace(/[^\S\r\n]+/g, ' ') // collapse excessive spaces
16
+ .replace(/\s*\n+\s*/g, '\n') // normalize newlines
17
+ .trim();
18
+ }
19
+
20
+ /**
21
+ * Ollama API Configuration Schema
22
+ * Ref: https://github.com/ollama/ollama/blob/main/docs/api.md#generate-a-completion
23
+ */
24
+ const API_CONFIG_SCHEMA = {
25
+ baseUrl: {
26
+ type: 'string',
27
+ default: 'http://localhost:11434',
28
+ required: true
29
+ },
30
+ model: {
31
+ type: 'string',
32
+ default: 'codellama',
33
+ required: true
34
+ },
35
+ endpoint: {
36
+ type: 'string',
37
+ default: '/api/generate',
38
+ required: true
39
+ },
40
+ temperature: {
41
+ type: 'number',
42
+ default: 0.3,
43
+ min: 0,
44
+ max: 1
45
+ },
46
+ maxTokens: {
47
+ type: 'number',
48
+ default: 4096,
49
+ min: 256,
50
+ max: 16384
51
+ },
52
+ timeoutMs: {
53
+ type: 'number',
54
+ default: 120000
55
+ },
56
+ stream: {
57
+ type: 'boolean',
58
+ default: true
59
+ }
60
+ };
61
+
62
+ /**
63
+ * Reads all .md files from a folder, sorted alphabetically, and concatenates their content.
64
+ * @param {string} promptFolder - Absolute or relative path to the prompts folder
65
+ * @returns {string} Combined prompt text
66
+ */
67
+ function loadPromptsFromFolder(promptFolder) {
68
+ const resolvedPath = path.resolve(promptFolder);
69
+ if (!fs.existsSync(resolvedPath)) {
70
+ throw new Error(`Prompt folder not found: "${resolvedPath}"`);
71
+ }
72
+ const stat = fs.statSync(resolvedPath);
73
+ if (!stat.isDirectory()) {
74
+ throw new Error(`Prompt path is not a directory: "${resolvedPath}"`);
75
+ }
76
+ const mdFiles = fs.readdirSync(resolvedPath).filter(file => path.extname(file).toLowerCase() === '.md').sort();
77
+ if (mdFiles.length === 0) {
78
+ throw new Error(`No .md prompt files found in: "${resolvedPath}"`);
79
+ }
80
+ const combined = mdFiles.map(file => {
81
+ const filePath = path.join(resolvedPath, file);
82
+ const content = fs.readFileSync(filePath, 'utf-8').trim();
83
+ return `<!-- prompt: ${file} -->\n${content}`;
84
+ });
85
+ return combined.join('\n\n');
86
+ }
87
+
88
+ /**
89
+ * Reads a single file and returns its content with metadata.
90
+ * @param {string} filePath
91
+ * @returns {{ filePath: string, fileName: string, content: string, extension: string }}
92
+ */
93
+ function readFileForReview(filePath) {
94
+ const resolvedPath = path.resolve(filePath);
95
+ if (!fs.existsSync(resolvedPath)) {
96
+ throw new Error(`File not found: "${resolvedPath}"`);
97
+ }
98
+ const stat = fs.statSync(resolvedPath);
99
+ if (!stat.isFile()) {
100
+ throw new Error(`Path is not a file: "${resolvedPath}"`);
101
+ }
102
+ return {
103
+ filePath: resolvedPath,
104
+ fileName: path.basename(resolvedPath),
105
+ content: fs.readFileSync(resolvedPath, 'utf-8'),
106
+ extension: path.extname(resolvedPath).slice(1)
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Reads all files from a folder (non-recursive) for review.
112
+ * @param {string} folderPath
113
+ * @param {string[]} [extensions] - Optional filter, e.g. ['.js', '.ts']
114
+ * @returns {Array<{ filePath: string, fileName: string, content: string, extension: string }>}
115
+ */
116
+ function readFilesFromFolder(folderPath, extensions = []) {
117
+ const resolvedPath = path.resolve(folderPath);
118
+ if (!fs.existsSync(resolvedPath)) {
119
+ throw new Error(`Folder not found: "${resolvedPath}"`);
120
+ }
121
+ const stat = fs.statSync(resolvedPath);
122
+ if (!stat.isDirectory()) {
123
+ throw new Error(`Path is not a directory: "${resolvedPath}"`);
124
+ }
125
+ const entries = fs.readdirSync(resolvedPath);
126
+ return entries.filter(entry => {
127
+ const fullPath = path.join(resolvedPath, entry);
128
+ if (!fs.statSync(fullPath).isFile()) return false;
129
+ if (extensions.length === 0) return true;
130
+ return extensions.includes(path.extname(entry).toLowerCase());
131
+ }).map(entry => readFileForReview(path.join(resolvedPath, entry)));
132
+ }
133
+
134
+ /**
135
+ * Validates and merges user config with schema defaults.
136
+ * @param {Object} userConfig
137
+ * @returns {Object} validated config
138
+ */
139
+ function buildConfig(userConfig = {}) {
140
+ const config = {};
141
+
142
+ // Pass through provider field for factory
143
+ if (userConfig.provider) {
144
+ config.provider = userConfig.provider;
145
+ }
146
+ for (const [key, schema] of Object.entries(API_CONFIG_SCHEMA)) {
147
+ const value = userConfig[key];
148
+ if (value === undefined || value === null) {
149
+ if (schema.required && schema.default === undefined) {
150
+ throw new Error(`Missing required config: "${key}"`);
151
+ }
152
+ config[key] = schema.default;
153
+ continue;
154
+ }
155
+ if (typeof value !== schema.type) {
156
+ throw new TypeError(`Config "${key}" must be of type ${schema.type}, got ${typeof value}`);
157
+ }
158
+ if (schema.min !== undefined && value < schema.min) {
159
+ throw new RangeError(`Config "${key}" must be >= ${schema.min}`);
160
+ }
161
+ if (schema.max !== undefined && value > schema.max) {
162
+ throw new RangeError(`Config "${key}" must be <= ${schema.max}`);
163
+ }
164
+ config[key] = value;
165
+ }
166
+ return config;
167
+ }
168
+
169
+ /**
170
+ * Builds the full review prompt with injected code.
171
+ * Uses custom prompt from folder if provided, otherwise falls back to default.
172
+ * @param {string} code - The source code to inject
173
+ * @param {string} [promptFolder] - Optional path to folder containing .md prompt files
174
+ * @returns {string}
175
+ */
176
+ function buildPrompt(code, promptFolder) {
177
+ if (!code || typeof code !== 'string') {
178
+ throw new Error('Code input must be a non-empty string');
179
+ }
180
+ let basePrompt;
181
+ if (promptFolder) {
182
+ basePrompt = loadPromptsFromFolder(promptFolder);
183
+ if (!basePrompt.includes('{{CODE}}')) {
184
+ basePrompt += `\n\n## Code to Review\n\`\`\`\n{{CODE}}\n\`\`\``;
185
+ }
186
+ } else {
187
+ basePrompt = DEFAULT_REVIEW_PROMPT + `\n\n## Code to Review\n\`\`\`\n{{CODE}}\n\`\`\``;
188
+ }
189
+
190
+ // Force JSON-only output instruction at the end to reduce drift
191
+ basePrompt += `
192
+
193
+ CRITICAL: Output ONLY a single valid JSON object. No markdown, tables, or extra text. If streaming, emit NDJSON lines containing only JSON fragments or the final JSON object. Do not include <think> or hidden content.`;
194
+ return basePrompt.replace('{{CODE}}', code.trim());
195
+ }
196
+
197
+ /**
198
+ * Parses JSON from Ollama response text, handling markdown fences.
199
+ * @param {string} text
200
+ * @param {Object} options
201
+ * @returns {Object}
202
+ */
203
+ function parseReviewResponse(text, {
204
+ fileName
205
+ }) {
206
+ let cleaned = text.trim();
207
+
208
+ //Strip <think>...</think> blocks (deepseek-r1 reasoning)
209
+ cleaned = cleaned.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
210
+
211
+ // Strip markdown code fences and extract JSON
212
+ const jsonFenceMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/);
213
+ if (jsonFenceMatch) {
214
+ cleaned = jsonFenceMatch[1].trim();
215
+ }
216
+
217
+ // Try to find a JSON object anywhere in the text
218
+ if (!cleaned.startsWith('{')) {
219
+ const jsonObjectMatch = cleaned.match(/(\{[\s\S]*\})/);
220
+ if (jsonObjectMatch) {
221
+ cleaned = jsonObjectMatch[1].trim();
222
+ }
223
+ }
224
+ try {
225
+ const parsed = JSON.parse(cleaned);
226
+ parsed.fileName = fileName;
227
+ return parsed;
228
+ } catch {
229
+ return {
230
+ fileName,
231
+ overallScore: null,
232
+ categories: null,
233
+ criticalIssues: [],
234
+ suggestions: [],
235
+ summary: cleaned,
236
+ rawResponse: true
237
+ };
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Fetches a code review from the configured AI provider.
243
+ *
244
+ * @param {string} code - Source code to review
245
+ * @param {Object} [options]
246
+ * @param {Object} [options.config] - API config overrides
247
+ * @param {string} [options.promptFolder] - Path to folder with .md prompt files
248
+ * @param {string} [options.fileName] - Original file name for context
249
+ * @param {Function} [options.onToken] - Callback for streaming tokens
250
+ * @returns {Promise<Object>} Structured review result
251
+ */
252
+ async function fetchCodeReview(code, options = {}) {
253
+ const {
254
+ config: userConfig = {},
255
+ promptFolder,
256
+ fileName,
257
+ onToken
258
+ } = options;
259
+ const config = buildConfig(userConfig);
260
+ const codeWithContext = fileName ? `// File: ${fileName}\n${code}` : code;
261
+ const prompt = buildPrompt(codeWithContext, promptFolder);
262
+ const provider = createProvider(config);
263
+ const {
264
+ text
265
+ } = await provider.generate({
266
+ prompt,
267
+ fileName,
268
+ onToken: chunk => {
269
+ // Forward raw token directly — let the caller decide how to handle it
270
+ if (chunk && typeof onToken === 'function') onToken(chunk);
271
+ }
272
+ });
273
+ const cleanText = sanitizeStreamText(text);
274
+ return parseReviewResponse(cleanText, {
275
+ fileName
276
+ });
277
+ }
278
+
279
+ /**
280
+ * Reviews a single file by path.
281
+ * @param {string} filePath - Path to the file to review
282
+ * @param {Object} [options]
283
+ * @param {Object} [options.config] - API config overrides
284
+ * @param {string} [options.promptFolder] - Path to folder with .md prompt files
285
+ * @param {Function} [options.onToken] - Callback for streaming tokens
286
+ * @returns {Promise<Object>}
287
+ */
288
+ async function reviewFile(filePath, options = {}) {
289
+ const file = readFileForReview(filePath);
290
+ return fetchCodeReview(file.content, {
291
+ ...options,
292
+ fileName: file.fileName
293
+ });
294
+ }
295
+
296
+ /**
297
+ * Reviews multiple files by their paths.
298
+ * @param {string[]} filePaths - Array of file paths to review
299
+ * @param {Object} [options]
300
+ * @param {Object} [options.config] - API config overrides
301
+ * @param {string} [options.promptFolder] - Path to folder with .md prompt files
302
+ * @param {number} [options.concurrency=1] - Number of concurrent reviews
303
+ * @param {boolean} [options.streamToConsole] - Whether to stream tokens to stdout
304
+ * @param {Function} [options.onResult] - Callback per completed review
305
+ * @returns {Promise<Object[]>} Array of review results
306
+ */
307
+ async function reviewFiles(filePaths, options = {}) {
308
+ const {
309
+ concurrency = 1,
310
+ onResult,
311
+ streamToConsole,
312
+ ...reviewOptions
313
+ } = options;
314
+ const results = [];
315
+ for (let i = 0; i < filePaths.length; i += concurrency) {
316
+ const batch = filePaths.slice(i, i + concurrency);
317
+ const promises = batch.map(fp => reviewFile(fp, {
318
+ ...reviewOptions,
319
+ onToken: token => {
320
+ if (!streamToConsole) return;
321
+ if (!token) return;
322
+ // Write raw token directly to stdout during streaming
323
+ process.stdout.write(token);
324
+ }
325
+ }).then(value => {
326
+ // Print newline after stream ends for this file
327
+ if (streamToConsole) process.stdout.write('\n');
328
+ return {
329
+ status: 'fulfilled',
330
+ value,
331
+ filePath: fp
332
+ };
333
+ }).catch(error => {
334
+ if (streamToConsole) process.stdout.write('\n');
335
+ return {
336
+ status: 'rejected',
337
+ reason: error,
338
+ filePath: fp
339
+ };
340
+ }));
341
+ for (const p of promises) {
342
+ p.then(r => {
343
+ let out;
344
+ if (r.status === 'fulfilled') {
345
+ out = typeof r.value === 'string' ? {
346
+ message: sanitizeStreamText(r.value)
347
+ } : r.value;
348
+ results.push(out);
349
+ } else {
350
+ var _r$reason;
351
+ const fp = r.filePath;
352
+ out = {
353
+ success: false,
354
+ fileName: path.basename(fp),
355
+ filePath: fp,
356
+ error: ((_r$reason = r.reason) === null || _r$reason === void 0 ? void 0 : _r$reason.message) || r.reason || 'Unknown error'
357
+ };
358
+ results.push(out);
359
+ }
360
+ if (typeof onResult === 'function') {
361
+ try {
362
+ onResult(out);
363
+ } catch {}
364
+ }
365
+ });
366
+ }
367
+ await Promise.allSettled(promises);
368
+ }
369
+ return results;
370
+ }
371
+
372
+ /**
373
+ * Reviews all files in a folder.
374
+ * @param {string} folderPath - Path to folder containing files to review
375
+ * @param {Object} [options]
376
+ * @param {Object} [options.config] - API config overrides
377
+ * @param {string} [options.promptFolder] - Path to folder with .md prompt files
378
+ * @param {string[]} [options.extensions] - File extensions to include, e.g. ['.js', '.ts']
379
+ * @param {number} [options.concurrency=1] - Number of concurrent reviews
380
+ * @returns {Promise<Object[]>}
381
+ */
382
+ async function reviewFolder(folderPath, options = {}) {
383
+ const {
384
+ extensions = [],
385
+ ...rest
386
+ } = options;
387
+ const files = readFilesFromFolder(folderPath, extensions);
388
+ const filePaths = files.map(f => f.filePath);
389
+ return reviewFiles(filePaths, rest);
390
+ }
391
+ module.exports = {
392
+ API_CONFIG_SCHEMA,
393
+ loadPromptsFromFolder,
394
+ readFileForReview,
395
+ readFilesFromFolder,
396
+ buildConfig,
397
+ buildPrompt,
398
+ parseReviewResponse,
399
+ fetchCodeReview,
400
+ reviewFile,
401
+ reviewFiles,
402
+ reviewFolder
403
+ };
@@ -0,0 +1,72 @@
1
+ You are an expert code reviewer. Analyze the provided code against the following 50-point checklist.
2
+ Score each applicable category (1-10) and provide specific feedback.
3
+
4
+ **CRITICAL INSTRUCTION: Your entire response MUST be a single valid JSON object. No explanation, no commentary, no markdown outside the JSON. Do not wrap in code fences. Output ONLY the JSON object.**
5
+
6
+ ## Review Categories
7
+
8
+ ### 1. Code Quality (10 points)
9
+ 1. Readability and clarity
10
+ 2. Consistent naming conventions
11
+ 3. Proper use of comments and documentation
12
+ 4. Code simplicity (no unnecessary complexity)
13
+ 5. DRY principle adherence
14
+ 6. Single Responsibility Principle
15
+ 7. Proper abstraction levels
16
+ 8. Clean function/method signatures
17
+ 9. Appropriate use of constants vs magic numbers
18
+ 10. Code formatting and indentation
19
+
20
+ ### 2. Error Handling (10 points)
21
+ 11. Proper try-catch usage
22
+ 12. Meaningful error messages
23
+ 13. Graceful degradation
24
+ 14. Input validation
25
+ 15. Null/undefined safety
26
+ 16. Boundary condition handling
27
+ 17. Async error handling
28
+ 18. Error propagation strategy
29
+ 19. Logging of errors
30
+ 20. User-facing error responses
31
+
32
+ ### 3. Performance (10 points)
33
+ 21. Algorithm efficiency (time complexity)
34
+ 22. Memory usage optimization
35
+ 23. Avoiding unnecessary computations
36
+ 24. Efficient data structures
37
+ 25. Lazy loading / deferred execution
38
+ 26. Caching strategy
39
+ 27. Loop optimization
40
+ 28. Avoiding memory leaks
41
+ 29. Database/API call optimization
42
+ 30. Bundle size / import efficiency
43
+
44
+ ### 4. Security (10 points)
45
+ 31. Input sanitization
46
+ 32. SQL/NoSQL injection prevention
47
+ 33. XSS prevention
48
+ 34. Authentication/authorization checks
49
+ 35. Sensitive data exposure
50
+ 36. Dependency vulnerability awareness
51
+ 37. CSRF protection
52
+ 38. Secure communication (HTTPS, encryption)
53
+ 39. Proper secret management
54
+ 40. Rate limiting / abuse prevention
55
+
56
+ ### 5. Maintainability (10 points)
57
+ 41. Modularity and reusability
58
+ 42. Testability
59
+ 43. Configuration externalization
60
+ 44. Dependency management
61
+ 45. Version compatibility
62
+ 46. Backward compatibility
63
+ 47. Documentation completeness
64
+ 48. Changelog / migration notes
65
+ 49. Linting / formatting rules followed
66
+ 50. CI/CD readiness
67
+
68
+ ## Required Output
69
+
70
+ Respond with ONLY this JSON structure (no other text before or after):
71
+ {"overallScore":<number 0-50>,"categories":{"codeQuality":{"score":<0-10>,"issues":[<string>,…]},"errorHandling":{"score":<0-10>,"issues":[<string>,…]},"performance":{"score":<0-10>,"issues":[<string>,…]},"security":{"score":<0-10>,"issues":[<string>,…]},"maintainability":{"score":<0-10>,"issues":[<string>,…]}},"criticalIssues":[<string>,…],"suggestions":[<string>,…],"summary":"<brief overall summary>"}
72
+
@@ -0,0 +1,70 @@
1
+ ## Components & CSS Files Review Checklist
2
+
3
+
4
+ ## Folder Structure Validation
5
+ All UAT-related files must be placed under the `{appName}/uat` directory.
6
+
7
+ The structure must follow:
8
+ - `{ComponentName}.css` – File names ends with
9
+ - it should closet to compponet file
10
+
11
+
12
+ ### 1. Prop Types Definition
13
+ Props must be defined with types in `{ComponentName}.propTypes.js`.
14
+ Prop names must be raw, common HTML-based names and not application-specific.
15
+
16
+ ### 2. Component Structure
17
+ `{ComponentName}.propTypes.js` must be imported into `{ComponentName}.js`.
18
+ Component name must start with a capital letter and must be a functional component (no class components).
19
+
20
+ ### 3. Component Purity
21
+ No Redux, state, or application logic is allowed in `{ComponentName}.js`.
22
+ Hook or functional logic must be moved to `{ComponentName}.behaviour.js` or `{ComponentName}.hook.js` and imported.
23
+
24
+ ### 4. Code Cleanliness
25
+ No duplicate lines, unused variables, unused methods, unused imports, or commented code.
26
+ Keep the component clean and minimal.
27
+
28
+ ### 5. Linting & Hooks Validation
29
+ Code must pass linter checks.
30
+ React hooks must follow proper usage rules and performance best practices.
31
+
32
+ ### 6. CSS Imports
33
+ CSS files must be imported as `styles` from `./{ComponentName}.css` in `{ComponentName}.js`
34
+ Other naming conventions are allowed if consistently used.
35
+
36
+ ### 7. CSS Standards & Naming
37
+ CSS must follow BEM naming conventions and be compatible with CSS Modules.
38
+ Class names must be HTML/component-based and not application-specific.
39
+
40
+ ### 8. CSS Variables & Layout Rules
41
+ All colors, fonts, widths, heights, and spacing must use variables only.
42
+ Width and height definitions must include appropriate min and max values.
43
+
44
+ ### 9. CSS Selector Rules
45
+ Do not use units with zero values (`0` instead of `0px` or `0rem`).
46
+ Limit selector nesting depth to the allowed maximum.
47
+ Do not use ID selectors—use class selectors only.
48
+ Do not use TAG selectors—use class selectors only. if required the file line must have proper comment.
49
+
50
+ ### 10. CSS Quality Rules
51
+ Avoid adjoining class selectors and prefer reusable single classes.
52
+ Avoid duplicate CSS properties, empty blocks, empty values, unknown or misspelled properties.
53
+ Do not use `!important`; if required, add a same-line comment explaining why.
54
+
55
+ ### 11. CSS Content Rules
56
+ Always use double quotes for the `content` property.
57
+ Only valid and supported CSS properties are allowed.
58
+
59
+ ### 12. HTML & Accessibility
60
+ HTML must follow accessibility (a11y) standards.
61
+ Avoid duplicate attributes and follow semantic HTML.
62
+ Reuse existing Haas or common components instead of rewriting code.
63
+
64
+ ### 13. Events & Text Handling
65
+ Event bindings must not create new inline functions.
66
+ No hard-coded English text is allowed; all text must be passed via props.
67
+
68
+ ### 14. Documentation
69
+ A `{ComponentName}.docs.js` file must be added and used for documentation.
70
+ Component usage and behavior must be clearly documented.