@webpieces/ai-hooks 0.0.1

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 (72) hide show
  1. package/README.md +43 -0
  2. package/bin/setup-ai-hooks.sh +137 -0
  3. package/openclaw.plugin.json +15 -0
  4. package/package.json +35 -0
  5. package/src/adapters/claude-code-hook.d.ts +1 -0
  6. package/src/adapters/claude-code-hook.js +97 -0
  7. package/src/adapters/claude-code-hook.js.map +1 -0
  8. package/src/adapters/openclaw-plugin.d.ts +14 -0
  9. package/src/adapters/openclaw-plugin.js +73 -0
  10. package/src/adapters/openclaw-plugin.js.map +1 -0
  11. package/src/core/build-context.d.ts +7 -0
  12. package/src/core/build-context.js +58 -0
  13. package/src/core/build-context.js.map +1 -0
  14. package/src/core/configs/default.d.ts +2 -0
  15. package/src/core/configs/default.js +22 -0
  16. package/src/core/configs/default.js.map +1 -0
  17. package/src/core/disable-directives.d.ts +9 -0
  18. package/src/core/disable-directives.js +92 -0
  19. package/src/core/disable-directives.js.map +1 -0
  20. package/src/core/load-config.d.ts +3 -0
  21. package/src/core/load-config.js +81 -0
  22. package/src/core/load-config.js.map +1 -0
  23. package/src/core/load-rules.d.ts +3 -0
  24. package/src/core/load-rules.js +131 -0
  25. package/src/core/load-rules.js.map +1 -0
  26. package/src/core/rejection-log.d.ts +2 -0
  27. package/src/core/rejection-log.js +146 -0
  28. package/src/core/rejection-log.js.map +1 -0
  29. package/src/core/report.d.ts +2 -0
  30. package/src/core/report.js +34 -0
  31. package/src/core/report.js.map +1 -0
  32. package/src/core/rules/catch-error-pattern.d.ts +3 -0
  33. package/src/core/rules/catch-error-pattern.js +91 -0
  34. package/src/core/rules/catch-error-pattern.js.map +1 -0
  35. package/src/core/rules/file-location.d.ts +3 -0
  36. package/src/core/rules/file-location.js +73 -0
  37. package/src/core/rules/file-location.js.map +1 -0
  38. package/src/core/rules/index.d.ts +1 -0
  39. package/src/core/rules/index.js +13 -0
  40. package/src/core/rules/index.js.map +1 -0
  41. package/src/core/rules/max-file-lines.d.ts +3 -0
  42. package/src/core/rules/max-file-lines.js +131 -0
  43. package/src/core/rules/max-file-lines.js.map +1 -0
  44. package/src/core/rules/no-any-unknown.d.ts +3 -0
  45. package/src/core/rules/no-any-unknown.js +30 -0
  46. package/src/core/rules/no-any-unknown.js.map +1 -0
  47. package/src/core/rules/no-destructure.d.ts +3 -0
  48. package/src/core/rules/no-destructure.js +30 -0
  49. package/src/core/rules/no-destructure.js.map +1 -0
  50. package/src/core/rules/no-unmanaged-exceptions.d.ts +3 -0
  51. package/src/core/rules/no-unmanaged-exceptions.js +41 -0
  52. package/src/core/rules/no-unmanaged-exceptions.js.map +1 -0
  53. package/src/core/rules/require-return-type.d.ts +3 -0
  54. package/src/core/rules/require-return-type.js +52 -0
  55. package/src/core/rules/require-return-type.js.map +1 -0
  56. package/src/core/runner.d.ts +2 -0
  57. package/src/core/runner.js +127 -0
  58. package/src/core/runner.js.map +1 -0
  59. package/src/core/strip-ts-noise.d.ts +1 -0
  60. package/src/core/strip-ts-noise.js +178 -0
  61. package/src/core/strip-ts-noise.js.map +1 -0
  62. package/src/core/to-error.d.ts +5 -0
  63. package/src/core/to-error.js +38 -0
  64. package/src/core/to-error.js.map +1 -0
  65. package/src/core/types.d.ts +89 -0
  66. package/src/core/types.js +90 -0
  67. package/src/core/types.js.map +1 -0
  68. package/src/index.d.ts +5 -0
  69. package/src/index.js +24 -0
  70. package/src/index.js.map +1 -0
  71. package/templates/claude-settings-hook.json +15 -0
  72. package/templates/webpieces.ai-hooks.seed.json +16 -0
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const types_1 = require("../types");
4
+ /**
5
+ * Matches a catch clause opening: } catch (paramName: typeAnnotation) {
6
+ * Captures: group 1 = param name, group 2 = type annotation (if present)
7
+ */
8
+ const CATCH_PATTERN = /\bcatch\s*\(\s*(\w+)(?:\s*:\s*(\w+))?\s*\)/;
9
+ /**
10
+ * Matches the required toError first statement (with or without comment-out).
11
+ * Group 1 = variable name, group 2 = param passed to toError
12
+ */
13
+ const TO_ERROR_PATTERN = /^\s*(?:\/\/\s*)?const\s+(\w+)\s*=\s*toError\(\s*(\w+)\s*\)\s*;?\s*$/;
14
+ const catchErrorPatternRule = {
15
+ name: 'catch-error-pattern',
16
+ description: 'Catch blocks must use: catch (err: unknown) { const error = toError(err); }',
17
+ scope: 'edit',
18
+ files: ['**/*.ts', '**/*.tsx'],
19
+ defaultOptions: {},
20
+ fixHint: [
21
+ 'catch (err: unknown) { const error = toError(err); ... }',
22
+ 'Or to explicitly ignore: catch (err: unknown) { //const error = toError(err); }',
23
+ 'For nested catches: catch (err2: unknown) { const error2 = toError(err2); }',
24
+ '// webpieces-disable catch-error-pattern -- <reason>',
25
+ ],
26
+ check(ctx) {
27
+ const violations = [];
28
+ const lines = ctx.strippedLines;
29
+ for (let i = 0; i < lines.length; i += 1) {
30
+ const stripped = lines[i];
31
+ const catchMatch = CATCH_PATTERN.exec(stripped);
32
+ if (!catchMatch)
33
+ continue;
34
+ const lineNum = i + 1;
35
+ if (ctx.isLineDisabled(lineNum, 'catch-error-pattern'))
36
+ continue;
37
+ const actualParam = catchMatch[1];
38
+ const typeAnnotation = catchMatch[2];
39
+ // Determine expected names from suffix on the actual param (err, err2, err3...)
40
+ const suffixMatch = actualParam.match(/^err(\d*)$/);
41
+ const suffix = suffixMatch ? suffixMatch[1] : '';
42
+ const expectedParam = 'err' + suffix;
43
+ const expectedVar = 'error' + suffix;
44
+ // Check parameter name
45
+ if (actualParam !== expectedParam) {
46
+ violations.push(new types_1.Violation(lineNum, ctx.lines[i].trim(), `Catch parameter must be named "${expectedParam}" (or "err2", "err3" for nested catches), got "${actualParam}"`));
47
+ }
48
+ // Check type annotation is unknown
49
+ if (typeAnnotation !== 'unknown') {
50
+ const msg = typeAnnotation
51
+ ? `Catch parameter must be typed as "unknown": catch (${expectedParam}: unknown), got "${typeAnnotation}"`
52
+ : `Catch parameter must be typed as "unknown": catch (${expectedParam}: unknown)`;
53
+ violations.push(new types_1.Violation(lineNum, ctx.lines[i].trim(), msg));
54
+ }
55
+ // Find next non-blank line after the catch opening to check for toError
56
+ const toErrorResult = findToErrorStatement(lines, i + 1);
57
+ if (toErrorResult === 'not-found') {
58
+ violations.push(new types_1.Violation(lineNum, ctx.lines[i].trim(), `Catch block must call toError(${actualParam}) as first statement: const ${expectedVar} = toError(${actualParam}); or //const ${expectedVar} = toError(${actualParam});`));
59
+ }
60
+ else if (toErrorResult !== 'end-of-content') {
61
+ // Validate variable name and param match
62
+ if (toErrorResult.varName !== expectedVar) {
63
+ const toErrorLineNum = toErrorResult.lineIndex + 1;
64
+ violations.push(new types_1.Violation(toErrorLineNum, ctx.lines[toErrorResult.lineIndex].trim(), `Error variable must be named "${expectedVar}", got "${toErrorResult.varName}"`));
65
+ }
66
+ if (toErrorResult.paramName !== actualParam) {
67
+ const toErrorLineNum = toErrorResult.lineIndex + 1;
68
+ violations.push(new types_1.Violation(toErrorLineNum, ctx.lines[toErrorResult.lineIndex].trim(), `toError() must be called with "${actualParam}", got "${toErrorResult.paramName}"`));
69
+ }
70
+ }
71
+ }
72
+ return violations;
73
+ },
74
+ };
75
+ function findToErrorStatement(lines, startIndex) {
76
+ for (let j = startIndex; j < lines.length; j += 1) {
77
+ const line = lines[j].trim();
78
+ if (line === '' || line === '{')
79
+ continue;
80
+ const match = TO_ERROR_PATTERN.exec(line);
81
+ if (match) {
82
+ return { varName: match[1], paramName: match[2], lineIndex: j };
83
+ }
84
+ // First non-blank line is not a toError call
85
+ return 'not-found';
86
+ }
87
+ // Ran off the end of the edit content — can't validate further
88
+ return 'end-of-content';
89
+ }
90
+ exports.default = catchErrorPatternRule;
91
+ //# sourceMappingURL=catch-error-pattern.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catch-error-pattern.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hooks/src/core/rules/catch-error-pattern.ts"],"names":[],"mappings":"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","sourcesContent":["import type { EditRule, EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\n/**\n * Matches a catch clause opening: } catch (paramName: typeAnnotation) {\n * Captures: group 1 = param name, group 2 = type annotation (if present)\n */\nconst CATCH_PATTERN = /\\bcatch\\s*\\(\\s*(\\w+)(?:\\s*:\\s*(\\w+))?\\s*\\)/;\n\n/**\n * Matches the required toError first statement (with or without comment-out).\n * Group 1 = variable name, group 2 = param passed to toError\n */\nconst TO_ERROR_PATTERN = /^\\s*(?:\\/\\/\\s*)?const\\s+(\\w+)\\s*=\\s*toError\\(\\s*(\\w+)\\s*\\)\\s*;?\\s*$/;\n\nconst catchErrorPatternRule: EditRule = {\n name: 'catch-error-pattern',\n description: 'Catch blocks must use: catch (err: unknown) { const error = toError(err); }',\n scope: 'edit',\n files: ['**/*.ts', '**/*.tsx'],\n defaultOptions: {},\n fixHint: [\n 'catch (err: unknown) { const error = toError(err); ... }',\n 'Or to explicitly ignore: catch (err: unknown) { //const error = toError(err); }',\n 'For nested catches: catch (err2: unknown) { const error2 = toError(err2); }',\n '// webpieces-disable catch-error-pattern -- <reason>',\n ],\n\n check(ctx: EditContext): readonly Violation[] {\n const violations: V[] = [];\n const lines = ctx.strippedLines;\n\n for (let i = 0; i < lines.length; i += 1) {\n const stripped = lines[i];\n const catchMatch = CATCH_PATTERN.exec(stripped);\n if (!catchMatch) continue;\n\n const lineNum = i + 1;\n if (ctx.isLineDisabled(lineNum, 'catch-error-pattern')) continue;\n\n const actualParam = catchMatch[1];\n const typeAnnotation = catchMatch[2];\n\n // Determine expected names from suffix on the actual param (err, err2, err3...)\n const suffixMatch = actualParam.match(/^err(\\d*)$/);\n const suffix = suffixMatch ? suffixMatch[1] : '';\n const expectedParam = 'err' + suffix;\n const expectedVar = 'error' + suffix;\n\n // Check parameter name\n if (actualParam !== expectedParam) {\n violations.push(new V(\n lineNum,\n ctx.lines[i].trim(),\n `Catch parameter must be named \"${expectedParam}\" (or \"err2\", \"err3\" for nested catches), got \"${actualParam}\"`,\n ));\n }\n\n // Check type annotation is unknown\n if (typeAnnotation !== 'unknown') {\n const msg = typeAnnotation\n ? `Catch parameter must be typed as \"unknown\": catch (${expectedParam}: unknown), got \"${typeAnnotation}\"`\n : `Catch parameter must be typed as \"unknown\": catch (${expectedParam}: unknown)`;\n violations.push(new V(lineNum, ctx.lines[i].trim(), msg));\n }\n\n // Find next non-blank line after the catch opening to check for toError\n const toErrorResult = findToErrorStatement(lines, i + 1);\n if (toErrorResult === 'not-found') {\n violations.push(new V(\n lineNum,\n ctx.lines[i].trim(),\n `Catch block must call toError(${actualParam}) as first statement: const ${expectedVar} = toError(${actualParam}); or //const ${expectedVar} = toError(${actualParam});`,\n ));\n } else if (toErrorResult !== 'end-of-content') {\n // Validate variable name and param match\n if (toErrorResult.varName !== expectedVar) {\n const toErrorLineNum = toErrorResult.lineIndex + 1;\n violations.push(new V(\n toErrorLineNum,\n ctx.lines[toErrorResult.lineIndex].trim(),\n `Error variable must be named \"${expectedVar}\", got \"${toErrorResult.varName}\"`,\n ));\n }\n if (toErrorResult.paramName !== actualParam) {\n const toErrorLineNum = toErrorResult.lineIndex + 1;\n violations.push(new V(\n toErrorLineNum,\n ctx.lines[toErrorResult.lineIndex].trim(),\n `toError() must be called with \"${actualParam}\", got \"${toErrorResult.paramName}\"`,\n ));\n }\n }\n }\n\n return violations;\n },\n};\n\ninterface ToErrorMatch {\n varName: string;\n paramName: string;\n lineIndex: number;\n}\n\nfunction findToErrorStatement(lines: readonly string[], startIndex: number): ToErrorMatch | 'not-found' | 'end-of-content' {\n for (let j = startIndex; j < lines.length; j += 1) {\n const line = lines[j].trim();\n if (line === '' || line === '{') continue;\n\n const match = TO_ERROR_PATTERN.exec(line);\n if (match) {\n return { varName: match[1], paramName: match[2], lineIndex: j };\n }\n // First non-blank line is not a toError call\n return 'not-found';\n }\n // Ran off the end of the edit content — can't validate further\n return 'end-of-content';\n}\n\nexport default catchErrorPatternRule;\n"]}
@@ -0,0 +1,3 @@
1
+ import type { FileRule } from '../types';
2
+ declare const fileLocationRule: FileRule;
3
+ export default fileLocationRule;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const fs = tslib_1.__importStar(require("fs"));
5
+ const path = tslib_1.__importStar(require("path"));
6
+ const types_1 = require("../types");
7
+ const DEFAULT_EXCLUDE_PATHS = [
8
+ 'node_modules', 'dist', '.nx', '.git',
9
+ 'architecture', 'tmp', 'scripts',
10
+ ];
11
+ const DEFAULT_ALLOWED_ROOT_FILES = ['jest.setup.ts'];
12
+ function isNodeModulesDir(name) {
13
+ return name === 'node_modules' || name.startsWith('node_modules_');
14
+ }
15
+ function shouldSkipDir(name, excludePaths) {
16
+ if (isNodeModulesDir(name))
17
+ return true;
18
+ return excludePaths.indexOf(name) >= 0;
19
+ }
20
+ function findProjectRoot(filePath, workspaceRoot) {
21
+ let dir = path.dirname(filePath);
22
+ while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {
23
+ if (fs.existsSync(path.join(dir, 'project.json')))
24
+ return dir;
25
+ dir = path.dirname(dir);
26
+ }
27
+ return null;
28
+ }
29
+ const fileLocationRule = {
30
+ name: 'file-location',
31
+ description: 'Every .ts file must belong to a project\'s src/ directory.',
32
+ scope: 'file',
33
+ files: ['**/*.ts', '**/*.tsx'],
34
+ defaultOptions: {
35
+ excludePaths: DEFAULT_EXCLUDE_PATHS,
36
+ allowedRootFiles: DEFAULT_ALLOWED_ROOT_FILES,
37
+ },
38
+ fixHint: [
39
+ 'Move the file into an existing project\'s src/ directory, or create a new project with project.json that owns the directory.',
40
+ 'Add the dir to file-location.excludePaths in webpieces.ai-hooks.json',
41
+ ],
42
+ check(ctx) {
43
+ if (ctx.tool !== 'Write')
44
+ return [];
45
+ const excludePaths = Array.isArray(ctx.options['excludePaths'])
46
+ ? ctx.options['excludePaths']
47
+ : DEFAULT_EXCLUDE_PATHS;
48
+ const allowedRootFiles = Array.isArray(ctx.options['allowedRootFiles'])
49
+ ? ctx.options['allowedRootFiles']
50
+ : DEFAULT_ALLOWED_ROOT_FILES;
51
+ const relParts = ctx.relativePath.split(path.sep);
52
+ const topDir = relParts[0];
53
+ if (topDir && shouldSkipDir(topDir, excludePaths))
54
+ return [];
55
+ if (relParts.length === 1 && allowedRootFiles.indexOf(relParts[0]) >= 0)
56
+ return [];
57
+ const projectRoot = findProjectRoot(ctx.filePath, ctx.workspaceRoot);
58
+ if (!projectRoot) {
59
+ return [new types_1.Violation(1, ctx.relativePath, 'File is not inside any Nx project. Move it into a project\'s src/ directory.')];
60
+ }
61
+ const relToProject = path.relative(projectRoot, ctx.filePath);
62
+ const fileName = path.basename(ctx.filePath);
63
+ if (fileName === 'jest.config.ts')
64
+ return [];
65
+ if (!relToProject.startsWith('src' + path.sep) && relToProject !== 'src') {
66
+ const projectName = path.relative(ctx.workspaceRoot, projectRoot);
67
+ return [new types_1.Violation(1, ctx.relativePath, `File is inside project \`${projectName}\` but outside its src/ directory. Move it into src/.`)];
68
+ }
69
+ return [];
70
+ },
71
+ };
72
+ exports.default = fileLocationRule;
73
+ //# sourceMappingURL=file-location.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-location.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hooks/src/core/rules/file-location.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport type { FileRule, FileContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst DEFAULT_EXCLUDE_PATHS = [\n 'node_modules', 'dist', '.nx', '.git',\n 'architecture', 'tmp', 'scripts',\n];\nconst DEFAULT_ALLOWED_ROOT_FILES = ['jest.setup.ts'];\n\nfunction isNodeModulesDir(name: string): boolean {\n return name === 'node_modules' || name.startsWith('node_modules_');\n}\n\nfunction shouldSkipDir(name: string, excludePaths: readonly string[]): boolean {\n if (isNodeModulesDir(name)) return true;\n return excludePaths.indexOf(name) >= 0;\n}\n\nfunction findProjectRoot(filePath: string, workspaceRoot: string): string | null {\n let dir = path.dirname(filePath);\n while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {\n if (fs.existsSync(path.join(dir, 'project.json'))) return dir;\n dir = path.dirname(dir);\n }\n return null;\n}\n\nconst fileLocationRule: FileRule = {\n name: 'file-location',\n description: 'Every .ts file must belong to a project\\'s src/ directory.',\n scope: 'file',\n files: ['**/*.ts', '**/*.tsx'],\n defaultOptions: {\n excludePaths: DEFAULT_EXCLUDE_PATHS,\n allowedRootFiles: DEFAULT_ALLOWED_ROOT_FILES,\n },\n fixHint: [\n 'Move the file into an existing project\\'s src/ directory, or create a new project with project.json that owns the directory.',\n 'Add the dir to file-location.excludePaths in webpieces.ai-hooks.json',\n ],\n\n check(ctx: FileContext): readonly Violation[] {\n if (ctx.tool !== 'Write') return [];\n\n const excludePaths = Array.isArray(ctx.options['excludePaths'])\n ? ctx.options['excludePaths'] as string[]\n : DEFAULT_EXCLUDE_PATHS;\n const allowedRootFiles = Array.isArray(ctx.options['allowedRootFiles'])\n ? ctx.options['allowedRootFiles'] as string[]\n : DEFAULT_ALLOWED_ROOT_FILES;\n\n const relParts = ctx.relativePath.split(path.sep);\n const topDir = relParts[0];\n\n if (topDir && shouldSkipDir(topDir, excludePaths)) return [];\n if (relParts.length === 1 && allowedRootFiles.indexOf(relParts[0]) >= 0) return [];\n\n const projectRoot = findProjectRoot(ctx.filePath, ctx.workspaceRoot);\n\n if (!projectRoot) {\n return [new V(\n 1,\n ctx.relativePath,\n 'File is not inside any Nx project. Move it into a project\\'s src/ directory.',\n )];\n }\n\n const relToProject = path.relative(projectRoot, ctx.filePath);\n const fileName = path.basename(ctx.filePath);\n if (fileName === 'jest.config.ts') return [];\n if (!relToProject.startsWith('src' + path.sep) && relToProject !== 'src') {\n const projectName = path.relative(ctx.workspaceRoot, projectRoot);\n return [new V(\n 1,\n ctx.relativePath,\n `File is inside project \\`${projectName}\\` but outside its src/ directory. Move it into src/.`,\n )];\n }\n\n return [];\n },\n};\n\nexport default fileLocationRule;\n"]}
@@ -0,0 +1 @@
1
+ export declare const builtInRuleNames: readonly string[];
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.builtInRuleNames = void 0;
4
+ exports.builtInRuleNames = [
5
+ 'no-any-unknown',
6
+ 'max-file-lines',
7
+ 'file-location',
8
+ 'no-destructure',
9
+ 'require-return-type',
10
+ 'no-unmanaged-exceptions',
11
+ 'catch-error-pattern',
12
+ ];
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hooks/src/core/rules/index.ts"],"names":[],"mappings":";;;AAAa,QAAA,gBAAgB,GAAsB;IAC/C,gBAAgB;IAChB,gBAAgB;IAChB,eAAe;IACf,gBAAgB;IAChB,qBAAqB;IACrB,yBAAyB;IACzB,qBAAqB;CACxB,CAAC","sourcesContent":["export const builtInRuleNames: readonly string[] = [\n 'no-any-unknown',\n 'max-file-lines',\n 'file-location',\n 'no-destructure',\n 'require-return-type',\n 'no-unmanaged-exceptions',\n 'catch-error-pattern',\n];\n"]}
@@ -0,0 +1,3 @@
1
+ import type { FileRule } from '../types';
2
+ declare const maxFileLinesRule: FileRule;
3
+ export default maxFileLinesRule;
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const fs = tslib_1.__importStar(require("fs"));
5
+ const path = tslib_1.__importStar(require("path"));
6
+ const types_1 = require("../types");
7
+ const DEFAULT_LIMIT = 900;
8
+ const INSTRUCT_DIR = '.webpieces/instruct-ai';
9
+ const INSTRUCT_FILE = 'webpieces.filesize.md';
10
+ const maxFileLinesRule = {
11
+ name: 'max-file-lines',
12
+ description: 'Cap file length at a configured line limit.',
13
+ scope: 'file',
14
+ files: ['**/*.ts', '**/*.tsx'],
15
+ defaultOptions: { limit: DEFAULT_LIMIT },
16
+ fixHint: [
17
+ 'READ .webpieces/instruct-ai/webpieces.filesize.md for step-by-step refactoring guidance.',
18
+ '// eslint-disable-next-line @webpieces/max-file-lines (also suppresses the eslint rule)',
19
+ ],
20
+ check(ctx) {
21
+ const limit = typeof ctx.options['limit'] === 'number'
22
+ ? ctx.options['limit']
23
+ : DEFAULT_LIMIT;
24
+ if (ctx.projectedFileLines <= limit)
25
+ return [];
26
+ writeInstructionFile(ctx.workspaceRoot);
27
+ return [new types_1.Violation(1, `(projected ${String(ctx.projectedFileLines)} lines)`, `File will be ${String(ctx.projectedFileLines)} lines, exceeding the ${String(limit)}-line limit. See .webpieces/instruct-ai/webpieces.filesize.md for detailed refactoring instructions.`)];
28
+ },
29
+ };
30
+ function writeInstructionFile(workspaceRoot) {
31
+ const dir = path.join(workspaceRoot, INSTRUCT_DIR);
32
+ const filePath = path.join(dir, INSTRUCT_FILE);
33
+ if (fs.existsSync(filePath))
34
+ return;
35
+ fs.mkdirSync(dir, { recursive: true });
36
+ fs.writeFileSync(filePath, FILESIZE_DOC_CONTENT);
37
+ }
38
+ // eslint-disable-next-line @webpieces/max-file-lines
39
+ const FILESIZE_DOC_CONTENT = `# AI Agent Instructions: File Too Long
40
+
41
+ **READ THIS FILE to fix files that are too long**
42
+
43
+ ## Core Principle
44
+
45
+ With **stateless systems + dependency injection, refactor is trivial**.
46
+ Pick a method or a few and move to new class XXXXX, then inject XXXXX
47
+ into all users of those methods via the constructor.
48
+ Delete those methods from original class.
49
+
50
+ **99% of files can be less than the configured max lines of code.**
51
+
52
+ Files should contain a SINGLE COHESIVE UNIT.
53
+ - One class per file (Java convention)
54
+ - If class is too large, extract child responsibilities
55
+ - Use dependency injection to compose functionality
56
+
57
+ ## Command: Reduce File Size
58
+
59
+ ### Step 1: Check for Multiple Classes
60
+ If the file contains multiple classes, **SEPARATE each class into its own file**.
61
+
62
+ ### Step 2: Extract Child Responsibilities (if single class is too large)
63
+
64
+ #### Pattern: Create New Service Class with Dependency Injection
65
+
66
+ \`\`\`typescript
67
+ // BAD: UserController.ts (800 lines, single class)
68
+ @provideSingleton()
69
+ @Controller()
70
+ export class UserController {
71
+ // 200 lines: CRUD operations
72
+ // 300 lines: validation logic
73
+ // 200 lines: notification logic
74
+ }
75
+
76
+ // GOOD: Extract validation service
77
+ // 1. Create UserValidationService.ts
78
+ @provideSingleton()
79
+ export class UserValidationService {
80
+ validateUserData(data: UserData): ValidationResult { /* ... */ }
81
+ validateEmail(email: string): boolean { /* ... */ }
82
+ }
83
+
84
+ // 2. Inject into UserController.ts
85
+ @provideSingleton()
86
+ @Controller()
87
+ export class UserController {
88
+ constructor(
89
+ @inject(TYPES.UserValidationService)
90
+ private validator: UserValidationService
91
+ ) {}
92
+ }
93
+ \`\`\`
94
+
95
+ ## AI Agent Action Steps
96
+
97
+ 1. **ANALYZE** the file structure:
98
+ - Count classes (if >1, separate immediately)
99
+ - Identify logical responsibilities within single class
100
+
101
+ 2. **IDENTIFY** "child code" to extract:
102
+ - Validation logic -> ValidationService
103
+ - Notification logic -> NotificationService
104
+ - Data transformation -> TransformerService
105
+ - External API calls -> ApiService
106
+ - Business rules -> RulesEngine
107
+
108
+ 3. **CREATE** new service file(s):
109
+ - Add \`@provideSingleton()\` decorator
110
+ - Move child methods to new class
111
+
112
+ 4. **UPDATE** dependency injection:
113
+ - Inject new service into original class constructor
114
+ - Replace direct method calls with \`this.serviceName.method()\`
115
+
116
+ 5. **VERIFY** file sizes:
117
+ - Original file should now be under the limit
118
+ - Each extracted file should be under the limit
119
+
120
+ ## Escape Hatch
121
+
122
+ If refactoring is genuinely not feasible, add a disable comment:
123
+
124
+ \`\`\`typescript
125
+ // eslint-disable-next-line @webpieces/max-file-lines
126
+ \`\`\`
127
+
128
+ Remember: Find the "child code" and pull it down into a new class. Once moved, the code's purpose becomes clear, making it easy to rename to a logical name.
129
+ `;
130
+ exports.default = maxFileLinesRule;
131
+ //# sourceMappingURL=max-file-lines.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"max-file-lines.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hooks/src/core/rules/max-file-lines.ts"],"names":[],"mappings":";;;AAAA,+CAAyB;AACzB,mDAA6B;AAG7B,oCAA0C;AAE1C,MAAM,aAAa,GAAG,GAAG,CAAC;AAC1B,MAAM,YAAY,GAAG,wBAAwB,CAAC;AAC9C,MAAM,aAAa,GAAG,uBAAuB,CAAC;AAE9C,MAAM,gBAAgB,GAAa;IAC/B,IAAI,EAAE,gBAAgB;IACtB,WAAW,EAAE,6CAA6C;IAC1D,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;IAC9B,cAAc,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE;IACxC,OAAO,EAAE;QACL,0FAA0F;QAC1F,0FAA0F;KAC7F;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,KAAK,GAAG,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,QAAQ;YAClD,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAW;YAChC,CAAC,CAAC,aAAa,CAAC;QACpB,IAAI,GAAG,CAAC,kBAAkB,IAAI,KAAK;YAAE,OAAO,EAAE,CAAC;QAC/C,oBAAoB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACxC,OAAO,CAAC,IAAI,iBAAC,CACT,CAAC,EACD,cAAc,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,SAAS,EACrD,gBAAgB,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,yBAAyB,MAAM,CAAC,KAAK,CAAC,sGAAsG,CAC7L,CAAC,CAAC;IACP,CAAC;CACJ,CAAC;AAEF,SAAS,oBAAoB,CAAC,aAAqB;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;IAC/C,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO;IACpC,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvC,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,oBAAoB,CAAC,CAAC;AACrD,CAAC;AAED,qDAAqD;AACrD,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0F5B,CAAC;AAEF,kBAAe,gBAAgB,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport type { FileRule, FileContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst DEFAULT_LIMIT = 900;\nconst INSTRUCT_DIR = '.webpieces/instruct-ai';\nconst INSTRUCT_FILE = 'webpieces.filesize.md';\n\nconst maxFileLinesRule: FileRule = {\n name: 'max-file-lines',\n description: 'Cap file length at a configured line limit.',\n scope: 'file',\n files: ['**/*.ts', '**/*.tsx'],\n defaultOptions: { limit: DEFAULT_LIMIT },\n fixHint: [\n 'READ .webpieces/instruct-ai/webpieces.filesize.md for step-by-step refactoring guidance.',\n '// eslint-disable-next-line @webpieces/max-file-lines (also suppresses the eslint rule)',\n ],\n\n check(ctx: FileContext): readonly Violation[] {\n const limit = typeof ctx.options['limit'] === 'number'\n ? ctx.options['limit'] as number\n : DEFAULT_LIMIT;\n if (ctx.projectedFileLines <= limit) return [];\n writeInstructionFile(ctx.workspaceRoot);\n return [new V(\n 1,\n `(projected ${String(ctx.projectedFileLines)} lines)`,\n `File will be ${String(ctx.projectedFileLines)} lines, exceeding the ${String(limit)}-line limit. See .webpieces/instruct-ai/webpieces.filesize.md for detailed refactoring instructions.`,\n )];\n },\n};\n\nfunction writeInstructionFile(workspaceRoot: string): void {\n const dir = path.join(workspaceRoot, INSTRUCT_DIR);\n const filePath = path.join(dir, INSTRUCT_FILE);\n if (fs.existsSync(filePath)) return;\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(filePath, FILESIZE_DOC_CONTENT);\n}\n\n// eslint-disable-next-line @webpieces/max-file-lines\nconst FILESIZE_DOC_CONTENT = `# AI Agent Instructions: File Too Long\n\n**READ THIS FILE to fix files that are too long**\n\n## Core Principle\n\nWith **stateless systems + dependency injection, refactor is trivial**.\nPick a method or a few and move to new class XXXXX, then inject XXXXX\ninto all users of those methods via the constructor.\nDelete those methods from original class.\n\n**99% of files can be less than the configured max lines of code.**\n\nFiles should contain a SINGLE COHESIVE UNIT.\n- One class per file (Java convention)\n- If class is too large, extract child responsibilities\n- Use dependency injection to compose functionality\n\n## Command: Reduce File Size\n\n### Step 1: Check for Multiple Classes\nIf the file contains multiple classes, **SEPARATE each class into its own file**.\n\n### Step 2: Extract Child Responsibilities (if single class is too large)\n\n#### Pattern: Create New Service Class with Dependency Injection\n\n\\`\\`\\`typescript\n// BAD: UserController.ts (800 lines, single class)\n@provideSingleton()\n@Controller()\nexport class UserController {\n // 200 lines: CRUD operations\n // 300 lines: validation logic\n // 200 lines: notification logic\n}\n\n// GOOD: Extract validation service\n// 1. Create UserValidationService.ts\n@provideSingleton()\nexport class UserValidationService {\n validateUserData(data: UserData): ValidationResult { /* ... */ }\n validateEmail(email: string): boolean { /* ... */ }\n}\n\n// 2. Inject into UserController.ts\n@provideSingleton()\n@Controller()\nexport class UserController {\n constructor(\n @inject(TYPES.UserValidationService)\n private validator: UserValidationService\n ) {}\n}\n\\`\\`\\`\n\n## AI Agent Action Steps\n\n1. **ANALYZE** the file structure:\n - Count classes (if >1, separate immediately)\n - Identify logical responsibilities within single class\n\n2. **IDENTIFY** \"child code\" to extract:\n - Validation logic -> ValidationService\n - Notification logic -> NotificationService\n - Data transformation -> TransformerService\n - External API calls -> ApiService\n - Business rules -> RulesEngine\n\n3. **CREATE** new service file(s):\n - Add \\`@provideSingleton()\\` decorator\n - Move child methods to new class\n\n4. **UPDATE** dependency injection:\n - Inject new service into original class constructor\n - Replace direct method calls with \\`this.serviceName.method()\\`\n\n5. **VERIFY** file sizes:\n - Original file should now be under the limit\n - Each extracted file should be under the limit\n\n## Escape Hatch\n\nIf refactoring is genuinely not feasible, add a disable comment:\n\n\\`\\`\\`typescript\n// eslint-disable-next-line @webpieces/max-file-lines\n\\`\\`\\`\n\nRemember: Find the \"child code\" and pull it down into a new class. Once moved, the code's purpose becomes clear, making it easy to rename to a logical name.\n`;\n\nexport default maxFileLinesRule;\n"]}
@@ -0,0 +1,3 @@
1
+ import type { EditRule } from '../types';
2
+ declare const noAnyRule: EditRule;
3
+ export default noAnyRule;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const types_1 = require("../types");
4
+ const ANY_PATTERN = /(?::\s*any\b|\bas\s+any\b|<any>|any\[\]|Array<any>|Promise<any>|Map<[^,<>]+,\s*any\s*>|Record<[^,<>]+,\s*any\s*>|Set<any>)/;
5
+ const noAnyRule = {
6
+ name: 'no-any-unknown',
7
+ description: 'Disallow the `any` keyword. Use concrete types or interfaces.',
8
+ scope: 'edit',
9
+ files: ['**/*.ts', '**/*.tsx'],
10
+ defaultOptions: {},
11
+ fixHint: [
12
+ 'Prefer: interface MyData { ... } or class MyData { ... }',
13
+ '// webpieces-disable no-any-unknown -- <one-line reason>',
14
+ ],
15
+ check(ctx) {
16
+ const violations = [];
17
+ for (let i = 0; i < ctx.strippedLines.length; i += 1) {
18
+ const stripped = ctx.strippedLines[i];
19
+ if (!ANY_PATTERN.test(stripped))
20
+ continue;
21
+ const lineNum = i + 1;
22
+ if (ctx.isLineDisabled(lineNum, 'no-any-unknown'))
23
+ continue;
24
+ violations.push(new types_1.Violation(lineNum, ctx.lines[i].trim(), '`any` erases type information. Use a concrete type, an interface, or `unknown` with type guards.'));
25
+ }
26
+ return violations;
27
+ },
28
+ };
29
+ exports.default = noAnyRule;
30
+ //# sourceMappingURL=no-any-unknown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-any-unknown.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hooks/src/core/rules/no-any-unknown.ts"],"names":[],"mappings":";;AACA,oCAA0C;AAE1C,MAAM,WAAW,GACb,4HAA4H,CAAC;AAEjI,MAAM,SAAS,GAAa;IACxB,IAAI,EAAE,gBAAgB;IACtB,WAAW,EAAE,+DAA+D;IAC5E,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;IAC9B,cAAc,EAAE,EAAE;IAClB,OAAO,EAAE;QACL,8DAA8D;QAC9D,0DAA0D;KAC7D;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,UAAU,GAAQ,EAAE,CAAC;QAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACnD,MAAM,QAAQ,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;YACtC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAC1C,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;YACtB,IAAI,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,gBAAgB,CAAC;gBAAE,SAAS;YAC5D,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CACjB,OAAO,EACP,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EACnB,kGAAkG,CACrG,CAAC,CAAC;QACP,CAAC;QACD,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ,CAAC;AAEF,kBAAe,SAAS,CAAC","sourcesContent":["import type { EditRule, EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst ANY_PATTERN =\n /(?::\\s*any\\b|\\bas\\s+any\\b|<any>|any\\[\\]|Array<any>|Promise<any>|Map<[^,<>]+,\\s*any\\s*>|Record<[^,<>]+,\\s*any\\s*>|Set<any>)/;\n\nconst noAnyRule: EditRule = {\n name: 'no-any-unknown',\n description: 'Disallow the `any` keyword. Use concrete types or interfaces.',\n scope: 'edit',\n files: ['**/*.ts', '**/*.tsx'],\n defaultOptions: {},\n fixHint: [\n 'Prefer: interface MyData { ... } or class MyData { ... }',\n '// webpieces-disable no-any-unknown -- <one-line reason>',\n ],\n\n check(ctx: EditContext): readonly Violation[] {\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const stripped = ctx.strippedLines[i];\n if (!ANY_PATTERN.test(stripped)) continue;\n const lineNum = i + 1;\n if (ctx.isLineDisabled(lineNum, 'no-any-unknown')) continue;\n violations.push(new V(\n lineNum,\n ctx.lines[i].trim(),\n '`any` erases type information. Use a concrete type, an interface, or `unknown` with type guards.',\n ));\n }\n return violations;\n },\n};\n\nexport default noAnyRule;\n"]}
@@ -0,0 +1,3 @@
1
+ import type { EditRule } from '../types';
2
+ declare const noDestructureRule: EditRule;
3
+ export default noDestructureRule;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const types_1 = require("../types");
4
+ const VARIABLE_DESTRUCTURE = /\b(?:const|let|var)\s*\{/;
5
+ const noDestructureRule = {
6
+ name: 'no-destructure',
7
+ description: 'Disallow destructuring patterns. Assign the whole result and pass it around or access properties explicitly.',
8
+ scope: 'edit',
9
+ files: ['**/*.ts', '**/*.tsx'],
10
+ defaultOptions: { allowTopLevel: true },
11
+ fixHint: [
12
+ 'Instead of: const { x, y } = methodCall(); prefer const obj = methodCall(); then pass obj to other methods or use obj.x',
13
+ '// webpieces-disable no-destructure -- <reason>',
14
+ ],
15
+ check(ctx) {
16
+ const violations = [];
17
+ for (let i = 0; i < ctx.strippedLines.length; i += 1) {
18
+ const stripped = ctx.strippedLines[i];
19
+ if (!VARIABLE_DESTRUCTURE.test(stripped))
20
+ continue;
21
+ const lineNum = i + 1;
22
+ if (ctx.isLineDisabled(lineNum, 'no-destructure'))
23
+ continue;
24
+ violations.push(new types_1.Violation(lineNum, ctx.lines[i].trim(), 'Destructuring pattern. Assign the whole result instead: const obj = methodCall(); then pass obj around or use obj.x'));
25
+ }
26
+ return violations;
27
+ },
28
+ };
29
+ exports.default = noDestructureRule;
30
+ //# sourceMappingURL=no-destructure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-destructure.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hooks/src/core/rules/no-destructure.ts"],"names":[],"mappings":";;AACA,oCAA0C;AAE1C,MAAM,oBAAoB,GAAG,0BAA0B,CAAC;AAExD,MAAM,iBAAiB,GAAa;IAChC,IAAI,EAAE,gBAAgB;IACtB,WAAW,EAAE,8GAA8G;IAC3H,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;IAC9B,cAAc,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE;IACvC,OAAO,EAAE;QACL,yHAAyH;QACzH,iDAAiD;KACpD;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,UAAU,GAAQ,EAAE,CAAC;QAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACnD,MAAM,QAAQ,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;YACtC,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACnD,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;YACtB,IAAI,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,gBAAgB,CAAC;gBAAE,SAAS;YAC5D,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CACjB,OAAO,EACP,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EACnB,qHAAqH,CACxH,CAAC,CAAC;QACP,CAAC;QACD,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ,CAAC;AAEF,kBAAe,iBAAiB,CAAC","sourcesContent":["import type { EditRule, EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst VARIABLE_DESTRUCTURE = /\\b(?:const|let|var)\\s*\\{/;\n\nconst noDestructureRule: EditRule = {\n name: 'no-destructure',\n description: 'Disallow destructuring patterns. Assign the whole result and pass it around or access properties explicitly.',\n scope: 'edit',\n files: ['**/*.ts', '**/*.tsx'],\n defaultOptions: { allowTopLevel: true },\n fixHint: [\n 'Instead of: const { x, y } = methodCall(); prefer const obj = methodCall(); then pass obj to other methods or use obj.x',\n '// webpieces-disable no-destructure -- <reason>',\n ],\n\n check(ctx: EditContext): readonly Violation[] {\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const stripped = ctx.strippedLines[i];\n if (!VARIABLE_DESTRUCTURE.test(stripped)) continue;\n const lineNum = i + 1;\n if (ctx.isLineDisabled(lineNum, 'no-destructure')) continue;\n violations.push(new V(\n lineNum,\n ctx.lines[i].trim(),\n 'Destructuring pattern. Assign the whole result instead: const obj = methodCall(); then pass obj around or use obj.x',\n ));\n }\n return violations;\n },\n};\n\nexport default noDestructureRule;\n"]}
@@ -0,0 +1,3 @@
1
+ import type { EditRule } from '../types';
2
+ declare const noUnmanagedExceptionsRule: EditRule;
3
+ export default noUnmanagedExceptionsRule;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const types_1 = require("../types");
4
+ const TRY_PATTERN = /\btry\s*\{/;
5
+ // Both webpieces-disable and the existing ESLint directive suppress this rule
6
+ const DISABLE_PATTERN = /@webpieces\/no-unmanaged-exceptions|webpieces-disable\s+(?:[\w-]+,\s*)*no-unmanaged-exceptions/;
7
+ const noUnmanagedExceptionsRule = {
8
+ name: 'no-unmanaged-exceptions',
9
+ description: 'try/catch is generally not allowed. Only allowed in chokepoints (filter, globalErrorHandler) or other rare locations.',
10
+ scope: 'edit',
11
+ files: ['**/*.ts', '**/*.tsx'],
12
+ defaultOptions: {},
13
+ fixHint: [
14
+ 'Exceptions should bubble to a chokepoint (filter in node.js, globalErrorHandler in Angular). Most code should NOT catch exceptions.',
15
+ '// webpieces-disable no-unmanaged-exceptions -- <reason>',
16
+ 'When try/catch IS used (after disabling), the catch block MUST use: catch (err: unknown) { const error = toError(err); ... } or //const error = toError(err); to explicitly ignore.',
17
+ ],
18
+ check(ctx) {
19
+ const violations = [];
20
+ for (let i = 0; i < ctx.strippedLines.length; i += 1) {
21
+ const stripped = ctx.strippedLines[i];
22
+ if (!TRY_PATTERN.test(stripped))
23
+ continue;
24
+ const lineNum = i + 1;
25
+ if (ctx.isLineDisabled(lineNum, 'no-unmanaged-exceptions'))
26
+ continue;
27
+ if (hasPrecedingDisable(ctx.lines, i))
28
+ continue;
29
+ violations.push(new types_1.Violation(lineNum, ctx.lines[i].trim(), 'try/catch is generally not allowed. It is only allowed in chokepoints (filter, globalErrorHandler) or other rare locations.'));
30
+ }
31
+ return violations;
32
+ },
33
+ };
34
+ function hasPrecedingDisable(lines, idx) {
35
+ if (idx === 0)
36
+ return false;
37
+ const prevLine = lines[idx - 1];
38
+ return DISABLE_PATTERN.test(prevLine);
39
+ }
40
+ exports.default = noUnmanagedExceptionsRule;
41
+ //# sourceMappingURL=no-unmanaged-exceptions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-unmanaged-exceptions.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hooks/src/core/rules/no-unmanaged-exceptions.ts"],"names":[],"mappings":";;AACA,oCAA0C;AAE1C,MAAM,WAAW,GAAG,YAAY,CAAC;AAEjC,8EAA8E;AAC9E,MAAM,eAAe,GAAG,gGAAgG,CAAC;AAEzH,MAAM,yBAAyB,GAAa;IACxC,IAAI,EAAE,yBAAyB;IAC/B,WAAW,EAAE,uHAAuH;IACpI,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;IAC9B,cAAc,EAAE,EAAE;IAClB,OAAO,EAAE;QACL,qIAAqI;QACrI,0DAA0D;QAC1D,qLAAqL;KACxL;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,UAAU,GAAQ,EAAE,CAAC;QAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACnD,MAAM,QAAQ,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;YACtC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAC1C,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;YACtB,IAAI,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,yBAAyB,CAAC;gBAAE,SAAS;YACrE,IAAI,mBAAmB,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;gBAAE,SAAS;YAChD,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CACjB,OAAO,EACP,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EACnB,6HAA6H,CAChI,CAAC,CAAC;QACP,CAAC;QACD,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ,CAAC;AAEF,SAAS,mBAAmB,CAAC,KAAwB,EAAE,GAAW;IAC9D,IAAI,GAAG,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5B,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IAChC,OAAO,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAED,kBAAe,yBAAyB,CAAC","sourcesContent":["import type { EditRule, EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\nconst TRY_PATTERN = /\\btry\\s*\\{/;\n\n// Both webpieces-disable and the existing ESLint directive suppress this rule\nconst DISABLE_PATTERN = /@webpieces\\/no-unmanaged-exceptions|webpieces-disable\\s+(?:[\\w-]+,\\s*)*no-unmanaged-exceptions/;\n\nconst noUnmanagedExceptionsRule: EditRule = {\n name: 'no-unmanaged-exceptions',\n description: 'try/catch is generally not allowed. Only allowed in chokepoints (filter, globalErrorHandler) or other rare locations.',\n scope: 'edit',\n files: ['**/*.ts', '**/*.tsx'],\n defaultOptions: {},\n fixHint: [\n 'Exceptions should bubble to a chokepoint (filter in node.js, globalErrorHandler in Angular). Most code should NOT catch exceptions.',\n '// webpieces-disable no-unmanaged-exceptions -- <reason>',\n 'When try/catch IS used (after disabling), the catch block MUST use: catch (err: unknown) { const error = toError(err); ... } or //const error = toError(err); to explicitly ignore.',\n ],\n\n check(ctx: EditContext): readonly Violation[] {\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const stripped = ctx.strippedLines[i];\n if (!TRY_PATTERN.test(stripped)) continue;\n const lineNum = i + 1;\n if (ctx.isLineDisabled(lineNum, 'no-unmanaged-exceptions')) continue;\n if (hasPrecedingDisable(ctx.lines, i)) continue;\n violations.push(new V(\n lineNum,\n ctx.lines[i].trim(),\n 'try/catch is generally not allowed. It is only allowed in chokepoints (filter, globalErrorHandler) or other rare locations.',\n ));\n }\n return violations;\n },\n};\n\nfunction hasPrecedingDisable(lines: readonly string[], idx: number): boolean {\n if (idx === 0) return false;\n const prevLine = lines[idx - 1];\n return DISABLE_PATTERN.test(prevLine);\n}\n\nexport default noUnmanagedExceptionsRule;\n"]}
@@ -0,0 +1,3 @@
1
+ import type { EditRule } from '../types';
2
+ declare const requireReturnTypeRule: EditRule;
3
+ export default requireReturnTypeRule;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const types_1 = require("../types");
4
+ // Matches function/method signatures that don't have `: ReturnType` before the `{` body opener.
5
+ // Pattern: function name(<params>) { — missing `: Type` between `)` and `{`
6
+ const FUNC_DECL_MISSING = /\bfunction\s+\w+\s*(?:<[^>]*>)?\s*\([^)]*\)\s*\{/;
7
+ // Matches class method signatures: indented, optional async, name(<params>) {
8
+ const METHOD_MISSING = /^\s{2,}(?:async\s+)?\w+\s*(?:<[^>]*>)?\s*\([^)]*\)\s*\{/;
9
+ // Arrow function: const name = (async)? (<params>) => — missing `: Type` before `=>`
10
+ const ARROW_MISSING = /\bconst\s+\w+\s*=\s*(?:async\s+)?(?:<[^>]*>)?\s*\([^)]*\)\s*=>/;
11
+ // Lines that have ): ReturnType before { or => — these are COMPLIANT
12
+ const HAS_RETURN_TYPE = /\)\s*:\s*\S/;
13
+ // Skip constructors, getters, setters, and control flow keywords
14
+ const SKIP_PATTERN = /\b(?:constructor|get\s+\w+|set\s+\w+|if|else|while|for|switch|catch|return)\s*\(/;
15
+ const requireReturnTypeRule = {
16
+ name: 'require-return-type',
17
+ description: 'Every function and method must declare its return type.',
18
+ scope: 'edit',
19
+ files: ['**/*.ts', '**/*.tsx'],
20
+ defaultOptions: {},
21
+ fixHint: [
22
+ 'Add return type: function foo(x: T): ReturnType { ... }',
23
+ '// webpieces-disable require-return-type -- <reason>',
24
+ ],
25
+ check(ctx) {
26
+ const violations = [];
27
+ for (let i = 0; i < ctx.strippedLines.length; i += 1) {
28
+ const stripped = ctx.strippedLines[i];
29
+ if (!isMissingReturnType(stripped))
30
+ continue;
31
+ const lineNum = i + 1;
32
+ if (ctx.isLineDisabled(lineNum, 'require-return-type'))
33
+ continue;
34
+ violations.push(new types_1.Violation(lineNum, ctx.lines[i].trim(), 'Missing return type annotation.'));
35
+ }
36
+ return violations;
37
+ },
38
+ };
39
+ function isMissingReturnType(line) {
40
+ if (SKIP_PATTERN.test(line))
41
+ return false;
42
+ const isFuncLike = FUNC_DECL_MISSING.test(line) ||
43
+ METHOD_MISSING.test(line) ||
44
+ ARROW_MISSING.test(line);
45
+ if (!isFuncLike)
46
+ return false;
47
+ if (HAS_RETURN_TYPE.test(line))
48
+ return false;
49
+ return true;
50
+ }
51
+ exports.default = requireReturnTypeRule;
52
+ //# sourceMappingURL=require-return-type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-return-type.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hooks/src/core/rules/require-return-type.ts"],"names":[],"mappings":";;AACA,oCAA0C;AAE1C,gGAAgG;AAChG,4EAA4E;AAC5E,MAAM,iBAAiB,GAAG,kDAAkD,CAAC;AAE7E,8EAA8E;AAC9E,MAAM,cAAc,GAAG,yDAAyD,CAAC;AAEjF,qFAAqF;AACrF,MAAM,aAAa,GAAG,gEAAgE,CAAC;AAEvF,qEAAqE;AACrE,MAAM,eAAe,GAAG,aAAa,CAAC;AAEtC,iEAAiE;AACjE,MAAM,YAAY,GAAG,kFAAkF,CAAC;AAExG,MAAM,qBAAqB,GAAa;IACpC,IAAI,EAAE,qBAAqB;IAC3B,WAAW,EAAE,yDAAyD;IACtE,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;IAC9B,cAAc,EAAE,EAAE;IAClB,OAAO,EAAE;QACL,yDAAyD;QACzD,sDAAsD;KACzD;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,UAAU,GAAQ,EAAE,CAAC;QAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACnD,MAAM,QAAQ,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;YACtC,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAC7C,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;YACtB,IAAI,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,qBAAqB,CAAC;gBAAE,SAAS;YACjE,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CACjB,OAAO,EACP,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EACnB,iCAAiC,CACpC,CAAC,CAAC;QACP,CAAC;QACD,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ,CAAC;AAEF,SAAS,mBAAmB,CAAC,IAAY;IACrC,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1C,MAAM,UAAU,GACZ,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;QAC5B,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;QACzB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC;IAC9B,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7C,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,kBAAe,qBAAqB,CAAC","sourcesContent":["import type { EditRule, EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\n\n// Matches function/method signatures that don't have `: ReturnType` before the `{` body opener.\n// Pattern: function name(<params>) { — missing `: Type` between `)` and `{`\nconst FUNC_DECL_MISSING = /\\bfunction\\s+\\w+\\s*(?:<[^>]*>)?\\s*\\([^)]*\\)\\s*\\{/;\n\n// Matches class method signatures: indented, optional async, name(<params>) {\nconst METHOD_MISSING = /^\\s{2,}(?:async\\s+)?\\w+\\s*(?:<[^>]*>)?\\s*\\([^)]*\\)\\s*\\{/;\n\n// Arrow function: const name = (async)? (<params>) => — missing `: Type` before `=>`\nconst ARROW_MISSING = /\\bconst\\s+\\w+\\s*=\\s*(?:async\\s+)?(?:<[^>]*>)?\\s*\\([^)]*\\)\\s*=>/;\n\n// Lines that have ): ReturnType before { or => — these are COMPLIANT\nconst HAS_RETURN_TYPE = /\\)\\s*:\\s*\\S/;\n\n// Skip constructors, getters, setters, and control flow keywords\nconst SKIP_PATTERN = /\\b(?:constructor|get\\s+\\w+|set\\s+\\w+|if|else|while|for|switch|catch|return)\\s*\\(/;\n\nconst requireReturnTypeRule: EditRule = {\n name: 'require-return-type',\n description: 'Every function and method must declare its return type.',\n scope: 'edit',\n files: ['**/*.ts', '**/*.tsx'],\n defaultOptions: {},\n fixHint: [\n 'Add return type: function foo(x: T): ReturnType { ... }',\n '// webpieces-disable require-return-type -- <reason>',\n ],\n\n check(ctx: EditContext): readonly Violation[] {\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const stripped = ctx.strippedLines[i];\n if (!isMissingReturnType(stripped)) continue;\n const lineNum = i + 1;\n if (ctx.isLineDisabled(lineNum, 'require-return-type')) continue;\n violations.push(new V(\n lineNum,\n ctx.lines[i].trim(),\n 'Missing return type annotation.',\n ));\n }\n return violations;\n },\n};\n\nfunction isMissingReturnType(line: string): boolean {\n if (SKIP_PATTERN.test(line)) return false;\n const isFuncLike =\n FUNC_DECL_MISSING.test(line) ||\n METHOD_MISSING.test(line) ||\n ARROW_MISSING.test(line);\n if (!isFuncLike) return false;\n if (HAS_RETURN_TYPE.test(line)) return false;\n return true;\n}\n\nexport default requireReturnTypeRule;\n"]}
@@ -0,0 +1,2 @@
1
+ import { ToolKind, NormalizedToolInput, BlockedResult } from './types';
2
+ export declare function run(toolKind: ToolKind, input: NormalizedToolInput, cwd: string): BlockedResult | null;