@sentry/warden 0.0.0

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 (199) hide show
  1. package/.agents/skills/find-bugs/SKILL.md +75 -0
  2. package/.agents/skills/vercel-react-best-practices/AGENTS.md +2934 -0
  3. package/.agents/skills/vercel-react-best-practices/SKILL.md +136 -0
  4. package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  5. package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
  6. package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
  7. package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  8. package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
  9. package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
  10. package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  11. package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  12. package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
  13. package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  14. package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  15. package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  16. package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  17. package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  18. package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  19. package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  20. package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  21. package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
  22. package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  23. package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  24. package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  25. package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  26. package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  27. package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  28. package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  29. package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  30. package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  31. package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  32. package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  33. package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  34. package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  35. package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  36. package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  37. package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  38. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  39. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  40. package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  41. package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  42. package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  43. package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  44. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  45. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  46. package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  47. package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  48. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  49. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  50. package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  51. package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  52. package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  53. package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  54. package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  55. package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
  56. package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  57. package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  58. package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
  59. package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  60. package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  61. package/.claude/settings.json +57 -0
  62. package/.claude/settings.local.json +88 -0
  63. package/.claude/skills/agent-prompt/SKILL.md +54 -0
  64. package/.claude/skills/agent-prompt/references/agentic-patterns.md +94 -0
  65. package/.claude/skills/agent-prompt/references/anti-patterns.md +140 -0
  66. package/.claude/skills/agent-prompt/references/context-design.md +124 -0
  67. package/.claude/skills/agent-prompt/references/core-principles.md +75 -0
  68. package/.claude/skills/agent-prompt/references/model-guidance.md +118 -0
  69. package/.claude/skills/agent-prompt/references/output-formats.md +98 -0
  70. package/.claude/skills/agent-prompt/references/skill-structure.md +115 -0
  71. package/.claude/skills/agent-prompt/references/system-prompts.md +115 -0
  72. package/.claude/skills/notseer/SKILL.md +131 -0
  73. package/.claude/skills/skill-writer/SKILL.md +140 -0
  74. package/.claude/skills/testing-guidelines/SKILL.md +132 -0
  75. package/.claude/skills/warden-skill/SKILL.md +250 -0
  76. package/.claude/skills/warden-skill/references/config-schema.md +133 -0
  77. package/.dex/config.toml +2 -0
  78. package/.github/workflows/ci.yml +33 -0
  79. package/.github/workflows/release.yml +54 -0
  80. package/.github/workflows/warden.yml +40 -0
  81. package/AGENTS.md +89 -0
  82. package/CONTRIBUTING.md +60 -0
  83. package/LICENSE +105 -0
  84. package/README.md +43 -0
  85. package/SPEC.md +263 -0
  86. package/action.yml +87 -0
  87. package/assets/favicon.png +0 -0
  88. package/assets/warden-icon-bw.svg +5 -0
  89. package/assets/warden-icon-purple.png +0 -0
  90. package/assets/warden-icon-purple.svg +5 -0
  91. package/docs/.claude/settings.local.json +11 -0
  92. package/docs/astro.config.mjs +43 -0
  93. package/docs/package.json +19 -0
  94. package/docs/pnpm-lock.yaml +4000 -0
  95. package/docs/public/favicon.svg +5 -0
  96. package/docs/src/components/Code.astro +141 -0
  97. package/docs/src/components/PackageManagerTabs.astro +183 -0
  98. package/docs/src/components/Terminal.astro +212 -0
  99. package/docs/src/layouts/Base.astro +380 -0
  100. package/docs/src/pages/cli.astro +167 -0
  101. package/docs/src/pages/config.astro +394 -0
  102. package/docs/src/pages/guide.astro +449 -0
  103. package/docs/src/pages/index.astro +490 -0
  104. package/docs/src/styles/global.css +551 -0
  105. package/docs/tsconfig.json +3 -0
  106. package/docs/vercel.json +5 -0
  107. package/eslint.config.js +33 -0
  108. package/package.json +73 -0
  109. package/src/action/index.ts +1 -0
  110. package/src/action/main.ts +868 -0
  111. package/src/cli/args.test.ts +477 -0
  112. package/src/cli/args.ts +415 -0
  113. package/src/cli/commands/add.ts +447 -0
  114. package/src/cli/commands/init.test.ts +136 -0
  115. package/src/cli/commands/init.ts +132 -0
  116. package/src/cli/commands/setup-app/browser.ts +38 -0
  117. package/src/cli/commands/setup-app/credentials.ts +45 -0
  118. package/src/cli/commands/setup-app/manifest.ts +48 -0
  119. package/src/cli/commands/setup-app/server.ts +172 -0
  120. package/src/cli/commands/setup-app.ts +156 -0
  121. package/src/cli/commands/sync.ts +114 -0
  122. package/src/cli/context.ts +131 -0
  123. package/src/cli/files.test.ts +155 -0
  124. package/src/cli/files.ts +89 -0
  125. package/src/cli/fix.test.ts +310 -0
  126. package/src/cli/fix.ts +387 -0
  127. package/src/cli/git.test.ts +119 -0
  128. package/src/cli/git.ts +318 -0
  129. package/src/cli/index.ts +14 -0
  130. package/src/cli/main.ts +672 -0
  131. package/src/cli/output/box.ts +235 -0
  132. package/src/cli/output/formatters.test.ts +187 -0
  133. package/src/cli/output/formatters.ts +269 -0
  134. package/src/cli/output/icons.ts +13 -0
  135. package/src/cli/output/index.ts +44 -0
  136. package/src/cli/output/ink-runner.tsx +337 -0
  137. package/src/cli/output/jsonl.test.ts +347 -0
  138. package/src/cli/output/jsonl.ts +126 -0
  139. package/src/cli/output/reporter.ts +435 -0
  140. package/src/cli/output/tasks.ts +374 -0
  141. package/src/cli/output/tty.test.ts +117 -0
  142. package/src/cli/output/tty.ts +60 -0
  143. package/src/cli/output/verbosity.test.ts +40 -0
  144. package/src/cli/output/verbosity.ts +31 -0
  145. package/src/cli/terminal.test.ts +148 -0
  146. package/src/cli/terminal.ts +301 -0
  147. package/src/config/index.ts +3 -0
  148. package/src/config/loader.test.ts +313 -0
  149. package/src/config/loader.ts +103 -0
  150. package/src/config/schema.ts +168 -0
  151. package/src/config/writer.test.ts +119 -0
  152. package/src/config/writer.ts +84 -0
  153. package/src/diff/classify.test.ts +162 -0
  154. package/src/diff/classify.ts +92 -0
  155. package/src/diff/coalesce.test.ts +208 -0
  156. package/src/diff/coalesce.ts +133 -0
  157. package/src/diff/context.test.ts +226 -0
  158. package/src/diff/context.ts +201 -0
  159. package/src/diff/index.ts +4 -0
  160. package/src/diff/parser.test.ts +212 -0
  161. package/src/diff/parser.ts +149 -0
  162. package/src/event/context.ts +132 -0
  163. package/src/event/index.ts +2 -0
  164. package/src/event/schedule-context.ts +101 -0
  165. package/src/examples/examples.integration.test.ts +66 -0
  166. package/src/examples/index.test.ts +101 -0
  167. package/src/examples/index.ts +122 -0
  168. package/src/examples/setup.ts +25 -0
  169. package/src/index.ts +115 -0
  170. package/src/output/dedup.test.ts +419 -0
  171. package/src/output/dedup.ts +607 -0
  172. package/src/output/github-checks.test.ts +300 -0
  173. package/src/output/github-checks.ts +476 -0
  174. package/src/output/github-issues.ts +329 -0
  175. package/src/output/index.ts +5 -0
  176. package/src/output/issue-renderer.ts +197 -0
  177. package/src/output/renderer.test.ts +727 -0
  178. package/src/output/renderer.ts +217 -0
  179. package/src/output/stale.test.ts +375 -0
  180. package/src/output/stale.ts +155 -0
  181. package/src/output/types.ts +34 -0
  182. package/src/sdk/index.ts +1 -0
  183. package/src/sdk/runner.test.ts +806 -0
  184. package/src/sdk/runner.ts +1232 -0
  185. package/src/skills/index.ts +36 -0
  186. package/src/skills/loader.test.ts +300 -0
  187. package/src/skills/loader.ts +423 -0
  188. package/src/skills/remote.test.ts +704 -0
  189. package/src/skills/remote.ts +604 -0
  190. package/src/triggers/matcher.test.ts +277 -0
  191. package/src/triggers/matcher.ts +152 -0
  192. package/src/types/index.ts +194 -0
  193. package/src/utils/async.ts +18 -0
  194. package/src/utils/index.test.ts +84 -0
  195. package/src/utils/index.ts +50 -0
  196. package/tsconfig.json +25 -0
  197. package/vitest.config.ts +8 -0
  198. package/vitest.integration.config.ts +11 -0
  199. package/warden.toml +19 -0
@@ -0,0 +1,277 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import {
3
+ matchGlob,
4
+ matchTrigger,
5
+ shouldFail,
6
+ countFindingsAtOrAbove,
7
+ countSeverity,
8
+ clearGlobCache,
9
+ getGlobCacheSize,
10
+ } from './matcher.js';
11
+ import type { Trigger } from '../config/schema.js';
12
+ import { SEVERITY_ORDER } from '../types/index.js';
13
+ import type { EventContext, SkillReport } from '../types/index.js';
14
+
15
+ /** Test helper to create a SkillReport with given severities */
16
+ function makeReport(severities: string[]): SkillReport {
17
+ return {
18
+ skill: 'test',
19
+ summary: 'Test report',
20
+ findings: severities.map((s, i) => ({
21
+ id: `finding-${i}`,
22
+ severity: s as 'critical' | 'high' | 'medium' | 'low' | 'info',
23
+ title: `Finding ${i}`,
24
+ description: 'Test finding',
25
+ })),
26
+ };
27
+ }
28
+
29
+ describe('matchGlob', () => {
30
+ beforeEach(() => {
31
+ clearGlobCache();
32
+ });
33
+
34
+ it('matches exact paths', () => {
35
+ expect(matchGlob('src/index.ts', 'src/index.ts')).toBe(true);
36
+ expect(matchGlob('src/index.ts', 'src/other.ts')).toBe(false);
37
+ });
38
+
39
+ it('matches single wildcard', () => {
40
+ expect(matchGlob('src/*.ts', 'src/index.ts')).toBe(true);
41
+ expect(matchGlob('src/*.ts', 'src/foo/index.ts')).toBe(false);
42
+ expect(matchGlob('*.ts', 'index.ts')).toBe(true);
43
+ });
44
+
45
+ it('matches double wildcard (globstar)', () => {
46
+ expect(matchGlob('src/**/*.ts', 'src/index.ts')).toBe(true);
47
+ expect(matchGlob('src/**/*.ts', 'src/foo/index.ts')).toBe(true);
48
+ expect(matchGlob('src/**/*.ts', 'src/foo/bar/index.ts')).toBe(true);
49
+ expect(matchGlob('**/*.ts', 'src/index.ts')).toBe(true);
50
+ });
51
+
52
+ it('matches question mark wildcard', () => {
53
+ expect(matchGlob('src/?.ts', 'src/a.ts')).toBe(true);
54
+ expect(matchGlob('src/?.ts', 'src/ab.ts')).toBe(false);
55
+ });
56
+
57
+ it('caches compiled patterns', () => {
58
+ matchGlob('src/*.ts', 'src/index.ts');
59
+ expect(getGlobCacheSize()).toBe(1);
60
+
61
+ // Same pattern should not increase cache size
62
+ matchGlob('src/*.ts', 'src/other.ts');
63
+ expect(getGlobCacheSize()).toBe(1);
64
+
65
+ // Different pattern should increase cache size
66
+ matchGlob('lib/*.js', 'lib/index.js');
67
+ expect(getGlobCacheSize()).toBe(2);
68
+ });
69
+
70
+ it('evicts oldest entry when cache exceeds max size', () => {
71
+ // Fill cache with 1000 patterns
72
+ for (let i = 0; i < 1000; i++) {
73
+ matchGlob(`pattern${i}/*.ts`, `pattern${i}/file.ts`);
74
+ }
75
+ expect(getGlobCacheSize()).toBe(1000);
76
+
77
+ // Adding one more should evict the oldest
78
+ matchGlob('newpattern/*.ts', 'newpattern/file.ts');
79
+ expect(getGlobCacheSize()).toBe(1000);
80
+
81
+ // The first pattern should be evicted (cache miss will re-add it)
82
+ // We can verify this by checking the cache size doesn't increase
83
+ // when we add it back
84
+ const sizeBefore = getGlobCacheSize();
85
+ matchGlob('pattern0/*.ts', 'pattern0/file.ts');
86
+ expect(getGlobCacheSize()).toBe(sizeBefore);
87
+ });
88
+
89
+ it('maintains LRU order by refreshing accessed entries', () => {
90
+ // Add patterns 0, 1, 2
91
+ matchGlob('pattern0/*.ts', 'pattern0/file.ts');
92
+ matchGlob('pattern1/*.ts', 'pattern1/file.ts');
93
+ matchGlob('pattern2/*.ts', 'pattern2/file.ts');
94
+
95
+ // Access pattern0 to make it most recently used
96
+ matchGlob('pattern0/*.ts', 'pattern0/file.ts');
97
+
98
+ // Fill cache to max (997 more patterns needed to reach 1000)
99
+ for (let i = 3; i < 1000; i++) {
100
+ matchGlob(`pattern${i}/*.ts`, `pattern${i}/file.ts`);
101
+ }
102
+ expect(getGlobCacheSize()).toBe(1000);
103
+
104
+ // Add one more - should evict pattern1 (oldest not-accessed)
105
+ matchGlob('newpattern/*.ts', 'newpattern/file.ts');
106
+ expect(getGlobCacheSize()).toBe(1000);
107
+ });
108
+ });
109
+
110
+ describe('matchTrigger', () => {
111
+ const baseContext: EventContext = {
112
+ eventType: 'pull_request',
113
+ action: 'opened',
114
+ repository: {
115
+ owner: 'test',
116
+ name: 'repo',
117
+ fullName: 'test/repo',
118
+ defaultBranch: 'main',
119
+ },
120
+ pullRequest: {
121
+ number: 1,
122
+ title: 'Test PR',
123
+ body: 'Test body',
124
+ author: 'user',
125
+ baseBranch: 'main',
126
+ headBranch: 'feature',
127
+ headSha: 'abc123',
128
+ files: [
129
+ { filename: 'src/index.ts', status: 'modified', additions: 10, deletions: 5 },
130
+ { filename: 'README.md', status: 'modified', additions: 2, deletions: 0 },
131
+ ],
132
+ },
133
+ repoPath: '/test/repo',
134
+ };
135
+
136
+ const baseTrigger: Trigger = {
137
+ name: 'test-trigger',
138
+ event: 'pull_request',
139
+ actions: ['opened', 'synchronize'],
140
+ skill: 'test-skill',
141
+ };
142
+
143
+ it('matches when event and action match', () => {
144
+ expect(matchTrigger(baseTrigger, baseContext)).toBe(true);
145
+ });
146
+
147
+ it('does not match wrong event type', () => {
148
+ const trigger = { ...baseTrigger, event: 'issues' as const };
149
+ expect(matchTrigger(trigger, baseContext)).toBe(false);
150
+ });
151
+
152
+ it('does not match wrong action', () => {
153
+ const trigger = { ...baseTrigger, actions: ['closed'] };
154
+ expect(matchTrigger(trigger, baseContext)).toBe(false);
155
+ });
156
+
157
+ it('matches with path filter', () => {
158
+ const trigger = { ...baseTrigger, filters: { paths: ['src/**/*.ts'] } };
159
+ expect(matchTrigger(trigger, baseContext)).toBe(true);
160
+ });
161
+
162
+ it('does not match when no files match path filter', () => {
163
+ const trigger = { ...baseTrigger, filters: { paths: ['lib/**/*.ts'] } };
164
+ expect(matchTrigger(trigger, baseContext)).toBe(false);
165
+ });
166
+
167
+ it('ignores files matching ignorePaths', () => {
168
+ const context = {
169
+ ...baseContext,
170
+ pullRequest: {
171
+ ...baseContext.pullRequest!,
172
+ files: [{ filename: 'README.md', status: 'modified' as const, additions: 1, deletions: 0 }],
173
+ },
174
+ };
175
+ const trigger = { ...baseTrigger, filters: { ignorePaths: ['*.md'] } };
176
+ expect(matchTrigger(trigger, context)).toBe(false);
177
+ });
178
+
179
+ it('fails when path filters defined but filenames undefined', () => {
180
+ const context = {
181
+ ...baseContext,
182
+ pullRequest: undefined,
183
+ };
184
+ const trigger = { ...baseTrigger, filters: { paths: ['src/**/*.ts'] } };
185
+ expect(matchTrigger(trigger, context)).toBe(false);
186
+ });
187
+
188
+ it('fails when ignorePaths defined but filenames undefined', () => {
189
+ const context = {
190
+ ...baseContext,
191
+ pullRequest: undefined,
192
+ };
193
+ const trigger = { ...baseTrigger, filters: { ignorePaths: ['*.md'] } };
194
+ expect(matchTrigger(trigger, context)).toBe(false);
195
+ });
196
+
197
+ it('fails when path filters defined but files array empty', () => {
198
+ const context = {
199
+ ...baseContext,
200
+ pullRequest: {
201
+ ...baseContext.pullRequest!,
202
+ files: [],
203
+ },
204
+ };
205
+ const trigger = { ...baseTrigger, filters: { paths: ['src/**/*.ts'] } };
206
+ expect(matchTrigger(trigger, context)).toBe(false);
207
+ });
208
+
209
+ it('matches when no filters defined and filenames unavailable', () => {
210
+ const context = {
211
+ ...baseContext,
212
+ pullRequest: undefined,
213
+ };
214
+ expect(matchTrigger(baseTrigger, context)).toBe(true);
215
+ });
216
+ });
217
+
218
+ describe('shouldFail', () => {
219
+ it('returns true when findings meet threshold', () => {
220
+ expect(shouldFail(makeReport(['high']), 'high')).toBe(true);
221
+ expect(shouldFail(makeReport(['critical']), 'high')).toBe(true);
222
+ expect(shouldFail(makeReport(['medium']), 'medium')).toBe(true);
223
+ });
224
+
225
+ it('returns false when findings below threshold', () => {
226
+ expect(shouldFail(makeReport(['low']), 'high')).toBe(false);
227
+ expect(shouldFail(makeReport(['info']), 'medium')).toBe(false);
228
+ expect(shouldFail(makeReport([]), 'info')).toBe(false);
229
+ });
230
+ });
231
+
232
+ describe('countFindingsAtOrAbove', () => {
233
+ it('counts findings at or above threshold', () => {
234
+ const report = makeReport(['critical', 'high', 'medium', 'low', 'info']);
235
+ expect(countFindingsAtOrAbove(report, 'critical')).toBe(1);
236
+ expect(countFindingsAtOrAbove(report, 'high')).toBe(2);
237
+ expect(countFindingsAtOrAbove(report, 'medium')).toBe(3);
238
+ expect(countFindingsAtOrAbove(report, 'low')).toBe(4);
239
+ expect(countFindingsAtOrAbove(report, 'info')).toBe(5);
240
+ });
241
+ });
242
+
243
+ describe('countSeverity', () => {
244
+ it('counts findings of specific severity across reports', () => {
245
+ const reports: SkillReport[] = [
246
+ {
247
+ skill: 'test1',
248
+ summary: 'Test',
249
+ findings: [
250
+ { id: '1', severity: 'high', title: 'High 1', description: 'desc' },
251
+ { id: '2', severity: 'medium', title: 'Medium 1', description: 'desc' },
252
+ ],
253
+ },
254
+ {
255
+ skill: 'test2',
256
+ summary: 'Test',
257
+ findings: [
258
+ { id: '3', severity: 'high', title: 'High 2', description: 'desc' },
259
+ { id: '4', severity: 'high', title: 'High 3', description: 'desc' },
260
+ ],
261
+ },
262
+ ];
263
+
264
+ expect(countSeverity(reports, 'high')).toBe(3);
265
+ expect(countSeverity(reports, 'medium')).toBe(1);
266
+ expect(countSeverity(reports, 'low')).toBe(0);
267
+ });
268
+ });
269
+
270
+ describe('SEVERITY_ORDER', () => {
271
+ it('has correct ordering (lower = more severe)', () => {
272
+ expect(SEVERITY_ORDER.critical).toBeLessThan(SEVERITY_ORDER.high);
273
+ expect(SEVERITY_ORDER.high).toBeLessThan(SEVERITY_ORDER.medium);
274
+ expect(SEVERITY_ORDER.medium).toBeLessThan(SEVERITY_ORDER.low);
275
+ expect(SEVERITY_ORDER.low).toBeLessThan(SEVERITY_ORDER.info);
276
+ });
277
+ });
@@ -0,0 +1,152 @@
1
+ import type { Trigger } from '../config/schema.js';
2
+ import { SEVERITY_ORDER } from '../types/index.js';
3
+ import type { EventContext, Severity, SeverityThreshold, SkillReport } from '../types/index.js';
4
+
5
+ /** Maximum number of patterns to cache (LRU eviction when exceeded) */
6
+ const GLOB_CACHE_MAX_SIZE = 1000;
7
+
8
+ /** Cache for compiled glob patterns with LRU eviction */
9
+ const globCache = new Map<string, RegExp>();
10
+
11
+ /** Clear the glob cache (useful for testing) */
12
+ export function clearGlobCache(): void {
13
+ globCache.clear();
14
+ }
15
+
16
+ /** Get current cache size (useful for testing) */
17
+ export function getGlobCacheSize(): number {
18
+ return globCache.size;
19
+ }
20
+
21
+ /**
22
+ * Convert a glob pattern to a regex (cached with LRU eviction).
23
+ */
24
+ function globToRegex(pattern: string): RegExp {
25
+ const cached = globCache.get(pattern);
26
+ if (cached) {
27
+ // Move to end for LRU ordering (delete and re-add)
28
+ globCache.delete(pattern);
29
+ globCache.set(pattern, cached);
30
+ return cached;
31
+ }
32
+
33
+ // Use placeholders to avoid replacement conflicts
34
+ let regexPattern = pattern
35
+ // First, replace glob patterns with placeholders
36
+ .replace(/\*\*\//g, '\0GLOBSTAR_SLASH\0')
37
+ .replace(/\*\*/g, '\0GLOBSTAR\0')
38
+ .replace(/\*/g, '\0STAR\0')
39
+ .replace(/\?/g, '\0QUESTION\0');
40
+
41
+ // Escape regex special characters
42
+ regexPattern = regexPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
43
+
44
+ // Replace placeholders with regex patterns
45
+ regexPattern = regexPattern
46
+ .replace(/\0GLOBSTAR_SLASH\0/g, '(?:.*/)?') // **/ matches zero or more directories
47
+ .replace(/\0GLOBSTAR\0/g, '.*') // ** matches anything
48
+ .replace(/\0STAR\0/g, '[^/]*') // * matches anything except /
49
+ .replace(/\0QUESTION\0/g, '[^/]'); // ? matches single char except /
50
+
51
+ const regex = new RegExp(`^${regexPattern}$`);
52
+
53
+ // Evict oldest entry if cache is full
54
+ if (globCache.size >= GLOB_CACHE_MAX_SIZE) {
55
+ const oldestKey = globCache.keys().next().value;
56
+ if (oldestKey !== undefined) {
57
+ globCache.delete(oldestKey);
58
+ }
59
+ }
60
+
61
+ globCache.set(pattern, regex);
62
+ return regex;
63
+ }
64
+
65
+ /**
66
+ * Match a glob pattern against a file path.
67
+ * Supports ** for recursive matching and * for single directory matching.
68
+ */
69
+ export function matchGlob(pattern: string, path: string): boolean {
70
+ return globToRegex(pattern).test(path);
71
+ }
72
+
73
+ /**
74
+ * Check if a trigger matches the given event context.
75
+ */
76
+ export function matchTrigger(trigger: Trigger, context: EventContext): boolean {
77
+ if (trigger.event !== context.eventType) {
78
+ return false;
79
+ }
80
+
81
+ // Schedule events don't have actions - they match based on whether
82
+ // any files match the paths filter (context was already built with matching files)
83
+ if (trigger.event === 'schedule') {
84
+ return (context.pullRequest?.files.length ?? 0) > 0;
85
+ }
86
+
87
+ // For non-schedule events, actions must match
88
+ if (!trigger.actions?.includes(context.action)) {
89
+ return false;
90
+ }
91
+
92
+ const filenames = context.pullRequest?.files.map((f) => f.filename);
93
+ const pathPatterns = trigger.filters?.paths;
94
+ const ignorePatterns = trigger.filters?.ignorePaths;
95
+
96
+ // Fail trigger match when path filters are defined but filenames unavailable
97
+ // This prevents filters from being silently bypassed on API failures
98
+ if ((pathPatterns || ignorePatterns) && (!filenames || filenames.length === 0)) {
99
+ return false;
100
+ }
101
+
102
+ if (pathPatterns && filenames) {
103
+ const hasMatch = filenames.some((file) =>
104
+ pathPatterns.some((pattern) => matchGlob(pattern, file))
105
+ );
106
+ if (!hasMatch) {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ if (ignorePatterns && filenames) {
112
+ const allIgnored = filenames.every((file) =>
113
+ ignorePatterns.some((pattern) => matchGlob(pattern, file))
114
+ );
115
+ if (allIgnored) {
116
+ return false;
117
+ }
118
+ }
119
+
120
+ return true;
121
+ }
122
+
123
+ /**
124
+ * Check if a report has any findings at or above the given severity threshold.
125
+ * Returns false if failOn is 'off' (disabled).
126
+ */
127
+ export function shouldFail(report: SkillReport, failOn: SeverityThreshold): boolean {
128
+ if (failOn === 'off') return false;
129
+ const threshold = SEVERITY_ORDER[failOn];
130
+ return report.findings.some((f) => SEVERITY_ORDER[f.severity] <= threshold);
131
+ }
132
+
133
+ /**
134
+ * Count findings at or above the given severity threshold.
135
+ * Returns 0 if failOn is 'off' (disabled).
136
+ */
137
+ export function countFindingsAtOrAbove(report: SkillReport, failOn: SeverityThreshold): number {
138
+ if (failOn === 'off') return 0;
139
+ const threshold = SEVERITY_ORDER[failOn];
140
+ return report.findings.filter((f) => SEVERITY_ORDER[f.severity] <= threshold).length;
141
+ }
142
+
143
+ /**
144
+ * Count findings of a specific severity across multiple reports.
145
+ */
146
+ export function countSeverity(reports: SkillReport[], severity: Severity): number {
147
+ return reports.reduce(
148
+ (count, report) =>
149
+ count + report.findings.filter((f) => f.severity === severity).length,
150
+ 0
151
+ );
152
+ }
@@ -0,0 +1,194 @@
1
+ import { z } from 'zod';
2
+
3
+ // Severity levels for findings
4
+ export const SeveritySchema = z.enum(['critical', 'high', 'medium', 'low', 'info']);
5
+ export type Severity = z.infer<typeof SeveritySchema>;
6
+
7
+ // Confidence levels for findings
8
+ export const ConfidenceSchema = z.enum(['high', 'medium', 'low']);
9
+ export type Confidence = z.infer<typeof ConfidenceSchema>;
10
+
11
+ /**
12
+ * Confidence order for comparison (lower = more confident).
13
+ * Single source of truth for confidence ordering across the codebase.
14
+ */
15
+ export const CONFIDENCE_ORDER: Record<Confidence, number> = {
16
+ high: 0,
17
+ medium: 1,
18
+ low: 2,
19
+ };
20
+
21
+ // Severity threshold for config options (includes 'off' to disable)
22
+ export const SeverityThresholdSchema = z.enum(['off', 'critical', 'high', 'medium', 'low', 'info']);
23
+ export type SeverityThreshold = z.infer<typeof SeverityThresholdSchema>;
24
+
25
+ /**
26
+ * Severity order for comparison (lower = more severe).
27
+ * Single source of truth for severity ordering across the codebase.
28
+ */
29
+ export const SEVERITY_ORDER: Record<Severity, number> = {
30
+ critical: 0,
31
+ high: 1,
32
+ medium: 2,
33
+ low: 3,
34
+ info: 4,
35
+ };
36
+
37
+ /**
38
+ * Filter findings to only include those at or above the given severity threshold.
39
+ * If no threshold is provided, returns all findings unchanged.
40
+ * If threshold is 'off', returns empty array (disabled).
41
+ */
42
+ export function filterFindingsBySeverity(findings: Finding[], threshold?: SeverityThreshold): Finding[] {
43
+ if (!threshold) return findings;
44
+ if (threshold === 'off') return [];
45
+ const thresholdOrder = SEVERITY_ORDER[threshold];
46
+ return findings.filter((f) => SEVERITY_ORDER[f.severity] <= thresholdOrder);
47
+ }
48
+
49
+ // Location within a file
50
+ export const LocationSchema = z.object({
51
+ path: z.string(),
52
+ startLine: z.number().int().positive(),
53
+ endLine: z.number().int().positive().optional(),
54
+ });
55
+ export type Location = z.infer<typeof LocationSchema>;
56
+
57
+ // Suggested fix with diff
58
+ export const SuggestedFixSchema = z.object({
59
+ description: z.string(),
60
+ diff: z.string(),
61
+ });
62
+ export type SuggestedFix = z.infer<typeof SuggestedFixSchema>;
63
+
64
+ // Individual finding from a skill
65
+ export const FindingSchema = z.object({
66
+ id: z.string(),
67
+ severity: SeveritySchema,
68
+ confidence: ConfidenceSchema.optional(),
69
+ title: z.string(),
70
+ description: z.string(),
71
+ location: LocationSchema.optional(),
72
+ suggestedFix: SuggestedFixSchema.optional(),
73
+ elapsedMs: z.number().nonnegative().optional(),
74
+ });
75
+ export type Finding = z.infer<typeof FindingSchema>;
76
+
77
+ // Usage statistics from SDK
78
+ export const UsageStatsSchema = z.object({
79
+ inputTokens: z.number().int().nonnegative(),
80
+ outputTokens: z.number().int().nonnegative(),
81
+ cacheReadInputTokens: z.number().int().nonnegative().optional(),
82
+ cacheCreationInputTokens: z.number().int().nonnegative().optional(),
83
+ costUSD: z.number().nonnegative(),
84
+ });
85
+ export type UsageStats = z.infer<typeof UsageStatsSchema>;
86
+
87
+ // Skipped file info for chunking
88
+ export const SkippedFileSchema = z.object({
89
+ filename: z.string(),
90
+ reason: z.enum(['pattern', 'builtin']),
91
+ pattern: z.string().optional(),
92
+ });
93
+ export type SkippedFile = z.infer<typeof SkippedFileSchema>;
94
+
95
+ // Skill report output
96
+ export const SkillReportSchema = z.object({
97
+ skill: z.string(),
98
+ summary: z.string(),
99
+ findings: z.array(FindingSchema),
100
+ metadata: z.record(z.string(), z.unknown()).optional(),
101
+ durationMs: z.number().nonnegative().optional(),
102
+ usage: UsageStatsSchema.optional(),
103
+ /** Files that were skipped due to chunking patterns */
104
+ skippedFiles: z.array(SkippedFileSchema).optional(),
105
+ /** Number of hunks that failed to analyze (SDK errors, API errors, etc.) */
106
+ failedHunks: z.number().int().nonnegative().optional(),
107
+ });
108
+ export type SkillReport = z.infer<typeof SkillReportSchema>;
109
+
110
+ // GitHub event types
111
+ export const GitHubEventTypeSchema = z.enum([
112
+ 'pull_request',
113
+ 'issues',
114
+ 'issue_comment',
115
+ 'pull_request_review',
116
+ 'pull_request_review_comment',
117
+ 'schedule',
118
+ ]);
119
+ export type GitHubEventType = z.infer<typeof GitHubEventTypeSchema>;
120
+
121
+ // Pull request actions
122
+ export const PullRequestActionSchema = z.enum([
123
+ 'opened',
124
+ 'synchronize',
125
+ 'reopened',
126
+ 'closed',
127
+ ]);
128
+ export type PullRequestAction = z.infer<typeof PullRequestActionSchema>;
129
+
130
+ // File change info
131
+ export const FileChangeSchema = z.object({
132
+ filename: z.string(),
133
+ status: z.enum(['added', 'removed', 'modified', 'renamed', 'copied', 'changed', 'unchanged']),
134
+ additions: z.number().int().nonnegative(),
135
+ deletions: z.number().int().nonnegative(),
136
+ patch: z.string().optional(),
137
+ chunks: z.number().int().nonnegative().optional(),
138
+ });
139
+ export type FileChange = z.infer<typeof FileChangeSchema>;
140
+
141
+ /**
142
+ * Count the number of chunks/hunks in a patch string.
143
+ * Each chunk starts with @@ -X,Y +A,B @@
144
+ */
145
+ export function countPatchChunks(patch: string | undefined): number {
146
+ if (!patch) return 0;
147
+ const matches = patch.match(/^@@\s/gm);
148
+ return matches?.length ?? 0;
149
+ }
150
+
151
+ // Pull request context
152
+ export const PullRequestContextSchema = z.object({
153
+ number: z.number().int().positive(),
154
+ title: z.string(),
155
+ body: z.string().nullable(),
156
+ author: z.string(),
157
+ baseBranch: z.string(),
158
+ headBranch: z.string(),
159
+ headSha: z.string(),
160
+ files: z.array(FileChangeSchema),
161
+ });
162
+ export type PullRequestContext = z.infer<typeof PullRequestContextSchema>;
163
+
164
+ // Repository context
165
+ export const RepositoryContextSchema = z.object({
166
+ owner: z.string(),
167
+ name: z.string(),
168
+ fullName: z.string(),
169
+ defaultBranch: z.string(),
170
+ });
171
+ export type RepositoryContext = z.infer<typeof RepositoryContextSchema>;
172
+
173
+ // Full event context
174
+ export const EventContextSchema = z.object({
175
+ eventType: GitHubEventTypeSchema,
176
+ action: z.string(),
177
+ repository: RepositoryContextSchema,
178
+ pullRequest: PullRequestContextSchema.optional(),
179
+ repoPath: z.string(),
180
+ });
181
+ export type EventContext = z.infer<typeof EventContextSchema>;
182
+
183
+ // Retry configuration for SDK calls
184
+ export const RetryConfigSchema = z.object({
185
+ /** Maximum number of retry attempts (default: 3) */
186
+ maxRetries: z.number().int().nonnegative().default(3),
187
+ /** Initial delay in milliseconds before first retry (default: 1000) */
188
+ initialDelayMs: z.number().int().positive().default(1000),
189
+ /** Multiplier for exponential backoff (default: 2) */
190
+ backoffMultiplier: z.number().positive().default(2),
191
+ /** Maximum delay in milliseconds between retries (default: 30000) */
192
+ maxDelayMs: z.number().int().positive().default(30000),
193
+ });
194
+ export type RetryConfig = z.infer<typeof RetryConfigSchema>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Process items with limited concurrency using chunked batches.
3
+ */
4
+ export async function processInBatches<T, R>(
5
+ items: T[],
6
+ fn: (item: T) => Promise<R>,
7
+ batchSize: number
8
+ ): Promise<R[]> {
9
+ const results: R[] = [];
10
+
11
+ for (let i = 0; i < items.length; i += batchSize) {
12
+ const batch = items.slice(i, i + batchSize);
13
+ const batchResults = await Promise.all(batch.map(fn));
14
+ results.push(...batchResults);
15
+ }
16
+
17
+ return results;
18
+ }