@webpieces/ai-hook-rules 0.4.636 → 0.4.638

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 (94) hide show
  1. package/README.md +11 -4
  2. package/package.json +2 -2
  3. package/src/adapters/hook-core.js +4 -3
  4. package/src/adapters/hook-core.js.map +1 -1
  5. package/src/bin/setup.js +72 -6
  6. package/src/bin/setup.js.map +1 -1
  7. package/src/core/custom-rule-adapter.js +3 -1
  8. package/src/core/custom-rule-adapter.js.map +1 -1
  9. package/src/core/decision-log.d.ts +38 -5
  10. package/src/core/decision-log.js +45 -6
  11. package/src/core/decision-log.js.map +1 -1
  12. package/src/core/l1-doc.js +6 -3
  13. package/src/core/l1-doc.js.map +1 -1
  14. package/src/core/l2-doc.d.ts +7 -0
  15. package/src/core/l2-doc.js +274 -0
  16. package/src/core/l2-doc.js.map +1 -0
  17. package/src/core/l2-rows.d.ts +97 -0
  18. package/src/core/l2-rows.js +229 -0
  19. package/src/core/l2-rows.js.map +1 -0
  20. package/src/core/load-rules.d.ts +8 -2
  21. package/src/core/load-rules.js +51 -31
  22. package/src/core/load-rules.js.map +1 -1
  23. package/src/core/main-sync-refresh.d.ts +8 -1
  24. package/src/core/main-sync-refresh.js +9 -2
  25. package/src/core/main-sync-refresh.js.map +1 -1
  26. package/src/core/main-sync-timeout.d.ts +32 -0
  27. package/src/core/main-sync-timeout.js +93 -0
  28. package/src/core/main-sync-timeout.js.map +1 -0
  29. package/src/core/rules/branch-creation-guard.js +1 -1
  30. package/src/core/rules/branch-creation-guard.js.map +1 -1
  31. package/src/core/rules/catch-error-pattern.js +1 -1
  32. package/src/core/rules/catch-error-pattern.js.map +1 -1
  33. package/src/core/rules/feature-branch-guard.d.ts +3 -3
  34. package/src/core/rules/feature-branch-guard.js +17 -4
  35. package/src/core/rules/feature-branch-guard.js.map +1 -1
  36. package/src/core/rules/index.d.ts +15 -1
  37. package/src/core/rules/index.js +18 -10
  38. package/src/core/rules/index.js.map +1 -1
  39. package/src/core/rules/match-rule.js +4 -1
  40. package/src/core/rules/match-rule.js.map +1 -1
  41. package/src/core/rules/max-file-lines.js +1 -1
  42. package/src/core/rules/max-file-lines.js.map +1 -1
  43. package/src/core/rules/merge-in-progress-guard.d.ts +9 -3
  44. package/src/core/rules/merge-in-progress-guard.js +9 -4
  45. package/src/core/rules/merge-in-progress-guard.js.map +1 -1
  46. package/src/core/rules/merged-branch-bash-guard.d.ts +3 -3
  47. package/src/core/rules/merged-branch-bash-guard.js +17 -6
  48. package/src/core/rules/merged-branch-bash-guard.js.map +1 -1
  49. package/src/core/rules/no-any-unknown.js +1 -1
  50. package/src/core/rules/no-any-unknown.js.map +1 -1
  51. package/src/core/rules/no-custom-css.js +1 -1
  52. package/src/core/rules/no-custom-css.js.map +1 -1
  53. package/src/core/rules/no-destructure.js +1 -1
  54. package/src/core/rules/no-destructure.js.map +1 -1
  55. package/src/core/rules/no-implicit-any.js +1 -1
  56. package/src/core/rules/no-implicit-any.js.map +1 -1
  57. package/src/core/rules/no-js-files.js +1 -1
  58. package/src/core/rules/no-js-files.js.map +1 -1
  59. package/src/core/rules/no-process-exit-outside-main.js +1 -1
  60. package/src/core/rules/no-process-exit-outside-main.js.map +1 -1
  61. package/src/core/rules/no-symbol-di-tokens.js +1 -1
  62. package/src/core/rules/no-symbol-di-tokens.js.map +1 -1
  63. package/src/core/rules/no-unmanaged-exceptions.js +1 -1
  64. package/src/core/rules/no-unmanaged-exceptions.js.map +1 -1
  65. package/src/core/rules/pr-creation-or-push-guard.d.ts +10 -3
  66. package/src/core/rules/pr-creation-or-push-guard.js +10 -4
  67. package/src/core/rules/pr-creation-or-push-guard.js.map +1 -1
  68. package/src/core/rules/pr-merge-guard.d.ts +3 -3
  69. package/src/core/rules/pr-merge-guard.js +2 -1
  70. package/src/core/rules/pr-merge-guard.js.map +1 -1
  71. package/src/core/rules/read-stale-guard.d.ts +3 -3
  72. package/src/core/rules/read-stale-guard.js +15 -6
  73. package/src/core/rules/read-stale-guard.js.map +1 -1
  74. package/src/core/rules/redirect-how-to-merge-main.d.ts +3 -3
  75. package/src/core/rules/redirect-how-to-merge-main.js +1 -1
  76. package/src/core/rules/redirect-how-to-merge-main.js.map +1 -1
  77. package/src/core/rules/require-return-type.js +1 -1
  78. package/src/core/rules/require-return-type.js.map +1 -1
  79. package/src/core/rules/stale-main-bash-guard.d.ts +3 -3
  80. package/src/core/rules/stale-main-bash-guard.js +5 -4
  81. package/src/core/rules/stale-main-bash-guard.js.map +1 -1
  82. package/src/core/rules/throw-cause-required.js +1 -1
  83. package/src/core/rules/throw-cause-required.js.map +1 -1
  84. package/src/core/rules/validate-ts-in-src.js +1 -1
  85. package/src/core/rules/validate-ts-in-src.js.map +1 -1
  86. package/src/core/rules/whole-repo-build-guard.js +6 -2
  87. package/src/core/rules/whole-repo-build-guard.js.map +1 -1
  88. package/src/core/runner.js +41 -22
  89. package/src/core/runner.js.map +1 -1
  90. package/src/core/sync-main.d.ts +9 -0
  91. package/src/core/sync-main.js +17 -1
  92. package/src/core/sync-main.js.map +1 -1
  93. package/src/core/types.d.ts +11 -0
  94. package/src/core/types.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"no-custom-css.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-custom-css.ts"],"names":[],"mappings":"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","sourcesContent":["import { NoCustomCssConfig, RULE_NAMES } from '@webpieces/rules-config';\n\nimport type { EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { EditRuleBase } from '../rule-base';\nimport { FixHint, Option, DisableEscape } from '../fix-hint';\n\n// Edit-time (regex/line) counterpart to the CI code-rule validate-no-custom-css. The hook has no TS\n// AST, so the `.ts` side matches the @Component style props by shape; the `.html` side matches inline\n// style attributes/bindings. Both are diff-precise via the hook's per-line edit context.\nconst RE_STYLE_URLS = /(^|\\s)styleUrls?\\s*:/; // styleUrls: / styleUrl:\nconst RE_STYLES_ARRAY = /(^|\\s)styles\\s*:\\s*\\[/; // styles: [ ... ] (component inline CSS)\nconst RE_INLINE_STYLE = /(^|\\s)style\\s*=\\s*[\"']/; // static style=\"…\"\nconst RE_NG_STYLE = /\\[?ngStyle\\]?\\s*=/; // [ngStyle]=\"…\"\nconst RE_STYLE_BINDING = /\\[style(?:\\.[\\w.-]+)?\\]/; // [style] / [style.width]\n\nconst TEST_PATHS: RegExp[] = [/\\.test\\.ts$/, /\\.spec\\.ts$/, /__tests__\\//];\n\nexport class NoCustomCssRule extends EditRuleBase<NoCustomCssConfig> {\n constructor(config: NoCustomCssConfig) { super(config, 'no-custom-css'); }\n\n readonly description =\n 'Ban hand-written CSS in Angular (styles/styleUrls in @Component, inline style=, [style.x], [ngStyle]) — style with Tailwind utility classes.';\n override readonly files = ['**/*.ts', '**/*.tsx', '**/*.html'];\n override readonly defaultOptions = { allowGlobs: [] };\n\n get fixHint(): FixHint {\n return new FixHint(\n 'Custom CSS bypasses Tailwind-first styling.',\n 'Pick one:',\n [\n new Option('Delete the CSS and use Tailwind utility classes (flex, grid, gap-4, text-red-600).', true),\n new Option('Need a specific value? Use an arbitrary-value class: w-[240px], bg-[#fffde7], grid-cols-[2fr_2fr_48px].'),\n new Option('Dynamic on/off? Prefer [class.x]=\"cond\" over a style binding.'),\n ],\n new DisableEscape(this.config.disableAllowed ?? true, '// webpieces-disable no-custom-css -- <reason>'),\n );\n }\n\n check(ctx: EditContext): readonly Violation[] {\n const allowGlobs = this.config.allowGlobs ?? [];\n if (this.isAllowedPath(ctx.relativePath, allowGlobs)) return [];\n\n const isHtml = ctx.relativePath.endsWith('.html');\n const disableAllowed = this.config.disableAllowed ?? true;\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const detail = this.detailForLine(ctx.strippedLines[i] ?? '', isHtml);\n if (!detail) continue;\n const lineNum = i + 1;\n if (disableAllowed && ctx.isLineDisabled(lineNum, RULE_NAMES.NO_CUSTOM_CSS)) continue;\n violations.push(new V(lineNum, `${detail}: ${ctx.lines[i]?.trim() ?? ''}`));\n }\n return violations;\n }\n\n /** The banned-pattern label for a line, or '' when clean. Branches on the file being a template. */\n private detailForLine(line: string, isHtml: boolean): string {\n if (isHtml) {\n if (RE_NG_STYLE.test(line)) return 'inline `[ngStyle]`';\n if (RE_STYLE_BINDING.test(line)) return 'inline `[style.x]` binding';\n if (RE_INLINE_STYLE.test(line)) return 'inline `style=` attribute';\n return '';\n }\n if (RE_STYLE_URLS.test(line)) return '`styleUrls`/`styleUrl` in @Component';\n if (RE_STYLES_ARRAY.test(line)) return '`styles` block in @Component';\n return '';\n }\n\n private isAllowedPath(relativePath: string, allowGlobs: readonly string[]): boolean {\n if (TEST_PATHS.some((re: RegExp) => re.test(relativePath))) return true;\n return allowGlobs.some((pattern: string) => this.globToRegex(pattern).test(relativePath));\n }\n\n private globToRegex(pattern: string): RegExp {\n let re = '';\n let i = 0;\n while (i < pattern.length) {\n const ch = pattern[i];\n if (ch === '*') {\n if (pattern[i + 1] === '*') {\n re += '.*';\n i += 2;\n if (pattern[i] === '/') i += 1;\n continue;\n }\n re += '[^/]*';\n i += 1;\n continue;\n }\n if (ch === '?') {\n re += '[^/]';\n i += 1;\n continue;\n }\n if ('.+^$(){}|[]\\\\'.includes(ch)) {\n re += '\\\\' + ch;\n i += 1;\n continue;\n }\n re += ch;\n i += 1;\n }\n return new RegExp('^' + re + '$');\n }\n}\n"]}
1
+ {"version":3,"file":"no-custom-css.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-custom-css.ts"],"names":[],"mappings":"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","sourcesContent":["import { NoCustomCssConfig, RULE_NAMES } from '@webpieces/rules-config';\n\nimport type { EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { EditRuleBase } from '../rule-base';\nimport { FixHint, Option, DisableEscape } from '../fix-hint';\n\n// Edit-time (regex/line) counterpart to the CI code-rule validate-no-custom-css. The hook has no TS\n// AST, so the `.ts` side matches the @Component style props by shape; the `.html` side matches inline\n// style attributes/bindings. Both are diff-precise via the hook's per-line edit context.\nconst RE_STYLE_URLS = /(^|\\s)styleUrls?\\s*:/; // styleUrls: / styleUrl:\nconst RE_STYLES_ARRAY = /(^|\\s)styles\\s*:\\s*\\[/; // styles: [ ... ] (component inline CSS)\nconst RE_INLINE_STYLE = /(^|\\s)style\\s*=\\s*[\"']/; // static style=\"…\"\nconst RE_NG_STYLE = /\\[?ngStyle\\]?\\s*=/; // [ngStyle]=\"…\"\nconst RE_STYLE_BINDING = /\\[style(?:\\.[\\w.-]+)?\\]/; // [style] / [style.width]\n\nconst TEST_PATHS: RegExp[] = [/\\.test\\.ts$/, /\\.spec\\.ts$/, /__tests__\\//];\n\nexport class NoCustomCssRule extends EditRuleBase<NoCustomCssConfig> {\n constructor(config: NoCustomCssConfig) { super(config, 'no-custom-css', 'no-custom-css'); }\n\n readonly description =\n 'Ban hand-written CSS in Angular (styles/styleUrls in @Component, inline style=, [style.x], [ngStyle]) — style with Tailwind utility classes.';\n override readonly files = ['**/*.ts', '**/*.tsx', '**/*.html'];\n override readonly defaultOptions = { allowGlobs: [] };\n\n get fixHint(): FixHint {\n return new FixHint(\n 'Custom CSS bypasses Tailwind-first styling.',\n 'Pick one:',\n [\n new Option('Delete the CSS and use Tailwind utility classes (flex, grid, gap-4, text-red-600).', true),\n new Option('Need a specific value? Use an arbitrary-value class: w-[240px], bg-[#fffde7], grid-cols-[2fr_2fr_48px].'),\n new Option('Dynamic on/off? Prefer [class.x]=\"cond\" over a style binding.'),\n ],\n new DisableEscape(this.config.disableAllowed ?? true, '// webpieces-disable no-custom-css -- <reason>'),\n );\n }\n\n check(ctx: EditContext): readonly Violation[] {\n const allowGlobs = this.config.allowGlobs ?? [];\n if (this.isAllowedPath(ctx.relativePath, allowGlobs)) return [];\n\n const isHtml = ctx.relativePath.endsWith('.html');\n const disableAllowed = this.config.disableAllowed ?? true;\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const detail = this.detailForLine(ctx.strippedLines[i] ?? '', isHtml);\n if (!detail) continue;\n const lineNum = i + 1;\n if (disableAllowed && ctx.isLineDisabled(lineNum, RULE_NAMES.NO_CUSTOM_CSS)) continue;\n violations.push(new V(lineNum, `${detail}: ${ctx.lines[i]?.trim() ?? ''}`));\n }\n return violations;\n }\n\n /** The banned-pattern label for a line, or '' when clean. Branches on the file being a template. */\n private detailForLine(line: string, isHtml: boolean): string {\n if (isHtml) {\n if (RE_NG_STYLE.test(line)) return 'inline `[ngStyle]`';\n if (RE_STYLE_BINDING.test(line)) return 'inline `[style.x]` binding';\n if (RE_INLINE_STYLE.test(line)) return 'inline `style=` attribute';\n return '';\n }\n if (RE_STYLE_URLS.test(line)) return '`styleUrls`/`styleUrl` in @Component';\n if (RE_STYLES_ARRAY.test(line)) return '`styles` block in @Component';\n return '';\n }\n\n private isAllowedPath(relativePath: string, allowGlobs: readonly string[]): boolean {\n if (TEST_PATHS.some((re: RegExp) => re.test(relativePath))) return true;\n return allowGlobs.some((pattern: string) => this.globToRegex(pattern).test(relativePath));\n }\n\n private globToRegex(pattern: string): RegExp {\n let re = '';\n let i = 0;\n while (i < pattern.length) {\n const ch = pattern[i];\n if (ch === '*') {\n if (pattern[i + 1] === '*') {\n re += '.*';\n i += 2;\n if (pattern[i] === '/') i += 1;\n continue;\n }\n re += '[^/]*';\n i += 1;\n continue;\n }\n if (ch === '?') {\n re += '[^/]';\n i += 1;\n continue;\n }\n if ('.+^$(){}|[]\\\\'.includes(ch)) {\n re += '\\\\' + ch;\n i += 1;\n continue;\n }\n re += ch;\n i += 1;\n }\n return new RegExp('^' + re + '$');\n }\n}\n"]}
@@ -7,7 +7,7 @@ const rule_base_1 = require("../rule-base");
7
7
  const fix_hint_1 = require("../fix-hint");
8
8
  const VARIABLE_DESTRUCTURE = /\b(?:const|let|var)\s*\{/;
9
9
  class NoDestructureRule extends rule_base_1.EditRuleBase {
10
- constructor(config) { super(config, 'no-destructure'); }
10
+ constructor(config) { super(config, 'no-destructure', 'no-destructure'); }
11
11
  description = 'Disallow destructuring patterns. Assign the whole result and pass it around or access properties explicitly.';
12
12
  files = ['**/*.ts', '**/*.tsx'];
13
13
  defaultOptions = { allowTopLevel: true, allowedPaths: [] };
@@ -1 +1 @@
1
- {"version":3,"file":"no-destructure.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-destructure.ts"],"names":[],"mappings":";;;AAAA,0DAA0F;AAG1F,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAA6D;AAE7D,MAAM,oBAAoB,GAAG,0BAA0B,CAAC;AAExD,MAAa,iBAAkB,SAAQ,wBAAiC;IACpE,YAAY,MAA2B,IAAI,KAAK,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAEpE,WAAW,GAAG,8GAA8G,CAAC;IACpH,KAAK,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAChC,cAAc,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC;IAC7E,IAAI,OAAO;QACP,OAAO,IAAI,kBAAO,CACd,yCAAyC,EACzC,WAAW,EACX;YACI,IAAI,iBAAM,CAAC,+FAA+F,EAAE,IAAI,CAAC;YACjH,IAAI,iBAAM,CAAC,uKAAuK,CAAC;SACtL,EACD,IAAI,wBAAa,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,EAAE,iDAAiD,CAAC,CAC3G,CAAC;IACN,CAAC;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,EAAE,CAAC;QACpD,IAAI,IAAA,6BAAc,EAAC,GAAG,CAAC,YAAY,EAAE,YAAY,CAAC;YAAE,OAAO,EAAE,CAAC;QAE9D,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,CAAC;QAC1D,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,cAAc,IAAI,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,yBAAU,CAAC,cAAc,CAAC;gBAAE,SAAS;YACvF,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ;AAjCD,8CAiCC","sourcesContent":["import { NoDestructureConfig, RULE_NAMES, isPathExcluded } from '@webpieces/rules-config';\n\nimport type { EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { EditRuleBase } from '../rule-base';\nimport { FixHint, DisableEscape, Option } from '../fix-hint';\n\nconst VARIABLE_DESTRUCTURE = /\\b(?:const|let|var)\\s*\\{/;\n\nexport class NoDestructureRule extends EditRuleBase<NoDestructureConfig> {\n constructor(config: NoDestructureConfig) { super(config, 'no-destructure'); }\n\n readonly description = 'Disallow destructuring patterns. Assign the whole result and pass it around or access properties explicitly.';\n override readonly files = ['**/*.ts', '**/*.tsx'];\n override readonly defaultOptions = { allowTopLevel: true, allowedPaths: [] };\n get fixHint(): FixHint {\n return new FixHint(\n 'Destructuring pattern on a call result.',\n 'Pick one:',\n [\n new Option('Assign the whole result instead: const obj = methodCall(); then pass obj around or use obj.x.', true),\n new Option('If this whole tree is destructuring by construction (a React/React Native component or hook tree), add a glob to no-destructure.allowedPaths in webpieces.config.json'),\n ],\n new DisableEscape(this.config.disableAllowed ?? true, '// webpieces-disable no-destructure -- <reason>'),\n );\n }\n\n check(ctx: EditContext): readonly Violation[] {\n const allowedPaths = this.config.allowedPaths ?? [];\n if (isPathExcluded(ctx.relativePath, allowedPaths)) return [];\n\n const disableAllowed = this.config.disableAllowed ?? true;\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 (disableAllowed && ctx.isLineDisabled(lineNum, RULE_NAMES.NO_DESTRUCTURE)) continue;\n violations.push(new V(lineNum, ctx.lines[i].trim()));\n }\n return violations;\n }\n}\n"]}
1
+ {"version":3,"file":"no-destructure.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-destructure.ts"],"names":[],"mappings":";;;AAAA,0DAA0F;AAG1F,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAA6D;AAE7D,MAAM,oBAAoB,GAAG,0BAA0B,CAAC;AAExD,MAAa,iBAAkB,SAAQ,wBAAiC;IACpE,YAAY,MAA2B,IAAI,KAAK,CAAC,MAAM,EAAE,gBAAgB,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAEtF,WAAW,GAAG,8GAA8G,CAAC;IACpH,KAAK,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAChC,cAAc,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC;IAC7E,IAAI,OAAO;QACP,OAAO,IAAI,kBAAO,CACd,yCAAyC,EACzC,WAAW,EACX;YACI,IAAI,iBAAM,CAAC,+FAA+F,EAAE,IAAI,CAAC;YACjH,IAAI,iBAAM,CAAC,uKAAuK,CAAC;SACtL,EACD,IAAI,wBAAa,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,EAAE,iDAAiD,CAAC,CAC3G,CAAC;IACN,CAAC;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,EAAE,CAAC;QACpD,IAAI,IAAA,6BAAc,EAAC,GAAG,CAAC,YAAY,EAAE,YAAY,CAAC;YAAE,OAAO,EAAE,CAAC;QAE9D,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,CAAC;QAC1D,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,cAAc,IAAI,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,yBAAU,CAAC,cAAc,CAAC;gBAAE,SAAS;YACvF,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ;AAjCD,8CAiCC","sourcesContent":["import { NoDestructureConfig, RULE_NAMES, isPathExcluded } from '@webpieces/rules-config';\n\nimport type { EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { EditRuleBase } from '../rule-base';\nimport { FixHint, DisableEscape, Option } from '../fix-hint';\n\nconst VARIABLE_DESTRUCTURE = /\\b(?:const|let|var)\\s*\\{/;\n\nexport class NoDestructureRule extends EditRuleBase<NoDestructureConfig> {\n constructor(config: NoDestructureConfig) { super(config, 'no-destructure', 'no-destructure'); }\n\n readonly description = 'Disallow destructuring patterns. Assign the whole result and pass it around or access properties explicitly.';\n override readonly files = ['**/*.ts', '**/*.tsx'];\n override readonly defaultOptions = { allowTopLevel: true, allowedPaths: [] };\n get fixHint(): FixHint {\n return new FixHint(\n 'Destructuring pattern on a call result.',\n 'Pick one:',\n [\n new Option('Assign the whole result instead: const obj = methodCall(); then pass obj around or use obj.x.', true),\n new Option('If this whole tree is destructuring by construction (a React/React Native component or hook tree), add a glob to no-destructure.allowedPaths in webpieces.config.json'),\n ],\n new DisableEscape(this.config.disableAllowed ?? true, '// webpieces-disable no-destructure -- <reason>'),\n );\n }\n\n check(ctx: EditContext): readonly Violation[] {\n const allowedPaths = this.config.allowedPaths ?? [];\n if (isPathExcluded(ctx.relativePath, allowedPaths)) return [];\n\n const disableAllowed = this.config.disableAllowed ?? true;\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 (disableAllowed && ctx.isLineDisabled(lineNum, RULE_NAMES.NO_DESTRUCTURE)) continue;\n violations.push(new V(lineNum, ctx.lines[i].trim()));\n }\n return violations;\n }\n}\n"]}
@@ -45,7 +45,7 @@ function findOffender(line) {
45
45
  return null;
46
46
  }
47
47
  class NoImplicitAnyRule extends rule_base_1.EditRuleBase {
48
- constructor(config) { super(config, 'no-implicit-any'); }
48
+ constructor(config) { super(config, 'no-implicit-any', 'no-implicit-any'); }
49
49
  description = 'Disallow function parameters without explicit type annotations (implicit-any).';
50
50
  files = ['**/*.ts', '**/*.tsx'];
51
51
  get fixHint() {
@@ -1 +1 @@
1
- {"version":3,"file":"no-implicit-any.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-implicit-any.ts"],"names":[],"mappings":";;;AAAA,0DAA0E;AAG1E,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAAqD;AAErD,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAC7C,MAAM,iBAAiB,GAAG,qCAAqC,CAAC;AAEhE,SAAS,iBAAiB,CAAC,SAAiB;IACxC,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACpE,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACpG,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;YAAE,SAAS;QACrC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,SAAS;QACjC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,SAAS;QACjC,IAAI,IAAI,KAAK,MAAM;YAAE,SAAS;QAC9B,IAAI,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IACrD,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,SAAS,YAAY,CAAC,IAAY;IAC9B,eAAe,CAAC,SAAS,GAAG,CAAC,CAAC;IAC9B,IAAI,CAAC,GAA2B,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3D,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;QAChB,MAAM,GAAG,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACpC,IAAI,GAAG;YAAE,OAAO,GAAG,CAAC;QACpB,CAAC,GAAG,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;IACD,iBAAiB,CAAC,SAAS,GAAG,CAAC,CAAC;IAChC,CAAC,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;QAChB,MAAM,GAAG,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACpC,IAAI,GAAG;YAAE,OAAO,GAAG,CAAC;QACpB,CAAC,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,MAAa,iBAAkB,SAAQ,wBAAiC;IACpE,YAAY,MAA2B,IAAI,KAAK,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAErE,WAAW,GAAG,gFAAgF,CAAC;IACtF,KAAK,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAClD,IAAI,OAAO;QACP,OAAO,IAAI,kBAAO,CACd,oDAAoD,EACpD,oEAAoE,EACpE,EAAE,EACF,IAAI,wBAAa,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,EAAE,2DAA2D,CAAC,CACrH,CAAC;IACN,CAAC;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,CAAC;QAC1D,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,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;YACtB,IAAI,cAAc,IAAI,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,yBAAU,CAAC,eAAe,CAAC;gBAAE,SAAS;YACxF,MAAM,QAAQ,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;YACxC,IAAI,CAAC,QAAQ;gBAAE,SAAS;YACxB,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CACjB,OAAO,EACP,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EACnB,cAAc,QAAQ,uEAAuE,CAChG,CAAC,CAAC;QACP,CAAC;QACD,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ;AA/BD,8CA+BC","sourcesContent":["import { NoImplicitAnyConfig, RULE_NAMES } from '@webpieces/rules-config';\n\nimport type { EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { EditRuleBase } from '../rule-base';\nimport { FixHint, DisableEscape } from '../fix-hint';\n\nconst ARROW_PARAMS_RE = /\\(([^()]*)\\)\\s*=>/g;\nconst FN_DECL_PARAMS_RE = /\\bfunction\\s*[\\w$]*\\s*\\(([^()]*)\\)/g;\n\nfunction firstUntypedParam(paramsStr: string): string | null {\n if (paramsStr.includes('{') || paramsStr.includes('[')) return null;\n const parts = paramsStr.split(',').map((p: string) => p.trim()).filter((p: string) => p.length > 0);\n for (const part of parts) {\n if (part.startsWith('...')) continue;\n if (part.includes(':')) continue;\n if (part.includes('=')) continue;\n if (part === 'this') continue;\n if (/^[a-zA-Z_$][\\w$]*$/.test(part)) return part;\n }\n return null;\n}\n\nfunction findOffender(line: string): string | null {\n ARROW_PARAMS_RE.lastIndex = 0;\n let m: RegExpExecArray | null = ARROW_PARAMS_RE.exec(line);\n while (m !== null) {\n const bad = firstUntypedParam(m[1]);\n if (bad) return bad;\n m = ARROW_PARAMS_RE.exec(line);\n }\n FN_DECL_PARAMS_RE.lastIndex = 0;\n m = FN_DECL_PARAMS_RE.exec(line);\n while (m !== null) {\n const bad = firstUntypedParam(m[1]);\n if (bad) return bad;\n m = FN_DECL_PARAMS_RE.exec(line);\n }\n return null;\n}\n\nexport class NoImplicitAnyRule extends EditRuleBase<NoImplicitAnyConfig> {\n constructor(config: NoImplicitAnyConfig) { super(config, 'no-implicit-any'); }\n\n readonly description = 'Disallow function parameters without explicit type annotations (implicit-any).';\n override readonly files = ['**/*.ts', '**/*.tsx'];\n get fixHint(): FixHint {\n return new FixHint(\n 'A parameter has no type annotation (implicit any).',\n 'Add explicit types: (x: string) => ... or function foo(x: number).',\n [],\n new DisableEscape(this.config.disableAllowed ?? true, '// webpieces-disable no-implicit-any -- <one-line reason>'),\n );\n }\n\n check(ctx: EditContext): readonly Violation[] {\n const disableAllowed = this.config.disableAllowed ?? true;\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const stripped = ctx.strippedLines[i];\n const lineNum = i + 1;\n if (disableAllowed && ctx.isLineDisabled(lineNum, RULE_NAMES.NO_IMPLICIT_ANY)) continue;\n const offender = findOffender(stripped);\n if (!offender) continue;\n violations.push(new V(\n lineNum,\n ctx.lines[i].trim(),\n `Parameter \"${offender}\" has no type annotation. Add an explicit type to avoid implicit-any.`,\n ));\n }\n return violations;\n }\n}\n"]}
1
+ {"version":3,"file":"no-implicit-any.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-implicit-any.ts"],"names":[],"mappings":"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","sourcesContent":["import { NoImplicitAnyConfig, RULE_NAMES } from '@webpieces/rules-config';\n\nimport type { EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { EditRuleBase } from '../rule-base';\nimport { FixHint, DisableEscape } from '../fix-hint';\n\nconst ARROW_PARAMS_RE = /\\(([^()]*)\\)\\s*=>/g;\nconst FN_DECL_PARAMS_RE = /\\bfunction\\s*[\\w$]*\\s*\\(([^()]*)\\)/g;\n\nfunction firstUntypedParam(paramsStr: string): string | null {\n if (paramsStr.includes('{') || paramsStr.includes('[')) return null;\n const parts = paramsStr.split(',').map((p: string) => p.trim()).filter((p: string) => p.length > 0);\n for (const part of parts) {\n if (part.startsWith('...')) continue;\n if (part.includes(':')) continue;\n if (part.includes('=')) continue;\n if (part === 'this') continue;\n if (/^[a-zA-Z_$][\\w$]*$/.test(part)) return part;\n }\n return null;\n}\n\nfunction findOffender(line: string): string | null {\n ARROW_PARAMS_RE.lastIndex = 0;\n let m: RegExpExecArray | null = ARROW_PARAMS_RE.exec(line);\n while (m !== null) {\n const bad = firstUntypedParam(m[1]);\n if (bad) return bad;\n m = ARROW_PARAMS_RE.exec(line);\n }\n FN_DECL_PARAMS_RE.lastIndex = 0;\n m = FN_DECL_PARAMS_RE.exec(line);\n while (m !== null) {\n const bad = firstUntypedParam(m[1]);\n if (bad) return bad;\n m = FN_DECL_PARAMS_RE.exec(line);\n }\n return null;\n}\n\nexport class NoImplicitAnyRule extends EditRuleBase<NoImplicitAnyConfig> {\n constructor(config: NoImplicitAnyConfig) { super(config, 'no-implicit-any', 'no-implicit-any'); }\n\n readonly description = 'Disallow function parameters without explicit type annotations (implicit-any).';\n override readonly files = ['**/*.ts', '**/*.tsx'];\n get fixHint(): FixHint {\n return new FixHint(\n 'A parameter has no type annotation (implicit any).',\n 'Add explicit types: (x: string) => ... or function foo(x: number).',\n [],\n new DisableEscape(this.config.disableAllowed ?? true, '// webpieces-disable no-implicit-any -- <one-line reason>'),\n );\n }\n\n check(ctx: EditContext): readonly Violation[] {\n const disableAllowed = this.config.disableAllowed ?? true;\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const stripped = ctx.strippedLines[i];\n const lineNum = i + 1;\n if (disableAllowed && ctx.isLineDisabled(lineNum, RULE_NAMES.NO_IMPLICIT_ANY)) continue;\n const offender = findOffender(stripped);\n if (!offender) continue;\n violations.push(new V(\n lineNum,\n ctx.lines[i].trim(),\n `Parameter \"${offender}\" has no type annotation. Add an explicit type to avoid implicit-any.`,\n ));\n }\n return violations;\n }\n}\n"]}
@@ -6,7 +6,7 @@ const types_1 = require("../types");
6
6
  const rule_base_1 = require("../rule-base");
7
7
  const fix_hint_1 = require("../fix-hint");
8
8
  class NoJsFilesRule extends rule_base_1.FileRuleBase {
9
- constructor(config) { super(config, 'no-js-files'); }
9
+ constructor(config) { super(config, 'no-js-files', 'no-js-files'); }
10
10
  description = 'Disallow writing new .js/.jsx files. Use .ts/.tsx instead.';
11
11
  files = ['**/*.js', '**/*.jsx'];
12
12
  defaultOptions = { allowedPaths: [] };
@@ -1 +1 @@
1
- {"version":3,"file":"no-js-files.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-js-files.ts"],"names":[],"mappings":";;;AAAA,0DAA0E;AAG1E,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAA8C;AAE9C,MAAa,aAAc,SAAQ,wBAA6B;IAC5D,YAAY,MAAuB,IAAI,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;IAE7D,WAAW,GAAG,4DAA4D,CAAC;IAClE,KAAK,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAChC,cAAc,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC;IAC/C,OAAO,GAAG,IAAI,kBAAO,CAC1B,8DAA8D,EAC9D,WAAW,EACX;QACI,IAAI,iBAAM,CAAC,+CAA+C,EAAE,IAAI,CAAC;QACjE,IAAI,iBAAM,CAAC,0GAA0G,CAAC;KACzH,CACJ,CAAC;IAEF,KAAK,CAAC,GAAgB;QAClB,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO;YAAE,OAAO,EAAE,CAAC;QAEpC,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,EAAE,CAAC;QAEpD,IAAI,IAAA,6BAAc,EAAC,GAAG,CAAC,YAAY,EAAE,YAAY,CAAC;YAAE,OAAO,EAAE,CAAC;QAE9D,OAAO,CAAC,IAAI,iBAAC,CAAC,CAAC,EAAE,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC;IACxC,CAAC;CACJ;AAxBD,sCAwBC","sourcesContent":["import { NoJsFilesConfig, isPathExcluded } from '@webpieces/rules-config';\n\nimport type { FileContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { FileRuleBase } from '../rule-base';\nimport { FixHint, Option } from '../fix-hint';\n\nexport class NoJsFilesRule extends FileRuleBase<NoJsFilesConfig> {\n constructor(config: NoJsFilesConfig) { super(config, 'no-js-files'); }\n\n readonly description = 'Disallow writing new .js/.jsx files. Use .ts/.tsx instead.';\n override readonly files = ['**/*.js', '**/*.jsx'];\n override readonly defaultOptions = { allowedPaths: [] };\n readonly fixHint = new FixHint(\n 'Writing .js/.jsx files is not allowed. Use .ts/.tsx instead.',\n 'Pick one:',\n [\n new Option('Write a .ts or .tsx file instead of .js/.jsx.', true),\n new Option('If this path must be .js (generated/legacy), add it to no-js-files.allowedPaths in webpieces.config.json'),\n ],\n );\n\n check(ctx: FileContext): readonly Violation[] {\n if (ctx.tool !== 'Write') return [];\n\n const allowedPaths = this.config.allowedPaths ?? [];\n\n if (isPathExcluded(ctx.relativePath, allowedPaths)) return [];\n\n return [new V(1, ctx.relativePath)];\n }\n}\n"]}
1
+ {"version":3,"file":"no-js-files.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-js-files.ts"],"names":[],"mappings":";;;AAAA,0DAA0E;AAG1E,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAA8C;AAE9C,MAAa,aAAc,SAAQ,wBAA6B;IAC5D,YAAY,MAAuB,IAAI,KAAK,CAAC,MAAM,EAAE,aAAa,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;IAE5E,WAAW,GAAG,4DAA4D,CAAC;IAClE,KAAK,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAChC,cAAc,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC;IAC/C,OAAO,GAAG,IAAI,kBAAO,CAC1B,8DAA8D,EAC9D,WAAW,EACX;QACI,IAAI,iBAAM,CAAC,+CAA+C,EAAE,IAAI,CAAC;QACjE,IAAI,iBAAM,CAAC,0GAA0G,CAAC;KACzH,CACJ,CAAC;IAEF,KAAK,CAAC,GAAgB;QAClB,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO;YAAE,OAAO,EAAE,CAAC;QAEpC,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,EAAE,CAAC;QAEpD,IAAI,IAAA,6BAAc,EAAC,GAAG,CAAC,YAAY,EAAE,YAAY,CAAC;YAAE,OAAO,EAAE,CAAC;QAE9D,OAAO,CAAC,IAAI,iBAAC,CAAC,CAAC,EAAE,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC;IACxC,CAAC;CACJ;AAxBD,sCAwBC","sourcesContent":["import { NoJsFilesConfig, isPathExcluded } from '@webpieces/rules-config';\n\nimport type { FileContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { FileRuleBase } from '../rule-base';\nimport { FixHint, Option } from '../fix-hint';\n\nexport class NoJsFilesRule extends FileRuleBase<NoJsFilesConfig> {\n constructor(config: NoJsFilesConfig) { super(config, 'no-js-files', 'no-js-files'); }\n\n readonly description = 'Disallow writing new .js/.jsx files. Use .ts/.tsx instead.';\n override readonly files = ['**/*.js', '**/*.jsx'];\n override readonly defaultOptions = { allowedPaths: [] };\n readonly fixHint = new FixHint(\n 'Writing .js/.jsx files is not allowed. Use .ts/.tsx instead.',\n 'Pick one:',\n [\n new Option('Write a .ts or .tsx file instead of .js/.jsx.', true),\n new Option('If this path must be .js (generated/legacy), add it to no-js-files.allowedPaths in webpieces.config.json'),\n ],\n );\n\n check(ctx: FileContext): readonly Violation[] {\n if (ctx.tool !== 'Write') return [];\n\n const allowedPaths = this.config.allowedPaths ?? [];\n\n if (isPathExcluded(ctx.relativePath, allowedPaths)) return [];\n\n return [new V(1, ctx.relativePath)];\n }\n}\n"]}
@@ -26,7 +26,7 @@ function enclosingFunctionName(strippedLines, idx) {
26
26
  return null;
27
27
  }
28
28
  class NoProcessExitOutsideMainRule extends rule_base_1.EditRuleBase {
29
- constructor(config) { super(config, 'no-process-exit-outside-main'); }
29
+ constructor(config) { super(config, 'no-process-exit-outside-main', 'no-process-exit-outside-main'); }
30
30
  description = 'Disallow process.exit() outside a main()/runMain wrapper (and importing another module\'s main). A deep exit crashes a reused server/command too early; throw a semantic error and let main pick the exit code.';
31
31
  files = ['**/*.ts', '**/*.tsx'];
32
32
  defaultOptions = {};
@@ -1 +1 @@
1
- {"version":3,"file":"no-process-exit-outside-main.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-process-exit-outside-main.ts"],"names":[],"mappings":"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","sourcesContent":["import { NoProcessExitOutsideMainConfig, RULE_NAMES, writeTemplateIfMissing, RepoRootFinder } from '@webpieces/rules-config';\n\nimport type { EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { EditRuleBase } from '../rule-base';\nimport { FixHint, Option, DisableEscape } from '../fix-hint';\n\nconst INSTRUCT_FILE = 'webpieces.noexitinmain.md';\nconst EXIT_REGEX = /\\bprocess\\.exit\\s*\\(/;\n// `import { ... main ... } from '...'` — importing another module's `main` (incl. `main as X`).\nconst IMPORT_MAIN_REGEX = /^\\s*import\\b[^;]*\\{[^}]*\\bmain\\b[^}]*\\}[^;]*\\bfrom\\b/;\n// Named function / arrow-const openers, used to find the enclosing function's name. Deliberately\n// only matches DEFINITIONS (`function foo(`, `const foo = (`/`= function`), never a call like\n// `main()` — so `main().catch(...)` inside runMain is skipped and the scan reaches `function runMain`.\nconst FUNC_DEF_REGEX = /(?:export\\s+)?(?:async\\s+)?function\\s+(\\w+)\\s*\\(|(?:export\\s+)?(?:const|let)\\s+(\\w+)\\s*=\\s*(?:async\\s*)?(?:\\(|function\\b)/;\nconst ALLOWED_FN = new Set(['main', 'runMain']);\n\n// Walk backward from `idx` for the nearest NAMED function/arrow definition and return its name.\n// Line-based heuristic (the edit engine has no AST): good enough to recognise a `main`/`runMain`\n// wrapper enclosing the exit. Returns null at module scope (no enclosing named function).\nfunction enclosingFunctionName(strippedLines: readonly string[], idx: number): string | null {\n for (let j = idx; j >= 0; j -= 1) {\n const m = FUNC_DEF_REGEX.exec(strippedLines[j] ?? '');\n if (m) return m[1] ?? m[2] ?? null;\n }\n return null;\n}\n\nexport class NoProcessExitOutsideMainRule extends EditRuleBase<NoProcessExitOutsideMainConfig> {\n constructor(config: NoProcessExitOutsideMainConfig) { super(config, 'no-process-exit-outside-main'); }\n\n readonly description = 'Disallow process.exit() outside a main()/runMain wrapper (and importing another module\\'s main). A deep exit crashes a reused server/command too early; throw a semantic error and let main pick the exit code.';\n override readonly files = ['**/*.ts', '**/*.tsx'];\n override readonly defaultOptions = {};\n get fixHint(): FixHint {\n return new FixHint(\n 'process.exit() outside main()/runMain (or an import of another module\\'s `main`) — a deep exit can crash a reused server or command far too early, and exit 0 masquerades as success.',\n 'READ the instruct-ai doc at the absolute path on the violation line above, then:',\n [\n new Option('Throw a semantic error instead of exiting — RuleFailError for an expected failure, an ordinary Error for a bug/precondition — and let main()/runMain translate it to an exit code.', true),\n new Option('A bin owns the single exit: `if (require.main === module) runMain(main)`. Need a specific code deep down? `throw new CliExitError(code, message)`.'),\n new Option('Do not import another module\\'s `main` — call a named function; only a thin wrapper calls main.'),\n ],\n new DisableEscape(this.config.disableAllowed ?? true, '// webpieces-disable no-process-exit-outside-main -- <reason>'),\n );\n }\n\n check(ctx: EditContext): readonly Violation[] {\n const disableAllowed = this.config.disableAllowed ?? true;\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const stripped = ctx.strippedLines[i] ?? '';\n const isExit = EXIT_REGEX.test(stripped)\n && !ALLOWED_FN.has(enclosingFunctionName(ctx.strippedLines, i) ?? '');\n const isImportMain = IMPORT_MAIN_REGEX.test(stripped);\n if (!isExit && !isImportMain) continue;\n const lineNum = i + 1;\n if (disableAllowed && ctx.isLineDisabled(lineNum, RULE_NAMES.NO_PROCESS_EXIT_OUTSIDE_MAIN)) continue;\n const docPath = new RepoRootFinder().instructAiDocPath(ctx.workspaceRoot, INSTRUCT_FILE);\n violations.push(new V(lineNum, ctx.lines[i]?.trim() ?? '',\n `A process exit outside main()/runMain. READ ${docPath} — throw a semantic error and let main pick the exit code.`));\n }\n if (violations.length > 0) writeTemplateIfMissing(ctx.workspaceRoot, INSTRUCT_FILE);\n return violations;\n }\n}\n"]}
1
+ {"version":3,"file":"no-process-exit-outside-main.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-process-exit-outside-main.ts"],"names":[],"mappings":";;;AAAA,0DAA6H;AAG7H,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAA6D;AAE7D,MAAM,aAAa,GAAG,2BAA2B,CAAC;AAClD,MAAM,UAAU,GAAG,sBAAsB,CAAC;AAC1C,gGAAgG;AAChG,MAAM,iBAAiB,GAAG,sDAAsD,CAAC;AACjF,iGAAiG;AACjG,8FAA8F;AAC9F,uGAAuG;AACvG,MAAM,cAAc,GAAG,2HAA2H,CAAC;AACnJ,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;AAEhD,gGAAgG;AAChG,iGAAiG;AACjG,0FAA0F;AAC1F,SAAS,qBAAqB,CAAC,aAAgC,EAAE,GAAW;IACxE,KAAK,IAAI,CAAC,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,MAAM,CAAC,GAAG,cAAc,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACtD,IAAI,CAAC;YAAE,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IACvC,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,MAAa,4BAA6B,SAAQ,wBAA4C;IAC1F,YAAY,MAAsC,IAAI,KAAK,CAAC,MAAM,EAAE,8BAA8B,EAAE,8BAA8B,CAAC,CAAC,CAAC,CAAC;IAE7H,WAAW,GAAG,iNAAiN,CAAC;IACvN,KAAK,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAChC,cAAc,GAAG,EAAE,CAAC;IACtC,IAAI,OAAO;QACP,OAAO,IAAI,kBAAO,CACd,uLAAuL,EACvL,kFAAkF,EAClF;YACI,IAAI,iBAAM,CAAC,oLAAoL,EAAE,IAAI,CAAC;YACtM,IAAI,iBAAM,CAAC,oJAAoJ,CAAC;YAChK,IAAI,iBAAM,CAAC,iGAAiG,CAAC;SAChH,EACD,IAAI,wBAAa,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,EAAE,+DAA+D,CAAC,CACzH,CAAC;IACN,CAAC;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,CAAC;QAC1D,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,IAAI,EAAE,CAAC;YAC5C,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC;mBACjC,CAAC,UAAU,CAAC,GAAG,CAAC,qBAAqB,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YAC1E,MAAM,YAAY,GAAG,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACtD,IAAI,CAAC,MAAM,IAAI,CAAC,YAAY;gBAAE,SAAS;YACvC,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;YACtB,IAAI,cAAc,IAAI,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,yBAAU,CAAC,4BAA4B,CAAC;gBAAE,SAAS;YACrG,MAAM,OAAO,GAAG,IAAI,6BAAc,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;YACzF,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EACrD,+CAA+C,OAAO,4DAA4D,CAAC,CAAC,CAAC;QAC7H,CAAC;QACD,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;YAAE,IAAA,qCAAsB,EAAC,GAAG,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;QACpF,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ;AArCD,oEAqCC","sourcesContent":["import { NoProcessExitOutsideMainConfig, RULE_NAMES, writeTemplateIfMissing, RepoRootFinder } from '@webpieces/rules-config';\n\nimport type { EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { EditRuleBase } from '../rule-base';\nimport { FixHint, Option, DisableEscape } from '../fix-hint';\n\nconst INSTRUCT_FILE = 'webpieces.noexitinmain.md';\nconst EXIT_REGEX = /\\bprocess\\.exit\\s*\\(/;\n// `import { ... main ... } from '...'` — importing another module's `main` (incl. `main as X`).\nconst IMPORT_MAIN_REGEX = /^\\s*import\\b[^;]*\\{[^}]*\\bmain\\b[^}]*\\}[^;]*\\bfrom\\b/;\n// Named function / arrow-const openers, used to find the enclosing function's name. Deliberately\n// only matches DEFINITIONS (`function foo(`, `const foo = (`/`= function`), never a call like\n// `main()` — so `main().catch(...)` inside runMain is skipped and the scan reaches `function runMain`.\nconst FUNC_DEF_REGEX = /(?:export\\s+)?(?:async\\s+)?function\\s+(\\w+)\\s*\\(|(?:export\\s+)?(?:const|let)\\s+(\\w+)\\s*=\\s*(?:async\\s*)?(?:\\(|function\\b)/;\nconst ALLOWED_FN = new Set(['main', 'runMain']);\n\n// Walk backward from `idx` for the nearest NAMED function/arrow definition and return its name.\n// Line-based heuristic (the edit engine has no AST): good enough to recognise a `main`/`runMain`\n// wrapper enclosing the exit. Returns null at module scope (no enclosing named function).\nfunction enclosingFunctionName(strippedLines: readonly string[], idx: number): string | null {\n for (let j = idx; j >= 0; j -= 1) {\n const m = FUNC_DEF_REGEX.exec(strippedLines[j] ?? '');\n if (m) return m[1] ?? m[2] ?? null;\n }\n return null;\n}\n\nexport class NoProcessExitOutsideMainRule extends EditRuleBase<NoProcessExitOutsideMainConfig> {\n constructor(config: NoProcessExitOutsideMainConfig) { super(config, 'no-process-exit-outside-main', 'no-process-exit-outside-main'); }\n\n readonly description = 'Disallow process.exit() outside a main()/runMain wrapper (and importing another module\\'s main). A deep exit crashes a reused server/command too early; throw a semantic error and let main pick the exit code.';\n override readonly files = ['**/*.ts', '**/*.tsx'];\n override readonly defaultOptions = {};\n get fixHint(): FixHint {\n return new FixHint(\n 'process.exit() outside main()/runMain (or an import of another module\\'s `main`) — a deep exit can crash a reused server or command far too early, and exit 0 masquerades as success.',\n 'READ the instruct-ai doc at the absolute path on the violation line above, then:',\n [\n new Option('Throw a semantic error instead of exiting — RuleFailError for an expected failure, an ordinary Error for a bug/precondition — and let main()/runMain translate it to an exit code.', true),\n new Option('A bin owns the single exit: `if (require.main === module) runMain(main)`. Need a specific code deep down? `throw new CliExitError(code, message)`.'),\n new Option('Do not import another module\\'s `main` — call a named function; only a thin wrapper calls main.'),\n ],\n new DisableEscape(this.config.disableAllowed ?? true, '// webpieces-disable no-process-exit-outside-main -- <reason>'),\n );\n }\n\n check(ctx: EditContext): readonly Violation[] {\n const disableAllowed = this.config.disableAllowed ?? true;\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const stripped = ctx.strippedLines[i] ?? '';\n const isExit = EXIT_REGEX.test(stripped)\n && !ALLOWED_FN.has(enclosingFunctionName(ctx.strippedLines, i) ?? '');\n const isImportMain = IMPORT_MAIN_REGEX.test(stripped);\n if (!isExit && !isImportMain) continue;\n const lineNum = i + 1;\n if (disableAllowed && ctx.isLineDisabled(lineNum, RULE_NAMES.NO_PROCESS_EXIT_OUTSIDE_MAIN)) continue;\n const docPath = new RepoRootFinder().instructAiDocPath(ctx.workspaceRoot, INSTRUCT_FILE);\n violations.push(new V(lineNum, ctx.lines[i]?.trim() ?? '',\n `A process exit outside main()/runMain. READ ${docPath} — throw a semantic error and let main pick the exit code.`));\n }\n if (violations.length > 0) writeTemplateIfMissing(ctx.workspaceRoot, INSTRUCT_FILE);\n return violations;\n }\n}\n"]}
@@ -45,7 +45,7 @@ function isAllowedPath(relativePath, allowedPaths) {
45
45
  return allowedPaths.some((pattern) => globToRegex(pattern).test(relativePath));
46
46
  }
47
47
  class NoSymbolDiTokensRule extends rule_base_1.EditRuleBase {
48
- constructor(config) { super(config, 'no-symbol-di-tokens'); }
48
+ constructor(config) { super(config, 'no-symbol-di-tokens', 'no-symbol-di-tokens'); }
49
49
  description = 'Disallow Symbol() DI tokens outside explicitly configured paths. Use @injectable(bindingScopeValues.Singleton) + inject-by-type instead.';
50
50
  files = ['**/*.ts', '**/*.tsx'];
51
51
  defaultOptions = { allowedPaths: [] };
@@ -1 +1 @@
1
- {"version":3,"file":"no-symbol-di-tokens.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-symbol-di-tokens.ts"],"names":[],"mappings":"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","sourcesContent":["import { NoSymbolDiTokensConfig, RULE_NAMES } from '@webpieces/rules-config';\n\nimport type { EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { EditRuleBase } from '../rule-base';\nimport { FixHint, Option, DisableEscape } from '../fix-hint';\n\nconst SYMBOL_DI_REGEX = /=\\s*Symbol(?:\\.for)?\\(/;\n\nconst TEST_PATHS: RegExp[] = [/\\.test\\.ts$/, /\\.spec\\.ts$/, /__tests__\\//];\n\nfunction globToRegex(pattern: string): RegExp {\n let re = '';\n let i = 0;\n while (i < pattern.length) {\n const ch = pattern[i];\n if (ch === '*') {\n if (pattern[i + 1] === '*') {\n re += '.*';\n i += 2;\n if (pattern[i] === '/') i += 1;\n continue;\n }\n re += '[^/]*';\n i += 1;\n continue;\n }\n if (ch === '?') {\n re += '[^/]';\n i += 1;\n continue;\n }\n if ('.+^$(){}|[]\\\\'.includes(ch)) {\n re += '\\\\' + ch;\n i += 1;\n continue;\n }\n re += ch;\n i += 1;\n }\n return new RegExp('^' + re + '$');\n}\n\nfunction isAllowedPath(relativePath: string, allowedPaths: readonly string[]): boolean {\n if (TEST_PATHS.some((re: RegExp) => re.test(relativePath))) return true;\n return allowedPaths.some((pattern: string) => globToRegex(pattern).test(relativePath));\n}\n\nexport class NoSymbolDiTokensRule extends EditRuleBase<NoSymbolDiTokensConfig> {\n constructor(config: NoSymbolDiTokensConfig) { super(config, 'no-symbol-di-tokens'); }\n\n readonly description = 'Disallow Symbol() DI tokens outside explicitly configured paths. Use @injectable(bindingScopeValues.Singleton) + inject-by-type instead.';\n override readonly files = ['**/*.ts', '**/*.tsx'];\n override readonly defaultOptions = { allowedPaths: [] };\n get fixHint(): FixHint {\n return new FixHint(\n 'Symbol() used as a DI token — we avoid Symbol DI tokens where we can.',\n 'Pick one:',\n [\n new Option('Use @injectable(bindingScopeValues.Singleton) on the class and inject by type — no Symbol needed.', true),\n new Option('Implement an API interface — import the Symbol from the API definition and use @provideSingletonDefaultForApi(TOKEN).'),\n new Option('External lib class (DataSource, Anthropic, etc.) — bind<Cls>(Cls).toDynamicValue(...).inSingletonScope() — no Symbol.'),\n ],\n new DisableEscape(this.config.disableAllowed ?? true, '// webpieces-disable no-symbol-di-tokens -- <reason>'),\n );\n }\n\n check(ctx: EditContext): readonly Violation[] {\n const allowedPaths = this.config.allowedPaths ?? [];\n if (isAllowedPath(ctx.relativePath, allowedPaths)) return [];\n\n const disableAllowed = this.config.disableAllowed ?? true;\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const stripped = ctx.strippedLines[i];\n if (!SYMBOL_DI_REGEX.test(stripped ?? '')) continue;\n const lineNum = i + 1;\n if (disableAllowed && ctx.isLineDisabled(lineNum, RULE_NAMES.NO_SYMBOL_DI_TOKENS)) continue;\n violations.push(new V(lineNum, ctx.lines[i]?.trim() ?? ''));\n }\n return violations;\n }\n}\n"]}
1
+ {"version":3,"file":"no-symbol-di-tokens.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-symbol-di-tokens.ts"],"names":[],"mappings":"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","sourcesContent":["import { NoSymbolDiTokensConfig, RULE_NAMES } from '@webpieces/rules-config';\n\nimport type { EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { EditRuleBase } from '../rule-base';\nimport { FixHint, Option, DisableEscape } from '../fix-hint';\n\nconst SYMBOL_DI_REGEX = /=\\s*Symbol(?:\\.for)?\\(/;\n\nconst TEST_PATHS: RegExp[] = [/\\.test\\.ts$/, /\\.spec\\.ts$/, /__tests__\\//];\n\nfunction globToRegex(pattern: string): RegExp {\n let re = '';\n let i = 0;\n while (i < pattern.length) {\n const ch = pattern[i];\n if (ch === '*') {\n if (pattern[i + 1] === '*') {\n re += '.*';\n i += 2;\n if (pattern[i] === '/') i += 1;\n continue;\n }\n re += '[^/]*';\n i += 1;\n continue;\n }\n if (ch === '?') {\n re += '[^/]';\n i += 1;\n continue;\n }\n if ('.+^$(){}|[]\\\\'.includes(ch)) {\n re += '\\\\' + ch;\n i += 1;\n continue;\n }\n re += ch;\n i += 1;\n }\n return new RegExp('^' + re + '$');\n}\n\nfunction isAllowedPath(relativePath: string, allowedPaths: readonly string[]): boolean {\n if (TEST_PATHS.some((re: RegExp) => re.test(relativePath))) return true;\n return allowedPaths.some((pattern: string) => globToRegex(pattern).test(relativePath));\n}\n\nexport class NoSymbolDiTokensRule extends EditRuleBase<NoSymbolDiTokensConfig> {\n constructor(config: NoSymbolDiTokensConfig) { super(config, 'no-symbol-di-tokens', 'no-symbol-di-tokens'); }\n\n readonly description = 'Disallow Symbol() DI tokens outside explicitly configured paths. Use @injectable(bindingScopeValues.Singleton) + inject-by-type instead.';\n override readonly files = ['**/*.ts', '**/*.tsx'];\n override readonly defaultOptions = { allowedPaths: [] };\n get fixHint(): FixHint {\n return new FixHint(\n 'Symbol() used as a DI token — we avoid Symbol DI tokens where we can.',\n 'Pick one:',\n [\n new Option('Use @injectable(bindingScopeValues.Singleton) on the class and inject by type — no Symbol needed.', true),\n new Option('Implement an API interface — import the Symbol from the API definition and use @provideSingletonDefaultForApi(TOKEN).'),\n new Option('External lib class (DataSource, Anthropic, etc.) — bind<Cls>(Cls).toDynamicValue(...).inSingletonScope() — no Symbol.'),\n ],\n new DisableEscape(this.config.disableAllowed ?? true, '// webpieces-disable no-symbol-di-tokens -- <reason>'),\n );\n }\n\n check(ctx: EditContext): readonly Violation[] {\n const allowedPaths = this.config.allowedPaths ?? [];\n if (isAllowedPath(ctx.relativePath, allowedPaths)) return [];\n\n const disableAllowed = this.config.disableAllowed ?? true;\n const violations: V[] = [];\n for (let i = 0; i < ctx.strippedLines.length; i += 1) {\n const stripped = ctx.strippedLines[i];\n if (!SYMBOL_DI_REGEX.test(stripped ?? '')) continue;\n const lineNum = i + 1;\n if (disableAllowed && ctx.isLineDisabled(lineNum, RULE_NAMES.NO_SYMBOL_DI_TOKENS)) continue;\n violations.push(new V(lineNum, ctx.lines[i]?.trim() ?? ''));\n }\n return violations;\n }\n}\n"]}
@@ -17,7 +17,7 @@ function hasPrecedingDisable(lines, idx) {
17
17
  return DISABLE_PATTERN.test(prevLine);
18
18
  }
19
19
  class NoUnmanagedExceptionsRule extends rule_base_1.EditRuleBase {
20
- constructor(config) { super(config, 'no-unmanaged-exceptions'); }
20
+ constructor(config) { super(config, 'no-unmanaged-exceptions', 'no-unmanaged-exceptions'); }
21
21
  description = 'try/catch is generally not allowed. Only allowed in chokepoints (filter, globalErrorHandler) or other rare locations.';
22
22
  files = ['**/*.ts', '**/*.tsx'];
23
23
  get fixHint() {
@@ -1 +1 @@
1
- {"version":3,"file":"no-unmanaged-exceptions.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-unmanaged-exceptions.ts"],"names":[],"mappings":";;;AAAA,0DAAkG;AAGlG,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAA6D;AAC7D,8DAA+D;AAE/D,MAAM,aAAa,GAAG,yBAAyB,CAAC;AAChD,MAAM,WAAW,GAAG,YAAY,CAAC;AAEjC,8EAA8E;AAC9E,MAAM,eAAe,GAAG,gGAAgG,CAAC;AAEzH,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,MAAa,yBAA0B,SAAQ,wBAAyC;IACpF,YAAY,MAAmC,IAAI,KAAK,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC,CAAC,CAAC;IAErF,WAAW,GAAG,uHAAuH,CAAC;IAC7H,KAAK,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAClD,IAAI,OAAO;QACP,OAAO,IAAI,kBAAO,CACd,oLAAoL,EACpL,2JAA2J,EAC3J;YACI,IAAI,iBAAM,CAAC,kJAAkJ,EAAE,IAAI,CAAC;YACpK,IAAI,iBAAM,CACN,gFAAgF;kBAC9E,2CAA2C;kBAC3C,6FAA6F;kBAC7F,4EAA4E;kBAC5E,sEAAsE,CAC3E;SACJ,EACD,IAAI,wBAAa,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,EAAE,0DAA0D,CAAC,CACpH,CAAC;IACN,CAAC;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,CAAC;QAC1D,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,cAAc,IAAI,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,yBAAU,CAAC,uBAAuB,CAAC;gBAAE,SAAS;YAChG,IAAI,cAAc,IAAI,mBAAmB,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;gBAAE,SAAS;YAClE,MAAM,OAAO,GAAG,IAAI,6BAAc,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;YACzF,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAC9C,iDAAiD,OAAO,wEAAwE,CAAC,CAAC,CAAC;QAC3I,CAAC;QACD,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;YAAE,IAAA,2CAAsB,EAAC,GAAG,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;QACpF,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ;AAvCD,8DAuCC","sourcesContent":["import { NoUnmanagedExceptionsConfig, RULE_NAMES, RepoRootFinder } from '@webpieces/rules-config';\n\nimport type { EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { EditRuleBase } from '../rule-base';\nimport { FixHint, Option, DisableEscape } from '../fix-hint';\nimport { writeTemplateIfMissing } from '../instruct-ai-writer';\n\nconst INSTRUCT_FILE = 'webpieces.exceptions.md';\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\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 class NoUnmanagedExceptionsRule extends EditRuleBase<NoUnmanagedExceptionsConfig> {\n constructor(config: NoUnmanagedExceptionsConfig) { super(config, 'no-unmanaged-exceptions'); }\n\n readonly description = 'try/catch is generally not allowed. Only allowed in chokepoints (filter, globalErrorHandler) or other rare locations.';\n override readonly files = ['**/*.ts', '**/*.tsx'];\n get fixHint(): FixHint {\n return new FixHint(\n 'try/catch is generally not allowed. READ the instruct-ai doc at the absolute path on the violation line above. Only chokepoints (filter, globalErrorHandler) may catch exceptions.',\n 'Pick one (NOTE: for external calls — execSync, fs, network — the preferred option is almost always right; let it throw, the top-level runner reports it):',\n [\n new Option('Remove the try/catch — let the exception bubble to the top-level chokepoint (filter, globalErrorHandler) where it is already logged and handled.', true),\n new Option(\n 'If you genuinely believe this IS a chokepoint, STOP and ask the human first:\\n'\n + ' - What exception could be thrown here\\n'\n + ' - What the current top-level chokepoint is (the try/catch at the top of the call stack)\\n'\n + ' - Why throwing to that chokepoint would be wrong in this specific case\\n'\n + ' Then ask whether to remove the try/catch or add a disable comment.',\n ),\n ],\n new DisableEscape(this.config.disableAllowed ?? true, '// webpieces-disable no-unmanaged-exceptions -- <reason>'),\n );\n }\n\n check(ctx: EditContext): readonly Violation[] {\n const disableAllowed = this.config.disableAllowed ?? true;\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 (disableAllowed && ctx.isLineDisabled(lineNum, RULE_NAMES.NO_UNMANAGED_EXCEPTIONS)) continue;\n if (disableAllowed && hasPrecedingDisable(ctx.lines, i)) continue;\n const docPath = new RepoRootFinder().instructAiDocPath(ctx.workspaceRoot, INSTRUCT_FILE);\n violations.push(new V(lineNum, ctx.lines[i].trim(),\n `try/catch is generally not allowed here. READ ${docPath} — only chokepoints (filter, globalErrorHandler) may catch exceptions.`));\n }\n if (violations.length > 0) writeTemplateIfMissing(ctx.workspaceRoot, INSTRUCT_FILE);\n return violations;\n }\n}\n"]}
1
+ {"version":3,"file":"no-unmanaged-exceptions.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/no-unmanaged-exceptions.ts"],"names":[],"mappings":";;;AAAA,0DAAkG;AAGlG,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAA6D;AAC7D,8DAA+D;AAE/D,MAAM,aAAa,GAAG,yBAAyB,CAAC;AAChD,MAAM,WAAW,GAAG,YAAY,CAAC;AAEjC,8EAA8E;AAC9E,MAAM,eAAe,GAAG,gGAAgG,CAAC;AAEzH,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,MAAa,yBAA0B,SAAQ,wBAAyC;IACpF,YAAY,MAAmC,IAAI,KAAK,CAAC,MAAM,EAAE,yBAAyB,EAAE,yBAAyB,CAAC,CAAC,CAAC,CAAC;IAEhH,WAAW,GAAG,uHAAuH,CAAC;IAC7H,KAAK,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAClD,IAAI,OAAO;QACP,OAAO,IAAI,kBAAO,CACd,oLAAoL,EACpL,2JAA2J,EAC3J;YACI,IAAI,iBAAM,CAAC,kJAAkJ,EAAE,IAAI,CAAC;YACpK,IAAI,iBAAM,CACN,gFAAgF;kBAC9E,2CAA2C;kBAC3C,6FAA6F;kBAC7F,4EAA4E;kBAC5E,sEAAsE,CAC3E;SACJ,EACD,IAAI,wBAAa,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,EAAE,0DAA0D,CAAC,CACpH,CAAC;IACN,CAAC;IAED,KAAK,CAAC,GAAgB;QAClB,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,CAAC;QAC1D,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,cAAc,IAAI,GAAG,CAAC,cAAc,CAAC,OAAO,EAAE,yBAAU,CAAC,uBAAuB,CAAC;gBAAE,SAAS;YAChG,IAAI,cAAc,IAAI,mBAAmB,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;gBAAE,SAAS;YAClE,MAAM,OAAO,GAAG,IAAI,6BAAc,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;YACzF,UAAU,CAAC,IAAI,CAAC,IAAI,iBAAC,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAC9C,iDAAiD,OAAO,wEAAwE,CAAC,CAAC,CAAC;QAC3I,CAAC;QACD,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;YAAE,IAAA,2CAAsB,EAAC,GAAG,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;QACpF,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ;AAvCD,8DAuCC","sourcesContent":["import { NoUnmanagedExceptionsConfig, RULE_NAMES, RepoRootFinder } from '@webpieces/rules-config';\n\nimport type { EditContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { EditRuleBase } from '../rule-base';\nimport { FixHint, Option, DisableEscape } from '../fix-hint';\nimport { writeTemplateIfMissing } from '../instruct-ai-writer';\n\nconst INSTRUCT_FILE = 'webpieces.exceptions.md';\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\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 class NoUnmanagedExceptionsRule extends EditRuleBase<NoUnmanagedExceptionsConfig> {\n constructor(config: NoUnmanagedExceptionsConfig) { super(config, 'no-unmanaged-exceptions', 'no-unmanaged-exceptions'); }\n\n readonly description = 'try/catch is generally not allowed. Only allowed in chokepoints (filter, globalErrorHandler) or other rare locations.';\n override readonly files = ['**/*.ts', '**/*.tsx'];\n get fixHint(): FixHint {\n return new FixHint(\n 'try/catch is generally not allowed. READ the instruct-ai doc at the absolute path on the violation line above. Only chokepoints (filter, globalErrorHandler) may catch exceptions.',\n 'Pick one (NOTE: for external calls — execSync, fs, network — the preferred option is almost always right; let it throw, the top-level runner reports it):',\n [\n new Option('Remove the try/catch — let the exception bubble to the top-level chokepoint (filter, globalErrorHandler) where it is already logged and handled.', true),\n new Option(\n 'If you genuinely believe this IS a chokepoint, STOP and ask the human first:\\n'\n + ' - What exception could be thrown here\\n'\n + ' - What the current top-level chokepoint is (the try/catch at the top of the call stack)\\n'\n + ' - Why throwing to that chokepoint would be wrong in this specific case\\n'\n + ' Then ask whether to remove the try/catch or add a disable comment.',\n ),\n ],\n new DisableEscape(this.config.disableAllowed ?? true, '// webpieces-disable no-unmanaged-exceptions -- <reason>'),\n );\n }\n\n check(ctx: EditContext): readonly Violation[] {\n const disableAllowed = this.config.disableAllowed ?? true;\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 (disableAllowed && ctx.isLineDisabled(lineNum, RULE_NAMES.NO_UNMANAGED_EXCEPTIONS)) continue;\n if (disableAllowed && hasPrecedingDisable(ctx.lines, i)) continue;\n const docPath = new RepoRootFinder().instructAiDocPath(ctx.workspaceRoot, INSTRUCT_FILE);\n violations.push(new V(lineNum, ctx.lines[i].trim(),\n `try/catch is generally not allowed here. READ ${docPath} — only chokepoints (filter, globalErrorHandler) may catch exceptions.`));\n }\n if (violations.length > 0) writeTemplateIfMissing(ctx.workspaceRoot, INSTRUCT_FILE);\n return violations;\n }\n}\n"]}
@@ -1,10 +1,17 @@
1
- import { PrCreationOrPushGuardConfig } from '@webpieces/rules-config';
1
+ import { PrLifecycleGuardConfig } from '@webpieces/rules-config';
2
2
  import type { BashContext, Violation } from '../types';
3
3
  import { BashRuleBase } from '../rule-base';
4
4
  import { FixHint } from '../fix-hint';
5
- export declare class PrCreationOrPushGuardRule extends BashRuleBase<PrCreationOrPushGuardConfig> {
5
+ export declare class PrCreationOrPushGuardRule extends BashRuleBase<PrLifecycleGuardConfig> {
6
+ /**
7
+ * The gated command this guard names, handed in by the LOADER from `commands.guardHints
8
+ * .prCreationOrPush` (already defaulted there). It is a constructor parameter, not a config field:
9
+ * the field `pr-creation-or-push-guard.upsertPrCommand` was a SECOND SPELLING that beat the
10
+ * commands section at the point of use, and the loader's name-keyed injection that fed it could
11
+ * miss a rename without failing the build. One spelling, wired at compile time.
12
+ */
6
13
  private readonly upsertPrCommand;
7
- constructor(config: PrCreationOrPushGuardConfig);
14
+ constructor(config: PrLifecycleGuardConfig, upsertPrCommand: string);
8
15
  readonly description = "Block manual `git push` and direct PR creation/edit (gh pr / gh api / curl) so pushes and PRs go only through the gated upsert-pr command.";
9
16
  get fixHint(): FixHint;
10
17
  check(ctx: BashContext): readonly Violation[];
@@ -5,7 +5,6 @@ const rules_config_1 = require("@webpieces/rules-config");
5
5
  const types_1 = require("../types");
6
6
  const rule_base_1 = require("../rule-base");
7
7
  const fix_hint_1 = require("../fix-hint");
8
- const DEFAULT_UPSERT_PR_COMMAND = 'pnpm wp-start-upsert-pr';
9
8
  const INSTRUCT_FILE = 'webpieces.git-workflow.md';
10
9
  /**
11
10
  * The hint BRANCHES ON INTENT, and that is not polish.
@@ -82,10 +81,17 @@ function targetsDevEnvironment(cmd, namespace, devBranch) {
82
81
  return push[1].split(/[\s:]+/).some((token) => token === devBranch || token === `refs/heads/${devBranch}`);
83
82
  }
84
83
  class PrCreationOrPushGuardRule extends rule_base_1.BashRuleBase {
84
+ /**
85
+ * The gated command this guard names, handed in by the LOADER from `commands.guardHints
86
+ * .prCreationOrPush` (already defaulted there). It is a constructor parameter, not a config field:
87
+ * the field `pr-creation-or-push-guard.upsertPrCommand` was a SECOND SPELLING that beat the
88
+ * commands section at the point of use, and the loader's name-keyed injection that fed it could
89
+ * miss a rename without failing the build. One spelling, wired at compile time.
90
+ */
85
91
  upsertPrCommand;
86
- constructor(config) {
87
- super(config, 'pr-creation-or-push-guard');
88
- this.upsertPrCommand = config.upsertPrCommand ?? DEFAULT_UPSERT_PR_COMMAND;
92
+ constructor(config, upsertPrCommand) {
93
+ super(config, 'pr-creation-or-push-guard', rules_config_1.PR_LIFECYCLE_GUARD_KEY);
94
+ this.upsertPrCommand = upsertPrCommand;
89
95
  }
90
96
  description = 'Block manual `git push` and direct PR creation/edit (gh pr / gh api / curl) so pushes and PRs go only through the gated upsert-pr command.';
91
97
  get fixHint() { return fixHintFor(this.upsertPrCommand); }
@@ -1 +1 @@
1
- {"version":3,"file":"pr-creation-or-push-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/pr-creation-or-push-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DEFAULT_DEV_BRANCH,\n DEFAULT_DEV_BRANCH_NAMESPACE,\n PrCreationOrPushGuardConfig,\n RepoRootFinder,\n WP_PUSH_DEV,\n loadAndValidate,\n writeTemplate,\n} from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\nimport { FixHint } from '../fix-hint';\n\nconst DEFAULT_UPSERT_PR_COMMAND = 'pnpm wp-start-upsert-pr';\nconst INSTRUCT_FILE = 'webpieces.git-workflow.md';\n\n/**\n * The hint BRANCHES ON INTENT, and that is not polish.\n *\n * There are now two legitimate destinations for a blocked push — land it on `main`, or get it onto the\n * shared dev server — and the block alone cannot tell them apart. The old flat hint said \"everything goes\n * through `wp-start-upsert-pr`\", so an AI told \"put my branch on dev\" would try `git push`, get blocked,\n * read this, and open a PR to main: the exact opposite of what it was asked to do, and the destructive\n * direction of the two. Naming both destinations is what makes the dev flow discoverable at all — nothing\n * else the AI can see mentions it.\n */\nfunction fixHintFor(upsertPrCommand: string): FixHint {\n return new FixHint(\n 'Direct PR creation/update AND manual `git push` are blocked.',\n 'Never push or open/update a PR by hand. There are TWO gated destinations — pick by what you were\\n'\n + 'actually trying to do:\\n\\n'\n + 'LANDING ON MAIN (a PR, prod-quality, reviewed):\\n'\n + ` ${upsertPrCommand}\\n`\n + ' It updates the branch from main (3-point merge) and runs the real build (nx affected), then\\n'\n + ' instructs you to write review.json and run `pnpm wp-finish-upsert-pr`, which assembles the\\n'\n + ' dashboard and creates/updates the PR — and pushes for you. A failing build = no push, no PR.\\n\\n'\n + 'JUST WANT IT ON THE SHARED DEV SERVER (no PR, not landing on main):\\n'\n + ` ${WP_PUSH_DEV}\\n`\n + ' It publishes a DISPOSABLE copy of your branch that the dev environment composes and rebuilds.\\n'\n + ' Your feature branch is never moved and never acquires another developer\\'s commits, which is\\n'\n + ' the whole reason the copy exists. Do NOT open a PR to main just to test something in dev.\\n\\n'\n + 'Both push internally as child processes this hook never sees, so the gated commands are\\n'\n + 'unaffected by this guard. There is nothing to paste or attest to; the commands do the work.\\n'\n + 'If a HUMAN genuinely needs an out-of-band push (neither destination above), do NOT do it\\n'\n + 'yourself — ask them to run the push, since a manual push bypasses the build gate, review.json,\\n'\n + 'and dashboard.\\n'\n + 'Full branch → update → PR flow: READ the instruct-ai git-workflow doc at the absolute path on the violation line above.\\n'\n + 'Add this to your memory so you don\\'t forget next time and waste tokens.',\n );\n}\n\n// Detect every way an agent could push or open/update a PR directly, so the ONLY path left is the\n// gated flow (wp-start-upsert-pr → wp-finish-upsert-pr, whose internal `git push` / `gh pr create`\n// run as child processes the hook never sees). Read-only `gh pr list` / `gh api .../pulls` GET are\n// intentionally allowed.\nfunction isBlockedPrOrPush(cmd: string): boolean {\n // A manual push is always blocked — the gated flow pushes for you behind the build gate.\n if (/\\bgit\\s+push\\b/.test(cmd)) return true;\n\n if (/\\bgh\\s+pr\\s+(create|edit)\\b/.test(cmd)) return true;\n\n const ghApiPulls = /\\bgh\\s+api\\b[^\\n]*\\/pulls\\b/.test(cmd);\n if (ghApiPulls && (/--method\\s+POST/i.test(cmd) || /-X\\s+POST/i.test(cmd) || /\\s-f\\b/.test(cmd) || /\\s-F\\b/.test(cmd) || /--field\\b/.test(cmd))) {\n return true;\n }\n\n const curlPulls = /\\bcurl\\b[^\\n]*api\\.github\\.com[^\\n]*\\/pulls\\b/.test(cmd);\n if (curlPulls && (/-X\\s*POST/i.test(cmd) || /--request\\s+POST/i.test(cmd) || /(\\s-d\\b|--data\\b)/.test(cmd))) {\n return true;\n }\n return false;\n}\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\n/**\n * Does this blocked push AIM at the shared dev environment?\n *\n * The refspec is the one place the AI already stated its intent unambiguously, so reading it is strictly\n * better than making the AI choose between two hints it has no way to disambiguate. A push whose\n * destination is the dev namespace or the dev branch itself gets the `wp-push-dev` remedy on the `→`\n * line; everything else keeps the PR-flow remedy it has always had.\n */\n// webpieces-disable no-function-outside-class -- module-level guard helper, matches the rest of this file\nfunction targetsDevEnvironment(cmd: string, namespace: string, devBranch: string): boolean {\n if (cmd.includes(`${namespace}/`)) return true;\n const push = /\\bgit\\s+push\\b([^\\n;&|]*)/.exec(cmd);\n if (push === null) return false;\n // Split on whitespace AND `:` so both halves of a `HEAD:<dest>` refspec are examined. Matching whole\n // tokens (never a substring) keeps a branch merely NAMED `develop` from reading as the dev branch.\n return push[1].split(/[\\s:]+/).some(\n (token: string): boolean => token === devBranch || token === `refs/heads/${devBranch}`);\n}\n\nexport class PrCreationOrPushGuardRule extends BashRuleBase<PrCreationOrPushGuardConfig> {\n private readonly upsertPrCommand: string;\n\n constructor(config: PrCreationOrPushGuardConfig) {\n super(config, 'pr-creation-or-push-guard');\n this.upsertPrCommand = config.upsertPrCommand ?? DEFAULT_UPSERT_PR_COMMAND;\n }\n\n readonly description = 'Block manual `git push` and direct PR creation/edit (gh pr / gh api / curl) so pushes and PRs go only through the gated upsert-pr command.';\n get fixHint(): FixHint { return fixHintFor(this.upsertPrCommand); }\n\n check(ctx: BashContext): readonly Violation[] {\n if (!isBlockedPrOrPush(ctx.commandCode)) return [];\n // Materialize the doc we are about to send the AI to. Pointing at a path that does not exist\n // (nothing has run a `wp-*` command in this tree yet) costs the AI a turn to discover; a STALE\n // copy from an older @webpieces is just as bad, so overwrite rather than write-if-missing.\n writeTemplate(ctx.workspaceRoot, INSTRUCT_FILE);\n const docPath = new RepoRootFinder().instructAiDocPath(ctx.workspaceRoot, INSTRUCT_FILE);\n if (this.aimedAtDevEnvironment(ctx)) {\n return [new V(1, truncate(ctx.command),\n 'Manual push is blocked — but this one is aimed at the shared DEV environment, which has its own\\n'\n + `gated command. Use \\`${WP_PUSH_DEV}\\` (publishes a disposable copy; never moves your branch, never\\n`\n + `opens a PR). Do NOT use ${this.upsertPrCommand} for this — that lands on main. Full flow: READ ${docPath}.`)];\n }\n return [new V(1, truncate(ctx.command),\n `Manual push / direct PR is blocked — use the gated flow (${this.upsertPrCommand}). Full flow: READ ${docPath}.`)];\n }\n\n /**\n * Read the configured dev refs, falling back to the defaults when this repo has no `pr-gate` section\n * at all (a consumer that has not adopted the gate still gets a correct hint for the default names).\n *\n * Calling the loader here is safe by construction: an INVALID config blocks every Bash call long\n * before a rule runs, so reaching this line already means the config loaded.\n */\n private aimedAtDevEnvironment(ctx: BashContext): boolean {\n const devDeploy = loadAndValidate(ctx.governedRoot).prGate.devDeploy;\n return targetsDevEnvironment(\n ctx.commandCode,\n devDeploy.branchNamespace === '' ? DEFAULT_DEV_BRANCH_NAMESPACE : devDeploy.branchNamespace,\n devDeploy.devBranch === '' ? DEFAULT_DEV_BRANCH : devDeploy.devBranch);\n }\n}\n"]}
1
+ {"version":3,"file":"pr-creation-or-push-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/pr-creation-or-push-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DEFAULT_DEV_BRANCH,\n DEFAULT_DEV_BRANCH_NAMESPACE,\n PrLifecycleGuardConfig,\n PR_LIFECYCLE_GUARD_KEY,\n RepoRootFinder,\n WP_PUSH_DEV,\n loadAndValidate,\n writeTemplate,\n} from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\nimport { FixHint } from '../fix-hint';\n\nconst INSTRUCT_FILE = 'webpieces.git-workflow.md';\n\n/**\n * The hint BRANCHES ON INTENT, and that is not polish.\n *\n * There are now two legitimate destinations for a blocked push — land it on `main`, or get it onto the\n * shared dev server — and the block alone cannot tell them apart. The old flat hint said \"everything goes\n * through `wp-start-upsert-pr`\", so an AI told \"put my branch on dev\" would try `git push`, get blocked,\n * read this, and open a PR to main: the exact opposite of what it was asked to do, and the destructive\n * direction of the two. Naming both destinations is what makes the dev flow discoverable at all — nothing\n * else the AI can see mentions it.\n */\nfunction fixHintFor(upsertPrCommand: string): FixHint {\n return new FixHint(\n 'Direct PR creation/update AND manual `git push` are blocked.',\n 'Never push or open/update a PR by hand. There are TWO gated destinations — pick by what you were\\n'\n + 'actually trying to do:\\n\\n'\n + 'LANDING ON MAIN (a PR, prod-quality, reviewed):\\n'\n + ` ${upsertPrCommand}\\n`\n + ' It updates the branch from main (3-point merge) and runs the real build (nx affected), then\\n'\n + ' instructs you to write review.json and run `pnpm wp-finish-upsert-pr`, which assembles the\\n'\n + ' dashboard and creates/updates the PR — and pushes for you. A failing build = no push, no PR.\\n\\n'\n + 'JUST WANT IT ON THE SHARED DEV SERVER (no PR, not landing on main):\\n'\n + ` ${WP_PUSH_DEV}\\n`\n + ' It publishes a DISPOSABLE copy of your branch that the dev environment composes and rebuilds.\\n'\n + ' Your feature branch is never moved and never acquires another developer\\'s commits, which is\\n'\n + ' the whole reason the copy exists. Do NOT open a PR to main just to test something in dev.\\n\\n'\n + 'Both push internally as child processes this hook never sees, so the gated commands are\\n'\n + 'unaffected by this guard. There is nothing to paste or attest to; the commands do the work.\\n'\n + 'If a HUMAN genuinely needs an out-of-band push (neither destination above), do NOT do it\\n'\n + 'yourself — ask them to run the push, since a manual push bypasses the build gate, review.json,\\n'\n + 'and dashboard.\\n'\n + 'Full branch → update → PR flow: READ the instruct-ai git-workflow doc at the absolute path on the violation line above.\\n'\n + 'Add this to your memory so you don\\'t forget next time and waste tokens.',\n );\n}\n\n// Detect every way an agent could push or open/update a PR directly, so the ONLY path left is the\n// gated flow (wp-start-upsert-pr → wp-finish-upsert-pr, whose internal `git push` / `gh pr create`\n// run as child processes the hook never sees). Read-only `gh pr list` / `gh api .../pulls` GET are\n// intentionally allowed.\nfunction isBlockedPrOrPush(cmd: string): boolean {\n // A manual push is always blocked — the gated flow pushes for you behind the build gate.\n if (/\\bgit\\s+push\\b/.test(cmd)) return true;\n\n if (/\\bgh\\s+pr\\s+(create|edit)\\b/.test(cmd)) return true;\n\n const ghApiPulls = /\\bgh\\s+api\\b[^\\n]*\\/pulls\\b/.test(cmd);\n if (ghApiPulls && (/--method\\s+POST/i.test(cmd) || /-X\\s+POST/i.test(cmd) || /\\s-f\\b/.test(cmd) || /\\s-F\\b/.test(cmd) || /--field\\b/.test(cmd))) {\n return true;\n }\n\n const curlPulls = /\\bcurl\\b[^\\n]*api\\.github\\.com[^\\n]*\\/pulls\\b/.test(cmd);\n if (curlPulls && (/-X\\s*POST/i.test(cmd) || /--request\\s+POST/i.test(cmd) || /(\\s-d\\b|--data\\b)/.test(cmd))) {\n return true;\n }\n return false;\n}\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\n/**\n * Does this blocked push AIM at the shared dev environment?\n *\n * The refspec is the one place the AI already stated its intent unambiguously, so reading it is strictly\n * better than making the AI choose between two hints it has no way to disambiguate. A push whose\n * destination is the dev namespace or the dev branch itself gets the `wp-push-dev` remedy on the `→`\n * line; everything else keeps the PR-flow remedy it has always had.\n */\n// webpieces-disable no-function-outside-class -- module-level guard helper, matches the rest of this file\nfunction targetsDevEnvironment(cmd: string, namespace: string, devBranch: string): boolean {\n if (cmd.includes(`${namespace}/`)) return true;\n const push = /\\bgit\\s+push\\b([^\\n;&|]*)/.exec(cmd);\n if (push === null) return false;\n // Split on whitespace AND `:` so both halves of a `HEAD:<dest>` refspec are examined. Matching whole\n // tokens (never a substring) keeps a branch merely NAMED `develop` from reading as the dev branch.\n return push[1].split(/[\\s:]+/).some(\n (token: string): boolean => token === devBranch || token === `refs/heads/${devBranch}`);\n}\n\nexport class PrCreationOrPushGuardRule extends BashRuleBase<PrLifecycleGuardConfig> {\n /**\n * The gated command this guard names, handed in by the LOADER from `commands.guardHints\n * .prCreationOrPush` (already defaulted there). It is a constructor parameter, not a config field:\n * the field `pr-creation-or-push-guard.upsertPrCommand` was a SECOND SPELLING that beat the\n * commands section at the point of use, and the loader's name-keyed injection that fed it could\n * miss a rename without failing the build. One spelling, wired at compile time.\n */\n private readonly upsertPrCommand: string;\n\n constructor(config: PrLifecycleGuardConfig, upsertPrCommand: string) {\n super(config, 'pr-creation-or-push-guard', PR_LIFECYCLE_GUARD_KEY);\n this.upsertPrCommand = upsertPrCommand;\n }\n\n readonly description = 'Block manual `git push` and direct PR creation/edit (gh pr / gh api / curl) so pushes and PRs go only through the gated upsert-pr command.';\n get fixHint(): FixHint { return fixHintFor(this.upsertPrCommand); }\n\n check(ctx: BashContext): readonly Violation[] {\n if (!isBlockedPrOrPush(ctx.commandCode)) return [];\n // Materialize the doc we are about to send the AI to. Pointing at a path that does not exist\n // (nothing has run a `wp-*` command in this tree yet) costs the AI a turn to discover; a STALE\n // copy from an older @webpieces is just as bad, so overwrite rather than write-if-missing.\n writeTemplate(ctx.workspaceRoot, INSTRUCT_FILE);\n const docPath = new RepoRootFinder().instructAiDocPath(ctx.workspaceRoot, INSTRUCT_FILE);\n if (this.aimedAtDevEnvironment(ctx)) {\n return [new V(1, truncate(ctx.command),\n 'Manual push is blocked — but this one is aimed at the shared DEV environment, which has its own\\n'\n + `gated command. Use \\`${WP_PUSH_DEV}\\` (publishes a disposable copy; never moves your branch, never\\n`\n + `opens a PR). Do NOT use ${this.upsertPrCommand} for this — that lands on main. Full flow: READ ${docPath}.`)];\n }\n return [new V(1, truncate(ctx.command),\n `Manual push / direct PR is blocked — use the gated flow (${this.upsertPrCommand}). Full flow: READ ${docPath}.`)];\n }\n\n /**\n * Read the configured dev refs, falling back to the defaults when this repo has no `pr-gate` section\n * at all (a consumer that has not adopted the gate still gets a correct hint for the default names).\n *\n * Calling the loader here is safe by construction: an INVALID config blocks every Bash call long\n * before a rule runs, so reaching this line already means the config loaded.\n */\n private aimedAtDevEnvironment(ctx: BashContext): boolean {\n const devDeploy = loadAndValidate(ctx.governedRoot).prGate.devDeploy;\n return targetsDevEnvironment(\n ctx.commandCode,\n devDeploy.branchNamespace === '' ? DEFAULT_DEV_BRANCH_NAMESPACE : devDeploy.branchNamespace,\n devDeploy.devBranch === '' ? DEFAULT_DEV_BRANCH : devDeploy.devBranch);\n }\n}\n"]}
@@ -1,9 +1,9 @@
1
- import { PrMergeGuardConfig } from '@webpieces/rules-config';
1
+ import { PrLifecycleGuardConfig } from '@webpieces/rules-config';
2
2
  import type { BashContext, Violation } from '../types';
3
3
  import { BashRuleBase } from '../rule-base';
4
4
  import { FixHint } from '../fix-hint';
5
- export declare class PrMergeGuardRule extends BashRuleBase<PrMergeGuardConfig> {
6
- constructor(config: PrMergeGuardConfig);
5
+ export declare class PrMergeGuardRule extends BashRuleBase<PrLifecycleGuardConfig> {
6
+ constructor(config: PrLifecycleGuardConfig);
7
7
  readonly description = "Redirect a hand-rolled `gh pr merge` to `pnpm wp-land-pr`, then require branch cleanup.";
8
8
  private currentBranch;
9
9
  private treeKind;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PrMergeGuardRule = void 0;
4
4
  const child_process_1 = require("child_process");
5
+ const rules_config_1 = require("@webpieces/rules-config");
5
6
  const types_1 = require("../types");
6
7
  const rule_base_1 = require("../rule-base");
7
8
  const fix_hint_1 = require("../fix-hint");
@@ -11,7 +12,7 @@ function truncate(s) {
11
12
  return s.length <= MAX ? s : s.slice(0, MAX) + '…';
12
13
  }
13
14
  class PrMergeGuardRule extends rule_base_1.BashRuleBase {
14
- constructor(config) { super(config, 'pr-merge-guard'); }
15
+ constructor(config) { super(config, 'pr-merge-guard', rules_config_1.PR_LIFECYCLE_GUARD_KEY); }
15
16
  description = 'Redirect a hand-rolled `gh pr merge` to `pnpm wp-land-pr`, then require branch cleanup.';
16
17
  // Substituted with the real branch name in check(); the getter reads it. Placeholder until then.
17
18
  currentBranch = '<current-branch>';
@@ -1 +1 @@
1
- {"version":3,"file":"pr-merge-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/pr-merge-guard.ts"],"names":[],"mappings":";;;AAAA,iDAAyC;AAKzC,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAAsC;AACtC,mDAAyD;AAEzD,SAAS,QAAQ,CAAC,CAAS;IACvB,MAAM,GAAG,GAAG,GAAG,CAAC;IAChB,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;AACvD,CAAC;AAED,MAAa,gBAAiB,SAAQ,wBAAgC;IAClE,YAAY,MAA0B,IAAI,KAAK,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAEnE,WAAW,GAAG,yFAAyF,CAAC;IAEjH,iGAAiG;IACzF,aAAa,GAAG,kBAAkB,CAAC;IAE3C,2FAA2F;IAC3F,8FAA8F;IACtF,QAAQ,GAAa,SAAS,CAAC;IAC/B,YAAY,GAAG,gBAAgB,CAAC;IAEvB,QAAQ,GAAG,IAAI,4BAAY,EAAE,CAAC;IAE/C,6FAA6F;IAC7F,iEAAiE;IACjE,EAAE;IACF,8FAA8F;IAC9F,gGAAgG;IAChG,+FAA+F;IAC/F,yEAAyE;IACzE,IAAI,OAAO;QACP,OAAO,IAAI,kBAAO,CACd,wEAAwE,EACxE;YACI,0FAA0F;YAC1F,0FAA0F;YAC1F,yFAAyF;YACzF,0FAA0F;YAC1F,4FAA4F;YAC5F,EAAE;YACF,mBAAmB;YACnB,EAAE;YACF,iFAAiF;YACjF,yFAAyF;YACzF,wFAAwF;YACxF,0BAA0B;YAC1B,EAAE;SACL;aACI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;aACxF,MAAM,CAAC,CAAC,EAAE,EAAE,yEAAyE,CAAC,CAAC;aACvF,IAAI,CAAC,IAAI,CAAC,CAClB,CAAC;IACN,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,KAAK,CAAC,GAAgB;QAClB,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC;YAAE,OAAO,EAAE,CAAC;QAExD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACxD,IAAI,CAAC,aAAa,GAAG,IAAA,wBAAQ,EAAC,iCAAiC,EAAE;YAC7D,GAAG,EAAE,GAAG,CAAC,aAAa;YACtB,QAAQ,EAAE,MAAM;SACnB,CAAC,CAAC,IAAI,EAAE,CAAC;QACV,IAAI,CAAC,YAAY,GAAG,GAAG,CAAC,aAAa,CAAC;QAEtC,OAAO,CAAC,IAAI,iBAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC;CACJ;AAhFD,4CAgFC","sourcesContent":["import { execSync } from 'child_process';\n\nimport { PrMergeGuardConfig } from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\nimport { FixHint } from '../fix-hint';\nimport { TreeRecovery, TreeKind } from './tree-recovery';\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\nexport class PrMergeGuardRule extends BashRuleBase<PrMergeGuardConfig> {\n constructor(config: PrMergeGuardConfig) { super(config, 'pr-merge-guard'); }\n\n readonly description = 'Redirect a hand-rolled `gh pr merge` to `pnpm wp-land-pr`, then require branch cleanup.';\n\n // Substituted with the real branch name in check(); the getter reads it. Placeholder until then.\n private currentBranch = '<current-branch>';\n\n // The tree we are standing in, resolved in check() so fixHint renders the ONE cleanup that\n // actually works here. 'unknown' until check() runs (fixHint is also read before/without it).\n private treeKind: TreeKind = 'unknown';\n private worktreePath = '<worktree-dir>';\n\n private readonly recovery = new TreeRecovery();\n\n // Single fix, no distinct options — the whole guidance lives in mainMessage so it renders as\n // one coherent block (never split into fake \"Fix Option 1/2/3\").\n //\n // `pnpm wp-cleanup` rather than `git branch -d <branch>`: a raw `-d` reads as destructive, so\n // agents ask permission and stop, and the branch survives. wp-cleanup is one named command that\n // deletes ONLY provably-dead branches — and it reaps every OTHER dead branch at the same time,\n // which is the moment that actually keeps the local branch list bounded.\n get fixHint(): FixHint {\n return new FixHint(\n 'Land the PR with `pnpm wp-land-pr`, never a hand-rolled `gh pr merge`.',\n [\n 'A bare `gh pr merge` leaves the commit message to the repo\\'s squash_merge_commit_title/',\n 'message settings. On a repo set to PR_TITLE + PR_BODY that now happens to be RIGHT — the',\n 'PR description IS the gated commit body — but on any other combination it is wrong, and',\n 'commonly means the internal \"Squash merge of <branch>\" subject. `pnpm wp-land-pr` passes',\n '`--subject`/`--body-file` explicitly, so it is right on EVERY repo regardless of settings:',\n '',\n ' pnpm wp-land-pr',\n '',\n 'It squash-merges when the PR is mergeable, and enables auto-merge with the same',\n 'subject/body when the checks are still running. It also does the two things a raw merge',\n 'never does: archive the pre-squash tip as archive/<date>/<branch>, and reap the landed',\n 'worktree. Then clean up:',\n '',\n ]\n .concat(this.recovery.cleanupSteps(this.treeKind, this.currentBranch, this.worktreePath))\n .concat(['', \"Add this to your memory so you don't forget next time and waste tokens.\"])\n .join('\\n'),\n );\n }\n\n /**\n * Any hand-rolled `gh pr merge` is blocked outright — there is no \"but I cleaned up afterwards\"\n * escape, because the damage is the commit message that merge writes into main, which cleanup\n * cannot undo. `pnpm wp-land-pr` is the way through; its own `gh pr merge` runs as a child\n * process this hook never sees, exactly like the other gated commands.\n *\n * ─── Why a CORRECT-LOOKING `gh pr merge --auto --subject --body-file` is still blocked ──────────\n * Full reasoning, and what was rejected: `decisions/0005-the-pr-description-is-the-merge-body.md`\n * § \"pr-merge-guard stays blocking\" (which supersedes `0004` § 8). It has now been re-examined\n * twice — once when the gated body moved out of the worktree, and again when it moved onto the PR\n * itself — and the block stands. A `--body-file` proves a file was passed, never that it holds the\n * GATED bytes: the guard sees a command string and cannot read the file, so any file at all — a\n * hand-written one, an older run's — could land as the reviewed body with nothing to distinguish it\n * afterwards.\n *\n * One more thing depends on that: landing DECIDES whether the archive/merge-info/reap bookkeeping\n * belongs to this tree, by comparing this tree's branch tip against the PR's `headRefOid`. A\n * hand-rolled merge skips that decision silently, which is how a landed worktree becomes a corpse.\n *\n * The tree kind still decides which cleanup steps the hint prints: in a linked worktree\n * `git checkout main` FATALS, so demanding it there would be demanding an impossible command.\n */\n check(ctx: BashContext): readonly Violation[] {\n if (!/gh\\s+pr\\s+merge/.test(ctx.commandCode)) return [];\n\n this.treeKind = this.recovery.kindOf(ctx.workspaceRoot);\n this.currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n }).trim();\n this.worktreePath = ctx.workspaceRoot;\n\n return [new V(1, truncate(ctx.command))];\n }\n}\n"]}
1
+ {"version":3,"file":"pr-merge-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/pr-merge-guard.ts"],"names":[],"mappings":";;;AAAA,iDAAyC;AAEzC,0DAAyF;AAGzF,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAAsC;AACtC,mDAAyD;AAEzD,SAAS,QAAQ,CAAC,CAAS;IACvB,MAAM,GAAG,GAAG,GAAG,CAAC;IAChB,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;AACvD,CAAC;AAED,MAAa,gBAAiB,SAAQ,wBAAoC;IACtE,YAAY,MAA8B,IAAI,KAAK,CAAC,MAAM,EAAE,gBAAgB,EAAE,qCAAsB,CAAC,CAAC,CAAC,CAAC;IAE/F,WAAW,GAAG,yFAAyF,CAAC;IAEjH,iGAAiG;IACzF,aAAa,GAAG,kBAAkB,CAAC;IAE3C,2FAA2F;IAC3F,8FAA8F;IACtF,QAAQ,GAAa,SAAS,CAAC;IAC/B,YAAY,GAAG,gBAAgB,CAAC;IAEvB,QAAQ,GAAG,IAAI,4BAAY,EAAE,CAAC;IAE/C,6FAA6F;IAC7F,iEAAiE;IACjE,EAAE;IACF,8FAA8F;IAC9F,gGAAgG;IAChG,+FAA+F;IAC/F,yEAAyE;IACzE,IAAI,OAAO;QACP,OAAO,IAAI,kBAAO,CACd,wEAAwE,EACxE;YACI,0FAA0F;YAC1F,0FAA0F;YAC1F,yFAAyF;YACzF,0FAA0F;YAC1F,4FAA4F;YAC5F,EAAE;YACF,mBAAmB;YACnB,EAAE;YACF,iFAAiF;YACjF,yFAAyF;YACzF,wFAAwF;YACxF,0BAA0B;YAC1B,EAAE;SACL;aACI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;aACxF,MAAM,CAAC,CAAC,EAAE,EAAE,yEAAyE,CAAC,CAAC;aACvF,IAAI,CAAC,IAAI,CAAC,CAClB,CAAC;IACN,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,KAAK,CAAC,GAAgB;QAClB,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC;YAAE,OAAO,EAAE,CAAC;QAExD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACxD,IAAI,CAAC,aAAa,GAAG,IAAA,wBAAQ,EAAC,iCAAiC,EAAE;YAC7D,GAAG,EAAE,GAAG,CAAC,aAAa;YACtB,QAAQ,EAAE,MAAM;SACnB,CAAC,CAAC,IAAI,EAAE,CAAC;QACV,IAAI,CAAC,YAAY,GAAG,GAAG,CAAC,aAAa,CAAC;QAEtC,OAAO,CAAC,IAAI,iBAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC;CACJ;AAhFD,4CAgFC","sourcesContent":["import { execSync } from 'child_process';\n\nimport { PrLifecycleGuardConfig, PR_LIFECYCLE_GUARD_KEY } from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\nimport { FixHint } from '../fix-hint';\nimport { TreeRecovery, TreeKind } from './tree-recovery';\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\nexport class PrMergeGuardRule extends BashRuleBase<PrLifecycleGuardConfig> {\n constructor(config: PrLifecycleGuardConfig) { super(config, 'pr-merge-guard', PR_LIFECYCLE_GUARD_KEY); }\n\n readonly description = 'Redirect a hand-rolled `gh pr merge` to `pnpm wp-land-pr`, then require branch cleanup.';\n\n // Substituted with the real branch name in check(); the getter reads it. Placeholder until then.\n private currentBranch = '<current-branch>';\n\n // The tree we are standing in, resolved in check() so fixHint renders the ONE cleanup that\n // actually works here. 'unknown' until check() runs (fixHint is also read before/without it).\n private treeKind: TreeKind = 'unknown';\n private worktreePath = '<worktree-dir>';\n\n private readonly recovery = new TreeRecovery();\n\n // Single fix, no distinct options — the whole guidance lives in mainMessage so it renders as\n // one coherent block (never split into fake \"Fix Option 1/2/3\").\n //\n // `pnpm wp-cleanup` rather than `git branch -d <branch>`: a raw `-d` reads as destructive, so\n // agents ask permission and stop, and the branch survives. wp-cleanup is one named command that\n // deletes ONLY provably-dead branches — and it reaps every OTHER dead branch at the same time,\n // which is the moment that actually keeps the local branch list bounded.\n get fixHint(): FixHint {\n return new FixHint(\n 'Land the PR with `pnpm wp-land-pr`, never a hand-rolled `gh pr merge`.',\n [\n 'A bare `gh pr merge` leaves the commit message to the repo\\'s squash_merge_commit_title/',\n 'message settings. On a repo set to PR_TITLE + PR_BODY that now happens to be RIGHT — the',\n 'PR description IS the gated commit body — but on any other combination it is wrong, and',\n 'commonly means the internal \"Squash merge of <branch>\" subject. `pnpm wp-land-pr` passes',\n '`--subject`/`--body-file` explicitly, so it is right on EVERY repo regardless of settings:',\n '',\n ' pnpm wp-land-pr',\n '',\n 'It squash-merges when the PR is mergeable, and enables auto-merge with the same',\n 'subject/body when the checks are still running. It also does the two things a raw merge',\n 'never does: archive the pre-squash tip as archive/<date>/<branch>, and reap the landed',\n 'worktree. Then clean up:',\n '',\n ]\n .concat(this.recovery.cleanupSteps(this.treeKind, this.currentBranch, this.worktreePath))\n .concat(['', \"Add this to your memory so you don't forget next time and waste tokens.\"])\n .join('\\n'),\n );\n }\n\n /**\n * Any hand-rolled `gh pr merge` is blocked outright — there is no \"but I cleaned up afterwards\"\n * escape, because the damage is the commit message that merge writes into main, which cleanup\n * cannot undo. `pnpm wp-land-pr` is the way through; its own `gh pr merge` runs as a child\n * process this hook never sees, exactly like the other gated commands.\n *\n * ─── Why a CORRECT-LOOKING `gh pr merge --auto --subject --body-file` is still blocked ──────────\n * Full reasoning, and what was rejected: `decisions/0005-the-pr-description-is-the-merge-body.md`\n * § \"pr-merge-guard stays blocking\" (which supersedes `0004` § 8). It has now been re-examined\n * twice — once when the gated body moved out of the worktree, and again when it moved onto the PR\n * itself — and the block stands. A `--body-file` proves a file was passed, never that it holds the\n * GATED bytes: the guard sees a command string and cannot read the file, so any file at all — a\n * hand-written one, an older run's — could land as the reviewed body with nothing to distinguish it\n * afterwards.\n *\n * One more thing depends on that: landing DECIDES whether the archive/merge-info/reap bookkeeping\n * belongs to this tree, by comparing this tree's branch tip against the PR's `headRefOid`. A\n * hand-rolled merge skips that decision silently, which is how a landed worktree becomes a corpse.\n *\n * The tree kind still decides which cleanup steps the hint prints: in a linked worktree\n * `git checkout main` FATALS, so demanding it there would be demanding an impossible command.\n */\n check(ctx: BashContext): readonly Violation[] {\n if (!/gh\\s+pr\\s+merge/.test(ctx.commandCode)) return [];\n\n this.treeKind = this.recovery.kindOf(ctx.workspaceRoot);\n this.currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n }).trim();\n this.worktreePath = ctx.workspaceRoot;\n\n return [new V(1, truncate(ctx.command))];\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { ReadStaleGuardConfig } from '@webpieces/rules-config';
1
+ import { BranchStateGuardConfig } from '@webpieces/rules-config';
2
2
  import type { FileContext, Violation } from '../types';
3
3
  import { FileRuleBase } from '../rule-base';
4
4
  import { FixHint } from '../fix-hint';
@@ -56,8 +56,8 @@ import { FixHint } from '../fix-hint';
56
56
  * never reaches the runner's rule loop). Fires the detached refresher on every call, which is also
57
57
  * what makes reads keep the shared main-sync cache warm for feature-branch-guard.
58
58
  */
59
- export declare class ReadStaleGuardRule extends FileRuleBase<ReadStaleGuardConfig> {
60
- constructor(config: ReadStaleGuardConfig);
59
+ export declare class ReadStaleGuardRule extends FileRuleBase<BranchStateGuardConfig> {
60
+ constructor(config: BranchStateGuardConfig);
61
61
  readonly description = "Block reads on a branch that is stale to read from \u2014 a `main` behind origin/main, or a feature branch whose PR is already merged.";
62
62
  readonly files: string[];
63
63
  readonly defaultOptions: {
@@ -11,6 +11,7 @@ const rule_base_1 = require("../rule-base");
11
11
  const fix_hint_1 = require("../fix-hint");
12
12
  const to_error_1 = require("../to-error");
13
13
  const main_sync_refresh_1 = require("../main-sync-refresh");
14
+ const main_sync_timeout_1 = require("../main-sync-timeout");
14
15
  const decision_log_1 = require("../decision-log");
15
16
  const l0_fault_codes_1 = require("../l0-fault-codes");
16
17
  const merged_branch_message_1 = require("./merged-branch-message");
@@ -71,7 +72,7 @@ const tree_recovery_1 = require("./tree-recovery");
71
72
  * what makes reads keep the shared main-sync cache warm for feature-branch-guard.
72
73
  */
73
74
  class ReadStaleGuardRule extends rule_base_1.FileRuleBase {
74
- constructor(config) { super(config, 'read-stale-guard'); }
75
+ constructor(config) { super(config, 'read-stale-guard', rules_config_1.BRANCH_STATE_GUARD_KEY); }
75
76
  description = 'Block reads on a branch that is stale to read from — a `main` behind origin/main, or a feature branch whose PR is already merged.';
76
77
  files = ['**/*'];
77
78
  defaultOptions = {
@@ -81,7 +82,7 @@ class ReadStaleGuardRule extends rule_base_1.FileRuleBase {
81
82
  new fix_hint_1.Option('On main, behind origin/main → git pull origin main. On an already-merged branch → git fetch origin main && git checkout -b <new-branch> origin/main. Then retry the read.', true),
82
83
  new fix_hint_1.Option("If that pull dies with 'fatal: Cannot fast-forward to multiple branches', .git/FETCH_HEAD holds a duplicate line — run 'git fetch --prune origin main' to rewrite it cleanly, then retry the pull."),
83
84
  new fix_hint_1.Option('Still allowed right now: Bash that does not read repo files (installs, upgrades, builds, tests, the pull itself, git/gh metadata), all Write/Edit, and reading webpieces.config.json. Content-reading Bash (cat/grep/ls/…) is blocked too on a stale main — see stale-main-bash-guard.'),
84
- new fix_hint_1.Option('Disable in webpieces.config.json under hookGuards → read-stale-guard (mode OFF) if intentional.'),
85
+ new fix_hint_1.Option('Disable in webpieces.config.json under hookGuards → branch-state-guard (mode OFF) if intentional — that one key governs the Write, Read and Bash halves of this policy together.'),
85
86
  ]);
86
87
  check(ctx) {
87
88
  // Outside the workspace root — no jurisdiction.
@@ -92,7 +93,7 @@ class ReadStaleGuardRule extends rule_base_1.FileRuleBase {
92
93
  return this.failOpen(ctx, branch, 'branch-undeterminable');
93
94
  // Keep the shared cache warm for the next call. Detached; never blocks this read. Fired for
94
95
  // BOTH states — the merged-branch signal comes out of that same cache.
95
- (0, main_sync_refresh_1.triggerMainSyncRefresh)(ctx.workspaceRoot, this.config.hangTimeoutMinutes ?? rules_config_1.DEFAULT_HANG_TIMEOUT_MINUTES);
96
+ (0, main_sync_refresh_1.triggerMainSyncRefresh)(ctx.workspaceRoot, (0, main_sync_timeout_1.hangTimeoutOf)(this.config));
96
97
  // Escape valve 3 — the read half of the config escape hatch. Ahead of BOTH states' blocks so
97
98
  // the agent can always read-then-edit the file that turns this guard off.
98
99
  if (this.isConfigFile(ctx.relativePath))
@@ -152,8 +153,16 @@ class ReadStaleGuardRule extends rule_base_1.FileRuleBase {
152
153
  // not seen yet is the `status === null` case above — still fail-open.)
153
154
  if (status.branch !== branch)
154
155
  return this.failOpen(ctx, branch, 'stale-cross-branch-cache', cache);
155
- if (!status.branchAlreadyMerged)
156
- return this.allow(ctx, branch, 'clean-feature-branch', cache);
156
+ // NOT-MERGED, or NOT-ASKED? `branchAlreadyMerged: false` is produced both by "this branch has
157
+ // no merged PR" and by "the forge could not be reached" (`gh` missing, unauthenticated,
158
+ // rate-limited, offline). Same allow either way — never block on data you could not establish
159
+ // — but the LOG must not call the second one an approval, or the trail cannot tell a policy
160
+ // that is protecting something from one that is quietly standing down.
161
+ if (!status.branchAlreadyMerged) {
162
+ return status.forgeReachable
163
+ ? this.allow(ctx, branch, 'clean-feature-branch', cache)
164
+ : this.failOpen(ctx, branch, 'no-forge', cache);
165
+ }
157
166
  if (this.isDirty(ctx.workspaceRoot)) {
158
167
  return this.failOpen(ctx, branch, 'dirty-merged-branch', cache);
159
168
  }
@@ -256,7 +265,7 @@ class ReadStaleGuardRule extends rule_base_1.FileRuleBase {
256
265
  return [new types_1.Violation(1, ctx.relativePath, message)];
257
266
  }
258
267
  logDecision(ctx, branch, verdict, reason, cache) {
259
- (0, decision_log_1.logGuardDecision)(ctx.workspaceRoot, new decision_log_1.GuardDecision('read-stale-guard', ctx.tool, ctx.relativePath, branch ?? 'unknown', verdict, reason, cache, l0_fault_codes_1.L0_FAULT_NONE, decision_log_1.MATRIX_L2));
268
+ (0, decision_log_1.logGuardDecision)(ctx.workspaceRoot, new decision_log_1.GuardDecision('read-stale-guard', ctx.tool, ctx.relativePath, branch ?? 'unknown', verdict, reason, cache, l0_fault_codes_1.L0_FAULT_NONE, (0, decision_log_1.matrixL2Row)(reason)));
260
269
  }
261
270
  /**
262
271
  * The current branch, WITHOUT spawning git on the common path.