@alex-dbs/coderuler 0.0.0-stage → 1.3.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 (121) hide show
  1. package/LICENSE +175 -0
  2. package/README.md +218 -2
  3. package/dist/adapter/cli/commands/check.d.ts +3 -0
  4. package/dist/adapter/cli/commands/check.js +93 -0
  5. package/dist/adapter/cli/commands/deps.d.ts +3 -0
  6. package/dist/adapter/cli/commands/deps.js +4 -0
  7. package/dist/adapter/cli/commands/inspect.d.ts +3 -0
  8. package/dist/adapter/cli/commands/inspect.js +59 -0
  9. package/dist/adapter/cli/commands/rules.d.ts +10 -0
  10. package/dist/adapter/cli/commands/rules.js +17 -0
  11. package/dist/adapter/cli/commands/skill.d.ts +4 -0
  12. package/dist/adapter/cli/commands/skill.js +7 -0
  13. package/dist/adapter/cli/commands/test.d.ts +3 -0
  14. package/dist/adapter/cli/commands/test.js +78 -0
  15. package/dist/adapter/cli/commands/update.d.ts +3 -0
  16. package/dist/adapter/cli/commands/update.js +7 -0
  17. package/dist/adapter/cli/commands/validate.d.ts +3 -0
  18. package/dist/adapter/cli/commands/validate.js +12 -0
  19. package/dist/adapter/cli/exit.d.ts +7 -0
  20. package/dist/adapter/cli/exit.js +10 -0
  21. package/dist/adapter/cli/inspect-json.d.ts +9 -0
  22. package/dist/adapter/cli/inspect-json.js +160 -0
  23. package/dist/adapter/cli/inspect-usage.d.ts +9 -0
  24. package/dist/adapter/cli/inspect-usage.js +14 -0
  25. package/dist/adapter/cli/program.d.ts +4 -0
  26. package/dist/adapter/cli/program.js +216 -0
  27. package/dist/adapter/cli/progress.d.ts +22 -0
  28. package/dist/adapter/cli/progress.js +79 -0
  29. package/dist/adapter/cli/reports.d.ts +47 -0
  30. package/dist/adapter/cli/reports.js +735 -0
  31. package/dist/adapter/cli/self-update.d.ts +1 -0
  32. package/dist/adapter/cli/self-update.js +89 -0
  33. package/dist/adapter/cli/services.d.ts +101 -0
  34. package/dist/adapter/cli/services.js +1 -0
  35. package/dist/adapter/cli/text.d.ts +5 -0
  36. package/dist/adapter/cli/text.js +36 -0
  37. package/dist/adapter/cli/version.d.ts +1 -0
  38. package/dist/adapter/cli/version.js +3 -0
  39. package/dist/adapter/decision/jev.d.ts +11 -0
  40. package/dist/adapter/decision/jev.js +138 -0
  41. package/dist/adapter/filesystem/dependencies.d.ts +26 -0
  42. package/dist/adapter/filesystem/dependencies.js +577 -0
  43. package/dist/adapter/filesystem/dependency-installation.d.ts +11 -0
  44. package/dist/adapter/filesystem/dependency-installation.js +373 -0
  45. package/dist/adapter/filesystem/project.d.ts +14 -0
  46. package/dist/adapter/filesystem/project.js +387 -0
  47. package/dist/adapter/filesystem/rule-resolution.d.ts +4 -0
  48. package/dist/adapter/filesystem/rule-resolution.js +516 -0
  49. package/dist/adapter/filesystem/rule-test-worker.d.ts +17 -0
  50. package/dist/adapter/filesystem/rule-test-worker.js +47 -0
  51. package/dist/adapter/filesystem/rule-tests.d.ts +3 -0
  52. package/dist/adapter/filesystem/rule-tests.js +264 -0
  53. package/dist/adapter/filesystem/rules.d.ts +13 -0
  54. package/dist/adapter/filesystem/rules.js +242 -0
  55. package/dist/adapter/git/changes.d.ts +1 -0
  56. package/dist/adapter/git/changes.js +54 -0
  57. package/dist/adapter/git/environment.d.ts +1 -0
  58. package/dist/adapter/git/environment.js +10 -0
  59. package/dist/adapter/github/api.d.ts +5 -0
  60. package/dist/adapter/github/api.js +40 -0
  61. package/dist/adapter/github/comments.d.ts +3 -0
  62. package/dist/adapter/github/comments.js +235 -0
  63. package/dist/adapter/github/pull-request.d.ts +16 -0
  64. package/dist/adapter/github/pull-request.js +139 -0
  65. package/dist/adapter/scope/astgrep-languages.d.ts +5 -0
  66. package/dist/adapter/scope/astgrep-languages.js +119 -0
  67. package/dist/adapter/scope/astgrep.d.ts +14 -0
  68. package/dist/adapter/scope/astgrep.js +183 -0
  69. package/dist/adapter/scope/csv.d.ts +4 -0
  70. package/dist/adapter/scope/csv.js +158 -0
  71. package/dist/adapter/scope/file.d.ts +2 -0
  72. package/dist/adapter/scope/file.js +4 -0
  73. package/dist/adapter/scope/json.d.ts +7 -0
  74. package/dist/adapter/scope/json.js +132 -0
  75. package/dist/adapter/scope/lines.d.ts +10 -0
  76. package/dist/adapter/scope/lines.js +42 -0
  77. package/dist/adapter/scope/protobuf-options.d.ts +29 -0
  78. package/dist/adapter/scope/protobuf-options.js +64 -0
  79. package/dist/adapter/scope/protobuf-parser.d.ts +30 -0
  80. package/dist/adapter/scope/protobuf-parser.js +772 -0
  81. package/dist/adapter/scope/protobuf.d.ts +5 -0
  82. package/dist/adapter/scope/protobuf.js +53 -0
  83. package/dist/adapter/scope/structured.d.ts +10 -0
  84. package/dist/adapter/scope/structured.js +84 -0
  85. package/dist/adapter/scope/yaml.d.ts +7 -0
  86. package/dist/adapter/scope/yaml.js +224 -0
  87. package/dist/adapter/skills/cli.d.ts +10 -0
  88. package/dist/adapter/skills/cli.js +77 -0
  89. package/dist/app/concurrency.d.ts +1 -0
  90. package/dist/app/concurrency.js +44 -0
  91. package/dist/app/evaluators.d.ts +14 -0
  92. package/dist/app/evaluators.js +175 -0
  93. package/dist/app/execute.d.ts +2 -0
  94. package/dist/app/execute.js +427 -0
  95. package/dist/app/jsonpath.d.ts +1 -0
  96. package/dist/app/jsonpath.js +8 -0
  97. package/dist/app/ports.d.ts +165 -0
  98. package/dist/app/ports.js +1 -0
  99. package/dist/app/rule-tests.d.ts +20 -0
  100. package/dist/app/rule-tests.js +11 -0
  101. package/dist/app/rule.d.ts +121 -0
  102. package/dist/app/rule.js +24 -0
  103. package/dist/app/selection.d.ts +2 -0
  104. package/dist/app/selection.js +23 -0
  105. package/dist/app/usage.d.ts +2 -0
  106. package/dist/app/usage.js +13 -0
  107. package/dist/app/validate.d.ts +11 -0
  108. package/dist/app/validate.js +415 -0
  109. package/dist/app/violation-comments.d.ts +10 -0
  110. package/dist/app/violation-comments.js +1 -0
  111. package/dist/cli.d.ts +2 -0
  112. package/dist/cli.js +3 -0
  113. package/dist/index.d.ts +22 -0
  114. package/dist/index.js +11 -0
  115. package/dist/infra/runtime.d.ts +3 -0
  116. package/dist/infra/runtime.js +168 -0
  117. package/dist/infra/settings.d.ts +5 -0
  118. package/dist/infra/settings.js +91 -0
  119. package/package.json +86 -4
  120. package/skills/coderuler-new-rule/SKILL.md +141 -0
  121. package/skills/coderuler-new-rule/reference.md +447 -0
@@ -0,0 +1,415 @@
1
+ import { RULE_BRAND } from './rule.js';
2
+ function invalid(label, detail) {
3
+ throw new Error(`${label}: ${detail}`);
4
+ }
5
+ export function isRecord(value) {
6
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
7
+ }
8
+ function isLineNumber(value) {
9
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 1;
10
+ }
11
+ function isStringList(value, allowEmpty) {
12
+ if (!Array.isArray(value))
13
+ return false;
14
+ if (!allowEmpty && value.length === 0)
15
+ return false;
16
+ for (let index = 0; index < value.length; index += 1) {
17
+ if (!Object.hasOwn(value, index))
18
+ return false;
19
+ const entry = value[index];
20
+ if (typeof entry !== 'string' || entry.length === 0)
21
+ return false;
22
+ }
23
+ return true;
24
+ }
25
+ function validateSettings(settings, at) {
26
+ const backend = settings['backend'];
27
+ if (backend !== undefined && backend !== 'jev')
28
+ invalid(at, 'settings.backend must be "jev"');
29
+ const model = settings['model'];
30
+ if (model !== undefined && (typeof model !== 'string' || model.trim() === '')) {
31
+ invalid(at, 'settings.model must be a non-blank string');
32
+ }
33
+ const threshold = settings['threshold'];
34
+ if (threshold !== undefined &&
35
+ (typeof threshold !== 'number' || !Number.isFinite(threshold) || threshold < 0 || threshold > 1)) {
36
+ invalid(at, 'settings.threshold must be a finite number between 0 and 1');
37
+ }
38
+ for (const key of ['inputCostPerMillion', 'outputCostPerMillion']) {
39
+ const value = settings[key];
40
+ if (value !== undefined && (typeof value !== 'number' || !Number.isFinite(value) || value < 0)) {
41
+ invalid(at, `settings.${key} must be a finite nonnegative number`);
42
+ }
43
+ }
44
+ }
45
+ function isGrouping(value) {
46
+ if (value === 'scope' || value === 'file' || value === 'folder' || value === 'project')
47
+ return true;
48
+ if (!isRecord(value) || value['by'] !== 'folder')
49
+ return false;
50
+ const level = value['level'];
51
+ return typeof level === 'number' && Number.isInteger(level) && level <= 0;
52
+ }
53
+ export function isRuleBranded(value) {
54
+ const target = typeof value === 'function' ? value.prototype : value;
55
+ if (target === null || (typeof target !== 'object' && typeof target !== 'function'))
56
+ return false;
57
+ return Reflect.get(target, RULE_BRAND) === true;
58
+ }
59
+ export function validateRule(value, ruleFile) {
60
+ const at = `Invalid rule at ${ruleFile}`;
61
+ if (!isRecord(value))
62
+ invalid(at, 'expected a rule object');
63
+ if (typeof value['enabled'] !== 'boolean')
64
+ invalid(at, 'enabled must be a boolean');
65
+ if (!isStringList(value['files'], false)) {
66
+ invalid(at, 'files must be a non-empty array of non-empty glob strings');
67
+ }
68
+ if (!isStringList(value['exclude'], true)) {
69
+ invalid(at, 'exclude must be an array of non-empty glob strings');
70
+ }
71
+ const tags = value['tags'];
72
+ if (tags !== undefined && !isStringList(tags, true)) {
73
+ invalid(at, 'tags must be an array of non-empty strings');
74
+ }
75
+ const scope = value['scope'];
76
+ if (scope !== undefined && typeof scope !== 'function')
77
+ invalid(at, 'scope must be a function');
78
+ const settings = value['settings'];
79
+ if (!isRecord(settings))
80
+ invalid(at, 'settings must be an object');
81
+ validateSettings(settings, at);
82
+ const comment = value['comment'];
83
+ if (comment !== undefined && (typeof comment !== 'string' || comment.trim() === '')) {
84
+ invalid(at, 'comment must be a non-blank string');
85
+ }
86
+ if (!isGrouping(value['group'])) {
87
+ invalid(at, 'group must be "scope", "file", "folder", "project", or { by: "folder", level: integer <= 0 }');
88
+ }
89
+ for (const hook of ['when', 'filter', 'select', 'check']) {
90
+ if (typeof value[hook] !== 'function')
91
+ invalid(at, `${hook} must be a function`);
92
+ }
93
+ return value;
94
+ }
95
+ export function validateJsonValue(value, label) {
96
+ assertJsonCompatible(value, label, new Set());
97
+ return value;
98
+ }
99
+ function assertJsonCompatible(value, label, ancestors) {
100
+ if (value === null || typeof value === 'boolean' || typeof value === 'string')
101
+ return;
102
+ if (typeof value === 'number') {
103
+ if (!Number.isFinite(value))
104
+ invalid(label, 'numbers must be finite');
105
+ return;
106
+ }
107
+ if (typeof value !== 'object') {
108
+ invalid(label, `expected JSON-compatible data, received ${typeof value}`);
109
+ }
110
+ const container = value;
111
+ if (ancestors.has(container))
112
+ invalid(label, 'cyclic data is not supported');
113
+ ancestors.add(container);
114
+ if ('toJSON' in value && typeof value.toJSON === 'function') {
115
+ invalid(label, 'expected plain JSON data without a toJSON serialization hook');
116
+ }
117
+ if (Array.isArray(value)) {
118
+ for (let index = 0; index < value.length; index += 1) {
119
+ if (!Object.hasOwn(value, index))
120
+ invalid(label, `arrays must not be sparse (missing index ${index})`);
121
+ assertJsonCompatible(value[index], `${label}[${index}]`, ancestors);
122
+ }
123
+ }
124
+ else if (!isRecord(value)) {
125
+ invalid(label, 'expected a plain JSON object or array');
126
+ }
127
+ else {
128
+ const prototype = Object.getPrototypeOf(value);
129
+ if (prototype !== null && prototype !== Object.prototype) {
130
+ invalid(label, 'expected a plain JSON object or array');
131
+ }
132
+ for (const key of Object.keys(value))
133
+ assertJsonCompatible(value[key], `${label}.${key}`, ancestors);
134
+ }
135
+ ancestors.delete(container);
136
+ }
137
+ function plainCaptures(value, label) {
138
+ if (!isRecord(value))
139
+ invalid(label, 'captures must be an object of strings or string arrays');
140
+ const prototype = Object.getPrototypeOf(value);
141
+ if (prototype !== null && prototype !== Object.prototype) {
142
+ invalid(label, 'captures must be a plain object of strings or string arrays');
143
+ }
144
+ const captures = {};
145
+ for (const key of Object.keys(value)) {
146
+ const capture = value[key];
147
+ const entry = typeof capture === 'string'
148
+ ? capture
149
+ : Array.isArray(capture) && isDenseStringArray(capture)
150
+ ? [...capture]
151
+ : undefined;
152
+ if (entry === undefined)
153
+ invalid(label, `capture "${key}" must be a string or an array of strings`);
154
+ Object.defineProperty(captures, key, { value: entry, enumerable: true, writable: true, configurable: true });
155
+ }
156
+ return captures;
157
+ }
158
+ function isDenseStringArray(value) {
159
+ for (let index = 0; index < value.length; index += 1) {
160
+ if (!Object.hasOwn(value, index) || typeof value[index] !== 'string')
161
+ return false;
162
+ }
163
+ return true;
164
+ }
165
+ function plainLines(value, label) {
166
+ if (!isRecord(value))
167
+ invalid(label, 'lines must be an object with start and end');
168
+ const { start, end } = value;
169
+ if (!isLineNumber(start) || !isLineNumber(end) || start > end) {
170
+ invalid(label, 'lines must be an increasing 1-based inclusive range');
171
+ }
172
+ return { start, end };
173
+ }
174
+ export function validateScopeMatch(value, label) {
175
+ if (!isRecord(value))
176
+ invalid(label, 'expected a scope match object');
177
+ const id = value['id'];
178
+ if (typeof id !== 'string' || id === '')
179
+ invalid(label, 'scope id must be a non-empty string');
180
+ const source = value['source'];
181
+ if (typeof source !== 'string')
182
+ invalid(label, 'scope source must be a string');
183
+ const rawData = value['data'];
184
+ const data = rawData === undefined ? undefined : validateJsonValue(rawData, `${label}.data`);
185
+ const rawCaptures = value['captures'];
186
+ const captures = rawCaptures === undefined ? undefined : plainCaptures(rawCaptures, `${label}.captures`);
187
+ const rawLines = value['lines'];
188
+ const lines = rawLines === undefined ? undefined : plainLines(rawLines, `${label}.lines`);
189
+ return {
190
+ id,
191
+ source,
192
+ ...(data === undefined ? {} : { data }),
193
+ ...(captures === undefined ? {} : { captures }),
194
+ ...(lines === undefined ? {} : { lines }),
195
+ };
196
+ }
197
+ export function validateScopeSubject(value, label) {
198
+ if (!isRecord(value))
199
+ invalid(label, 'expected a scope subject object');
200
+ if (value['kind'] !== 'scope')
201
+ invalid(label, 'kind must be "scope"');
202
+ const filepath = value['filepath'];
203
+ if (typeof filepath !== 'string' || filepath === '') {
204
+ invalid(label, 'filepath must be a non-empty working-root-relative string');
205
+ }
206
+ return { kind: 'scope', filepath, ...validateScopeMatch(value, label) };
207
+ }
208
+ export function validateScopeSubjects(value, label) {
209
+ if (!Array.isArray(value))
210
+ invalid(label, 'selection must be an array of scope subjects');
211
+ const subjects = value.map((entry, index) => validateScopeSubject(entry, `${label}[${index}]`));
212
+ const seen = new Set();
213
+ for (const subject of subjects) {
214
+ const key = `${subject.filepath}\u0000${subject.id}`;
215
+ if (seen.has(key))
216
+ invalid(label, `duplicate scope id "${subject.id}" for ${subject.filepath}`);
217
+ seen.add(key);
218
+ }
219
+ return subjects;
220
+ }
221
+ export function validatePredicateResult(value, label) {
222
+ if (typeof value === 'boolean')
223
+ return value;
224
+ if (isRecord(value) && typeof value['applies'] === 'boolean') {
225
+ const reason = value['reason'];
226
+ if (reason !== undefined && typeof reason !== 'string')
227
+ invalid(label, 'predicate reason must be a string');
228
+ return value;
229
+ }
230
+ invalid(label, 'predicate must return a boolean or { applies, reason? }');
231
+ }
232
+ export function validateAiUsage(value, label) {
233
+ if (!isRecord(value))
234
+ invalid(label, 'usage must be an object');
235
+ for (const key of ['calls', 'unreportedCalls', 'inputTokens', 'outputTokens']) {
236
+ const count = value[key];
237
+ if (typeof count !== 'number' || !Number.isSafeInteger(count) || count < 0) {
238
+ invalid(label, `usage.${key} must be a nonnegative safe integer`);
239
+ }
240
+ }
241
+ if (value['unreportedCalls'] > value['calls']) {
242
+ invalid(label, 'usage.unreportedCalls must not exceed usage.calls');
243
+ }
244
+ for (const key of ['inputCost', 'outputCost']) {
245
+ const cost = value[key];
246
+ if (typeof cost !== 'number' || !Number.isFinite(cost) || cost < 0) {
247
+ invalid(label, `usage.${key} must be a finite nonnegative number`);
248
+ }
249
+ }
250
+ return value;
251
+ }
252
+ function indexLocationMembers(subject) {
253
+ const byFilepath = new Map();
254
+ const members = subject.kind === 'group' ? subject.members : [subject];
255
+ for (const member of members) {
256
+ const { filepath, id } = member;
257
+ let file = byFilepath.get(filepath);
258
+ if (file === undefined) {
259
+ file = { all: [], byId: new Map() };
260
+ byFilepath.set(filepath, file);
261
+ }
262
+ file.all.push(member);
263
+ const matches = file.byId.get(id);
264
+ if (matches === undefined)
265
+ file.byId.set(id, [member]);
266
+ else
267
+ matches.push(member);
268
+ }
269
+ return byFilepath;
270
+ }
271
+ function indexLocationRanges(members) {
272
+ const ranges = [];
273
+ for (const member of members) {
274
+ const lines = member.lines;
275
+ if (lines === undefined)
276
+ return { unbounded: true, ranges };
277
+ ranges.push({ start: lines.start, maxEnd: lines.end });
278
+ }
279
+ ranges.sort((left, right) => left.start - right.start);
280
+ let maxEnd = 0;
281
+ for (const range of ranges) {
282
+ maxEnd = Math.max(maxEnd, range.maxEnd);
283
+ range.maxEnd = maxEnd;
284
+ }
285
+ return { unbounded: false, ranges };
286
+ }
287
+ function containsLocationRange(file, lines) {
288
+ const { unbounded, ranges } = file.rangeIndex ??= indexLocationRanges(file.all);
289
+ if (unbounded)
290
+ return true;
291
+ let low = 0;
292
+ let high = ranges.length;
293
+ while (low < high) {
294
+ const middle = low + Math.floor((high - low) / 2);
295
+ if (ranges[middle].start <= lines.start)
296
+ low = middle + 1;
297
+ else
298
+ high = middle;
299
+ }
300
+ return low > 0 && ranges[low - 1].maxEnd >= lines.end;
301
+ }
302
+ function plainViolationLocations(value, label, subject) {
303
+ if (!Array.isArray(value))
304
+ invalid(label, 'check locations must be an array');
305
+ const locations = [];
306
+ let membersByFilepath;
307
+ for (let index = 0; index < value.length; index += 1) {
308
+ const at = `${label} locations[${index}]`;
309
+ if (!Object.hasOwn(value, index))
310
+ invalid(at, 'missing location entry');
311
+ const location = value[index];
312
+ if (!isRecord(location))
313
+ invalid(at, 'location must be an object');
314
+ const { filepath, scopeId, lines: rawLines } = location;
315
+ if (typeof filepath !== 'string' || filepath === '') {
316
+ invalid(at, 'location filepath must be a non-empty string');
317
+ }
318
+ if (scopeId !== undefined && (typeof scopeId !== 'string' || scopeId === '')) {
319
+ invalid(at, 'location scopeId must be a non-empty string');
320
+ }
321
+ let lines = rawLines === undefined ? undefined : plainLines(rawLines, at);
322
+ if (scopeId === undefined && lines === undefined) {
323
+ invalid(at, 'location must specify scopeId or lines');
324
+ }
325
+ if (subject !== undefined) {
326
+ const file = (membersByFilepath ??= indexLocationMembers(subject)).get(filepath);
327
+ if (file === undefined)
328
+ invalid(at, 'location must reference an evaluated scope or group member');
329
+ const members = scopeId === undefined ? file.all : file.byId.get(scopeId);
330
+ if (members === undefined)
331
+ invalid(at, 'location must reference an evaluated scope or group member');
332
+ let contained = false;
333
+ let resolvedLines;
334
+ if (scopeId === undefined && lines !== undefined) {
335
+ contained = containsLocationRange(file, lines);
336
+ }
337
+ else {
338
+ for (const member of members) {
339
+ const memberLines = member.lines;
340
+ if (lines !== undefined) {
341
+ if (memberLines === undefined ||
342
+ (lines.start >= memberLines.start && lines.end <= memberLines.end)) {
343
+ contained = true;
344
+ }
345
+ }
346
+ else if (memberLines !== undefined) {
347
+ if (resolvedLines !== undefined &&
348
+ (resolvedLines.start !== memberLines.start || resolvedLines.end !== memberLines.end)) {
349
+ invalid(at, 'location scopeId identifies multiple line ranges');
350
+ }
351
+ resolvedLines = { start: memberLines.start, end: memberLines.end };
352
+ }
353
+ }
354
+ }
355
+ if (lines !== undefined && !contained) {
356
+ invalid(at, 'location lines must be contained in an evaluated scope or group member');
357
+ }
358
+ lines ??= resolvedLines;
359
+ }
360
+ locations.push({
361
+ filepath,
362
+ ...(scopeId === undefined ? {} : { scopeId }),
363
+ ...(lines === undefined ? {} : { lines }),
364
+ });
365
+ }
366
+ return locations;
367
+ }
368
+ export function validateCheckResult(value, label, subject) {
369
+ if (!isRecord(value))
370
+ invalid(label, 'check must return a check result object');
371
+ const status = value['status'];
372
+ if (status !== 'pass' && status !== 'violation') {
373
+ invalid(label, 'check status must be "pass" or "violation"');
374
+ }
375
+ const message = value['message'];
376
+ if (message !== undefined && typeof message !== 'string') {
377
+ invalid(label, 'check message must be a string');
378
+ }
379
+ const model = value['model'];
380
+ if (model !== undefined && (typeof model !== 'string' || model.trim() === '')) {
381
+ invalid(label, 'check model must be a non-blank string');
382
+ }
383
+ const probability = value['probability'];
384
+ if (probability !== undefined &&
385
+ (typeof probability !== 'number' ||
386
+ !Number.isFinite(probability) ||
387
+ probability < 0 ||
388
+ probability > 1)) {
389
+ invalid(label, 'check probability must be a finite number between 0 and 1');
390
+ }
391
+ const threshold = value['threshold'];
392
+ if (threshold !== undefined &&
393
+ (typeof threshold !== 'number' ||
394
+ !Number.isFinite(threshold) ||
395
+ threshold < 0 ||
396
+ threshold > 1)) {
397
+ invalid(label, 'check threshold must be a finite number between 0 and 1');
398
+ }
399
+ const rawUsage = value['usage'];
400
+ const usage = rawUsage === undefined ? undefined : validateAiUsage(rawUsage, label);
401
+ const rawLocations = value['locations'];
402
+ if (rawLocations !== undefined && status !== 'violation') {
403
+ invalid(label, 'check locations are only allowed on violation results');
404
+ }
405
+ const locations = rawLocations === undefined ? undefined : plainViolationLocations(rawLocations, label, subject);
406
+ return {
407
+ status,
408
+ ...(message === undefined ? {} : { message }),
409
+ ...(probability === undefined ? {} : { probability }),
410
+ ...(threshold === undefined ? {} : { threshold }),
411
+ ...(model === undefined ? {} : { model }),
412
+ ...(usage === undefined ? {} : { usage }),
413
+ ...(locations === undefined ? {} : { locations }),
414
+ };
415
+ }
@@ -0,0 +1,10 @@
1
+ export interface ViolationComment {
2
+ readonly body: string;
3
+ readonly locations: readonly {
4
+ readonly filepath: string;
5
+ readonly lines: {
6
+ readonly start: number;
7
+ readonly end: number;
8
+ };
9
+ }[];
10
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from './infra/runtime.js';
3
+ process.exitCode = await runCli(process.argv.slice(2));
@@ -0,0 +1,22 @@
1
+ export { Rule } from './app/rule.js';
2
+ export { test } from './app/rule-tests.js';
3
+ export type { RuleTestCase } from './app/rule-tests.js';
4
+ export type { AiExamples, AiOptions, AiUsage, Awaitable, CheckContext, CheckResult, Evaluator, Grouping, GroupSubject, JsonValue, PredicateResult, ProjectContext, RuleSettings, ScopeMatch, ScopeSelector, ScopeSubject, Selection, SelectionContext, SourceFile, Subject, TokenPricing, TokenUsage, ViolationLocation, } from './app/rule.js';
5
+ export { ai, pass, regex, violation } from './app/evaluators.js';
6
+ export type { RegexOptions } from './app/evaluators.js';
7
+ export type { CsvOptions, CsvRow, DecisionBackend, DecisionExchange, DecisionInstructions, DecisionObserver, DecisionOutcome, DecisionRequest, DecisionRuntime, LoadedRule, Project, RuleEvaluation, RuleLoader, RuleReport, RuleSelection, RunContext, RunEvent, } from './app/ports.js';
8
+ import { astGrep } from './adapter/scope/astgrep.js';
9
+ import { csv } from './adapter/scope/csv.js';
10
+ import { file } from './adapter/scope/file.js';
11
+ import { json } from './adapter/scope/json.js';
12
+ import { protobuf } from './adapter/scope/protobuf.js';
13
+ import { yaml } from './adapter/scope/yaml.js';
14
+ export { createAstGrepLanguageRegistrations } from './adapter/scope/astgrep-languages.js';
15
+ export declare const scopes: {
16
+ file: typeof file;
17
+ json: typeof json;
18
+ yaml: typeof yaml;
19
+ csv: typeof csv;
20
+ astGrep: typeof astGrep;
21
+ protobuf: typeof protobuf;
22
+ };
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { Rule } from './app/rule.js';
2
+ export { test } from './app/rule-tests.js';
3
+ export { ai, pass, regex, violation } from './app/evaluators.js';
4
+ import { astGrep } from './adapter/scope/astgrep.js';
5
+ import { csv } from './adapter/scope/csv.js';
6
+ import { file } from './adapter/scope/file.js';
7
+ import { json } from './adapter/scope/json.js';
8
+ import { protobuf } from './adapter/scope/protobuf.js';
9
+ import { yaml } from './adapter/scope/yaml.js';
10
+ export { createAstGrepLanguageRegistrations } from './adapter/scope/astgrep-languages.js';
11
+ export const scopes = { file, json, yaml, csv, astGrep, protobuf };
@@ -0,0 +1,3 @@
1
+ import type { CliServices } from '../adapter/cli/services.js';
2
+ export declare function createServices(cwd: string, env: Readonly<Record<string, string | undefined>>): CliServices;
3
+ export declare function runCli(argv: readonly string[]): Promise<number>;
@@ -0,0 +1,168 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join, resolve } from 'node:path';
4
+ import { runProgram } from '../adapter/cli/program.js';
5
+ import { terminalCapabilities } from '../adapter/cli/progress.js';
6
+ import { createRedactor, escapeDiagnostic } from '../adapter/cli/text.js';
7
+ import { installedVersion } from '../adapter/cli/version.js';
8
+ const EXIT_FAILURE = 2;
9
+ const ENVIRONMENT_FILE = '.env';
10
+ function loadEnvironment(cwd) {
11
+ try {
12
+ process.loadEnvFile(resolve(cwd, ENVIRONMENT_FILE));
13
+ }
14
+ catch (error) {
15
+ if (error.code !== 'ENOENT')
16
+ throw error;
17
+ }
18
+ }
19
+ export function createServices(cwd, env) {
20
+ const redact = createRedactor([env.TYPESAFE_API_KEY, env.GITHUB_TOKEN]);
21
+ let dependenciesPromise;
22
+ let ruleServicesPromise;
23
+ const getDependencies = (context) => dependenciesPromise ??= (async () => {
24
+ const { createRuleDependencyManager } = await import('../adapter/filesystem/dependencies.js');
25
+ return createRuleDependencyManager({
26
+ ...(env.RK_DEPENDENCY_CACHE === undefined ? {} : { cacheRoot: resolve(cwd, env.RK_DEPENDENCY_CACHE) }),
27
+ onWarning: message => context.writeError(`coderuler: ${context.redact(escapeDiagnostic(message))}\n`),
28
+ });
29
+ })();
30
+ const getRuleServices = (context) => ruleServicesPromise ??= (async () => {
31
+ const dependencies = await getDependencies(context);
32
+ const { createRuleLoader, resolveRuleSelector } = await import('../adapter/filesystem/rules.js');
33
+ const loader = createRuleLoader({ dependencies });
34
+ return {
35
+ loadRules: selection => loader.load(selection),
36
+ resolveRuleSelector,
37
+ };
38
+ })();
39
+ const services = {
40
+ cwd,
41
+ version: installedVersion,
42
+ redact,
43
+ terminal: terminalCapabilities({
44
+ stdoutIsTTY: process.stdout.isTTY === true,
45
+ stderrIsTTY: process.stderr.isTTY === true,
46
+ get columns() { return process.stderr.columns; },
47
+ get stdoutColumns() { return process.stdout.columns; },
48
+ }, env),
49
+ write(text) {
50
+ process.stdout.write(text);
51
+ },
52
+ writeError(text) {
53
+ process.stderr.write(text);
54
+ },
55
+ load: {
56
+ async validate(context) {
57
+ return getRuleServices(context);
58
+ },
59
+ async check(context) {
60
+ const [ruleServices, project, execution, settings] = await Promise.all([
61
+ getRuleServices(context),
62
+ import('../adapter/filesystem/project.js'),
63
+ import('../app/execute.js'),
64
+ import('./settings.js'),
65
+ ]);
66
+ return {
67
+ ...ruleServices,
68
+ createProject: project.createProject,
69
+ resolveTarget: project.resolveTarget,
70
+ runRules: execution.executeRules,
71
+ decisionResolver: (thresholdOverride, pricingOverrides) => settings.createDecisionResolver(env, thresholdOverride, pricingOverrides),
72
+ async changedFiles(root, base) {
73
+ const { changedFiles } = await import('../adapter/git/changes.js');
74
+ return changedFiles(root, base);
75
+ },
76
+ async checkoutPullRequest(selector, baseBranch) {
77
+ const { createPullRequestCheckout } = await import('../adapter/github/pull-request.js');
78
+ return createPullRequestCheckout(cwd, env)(selector, baseBranch);
79
+ },
80
+ async commentOnPullRequest(root, comments, pullRequest) {
81
+ const { createGithubCommenter } = await import('../adapter/github/comments.js');
82
+ return createGithubCommenter(env)(root, comments, pullRequest);
83
+ },
84
+ };
85
+ },
86
+ async inspect(context) {
87
+ const [ruleServices, project, execution, settings] = await Promise.all([
88
+ getRuleServices(context),
89
+ import('../adapter/filesystem/project.js'),
90
+ import('../app/execute.js'),
91
+ import('./settings.js'),
92
+ ]);
93
+ return {
94
+ ...ruleServices,
95
+ createProject: project.createProject,
96
+ resolveTarget: project.resolveTarget,
97
+ runRules: execution.executeRules,
98
+ decisionResolver: (thresholdOverride, pricingOverrides) => settings.createDecisionResolver(env, thresholdOverride, pricingOverrides),
99
+ };
100
+ },
101
+ async test(context) {
102
+ const ruleServices = await getRuleServices(context);
103
+ const { runRuleTests } = await import('../adapter/filesystem/rule-tests.js');
104
+ return {
105
+ ...ruleServices,
106
+ testRules: (rules, onResult, timeoutMs, concurrency, includeAI) => runRuleTests(rules, onResult, timeoutMs, concurrency, includeAI),
107
+ };
108
+ },
109
+ async update(_context) {
110
+ const { createToolUpdater } = await import('../adapter/cli/self-update.js');
111
+ return { updateTool: createToolUpdater(cwd, env) };
112
+ },
113
+ async skill(_context) {
114
+ const { createSkillManager } = await import('../adapter/skills/cli.js');
115
+ return { manageSkill: createSkillManager(cwd, env) };
116
+ },
117
+ async deps(context) {
118
+ const dependencies = await getDependencies(context);
119
+ const { default: npa } = await import('npm-package-arg');
120
+ const manageDependencies = async (operation, packages, options) => {
121
+ if (options.global === true && options.directory !== undefined) {
122
+ throw new Error('--global and --directory are mutually exclusive');
123
+ }
124
+ const directory = options.global === true
125
+ ? resolve(homedir(), '.coderuler')
126
+ : resolve(cwd, options.directory ?? '.coderuler');
127
+ if (operation !== 'add' && !(await stat(directory)).isDirectory()) {
128
+ throw new Error(`Rule dependency directory is not a directory: ${directory}`);
129
+ }
130
+ if (operation === 'sync') {
131
+ const prepared = await dependencies.sync(directory);
132
+ context.write(context.redact(escapeDiagnostic(`Rule dependencies ready: ${prepared.directory}`)) + '\n');
133
+ return;
134
+ }
135
+ const prepared = await dependencies.change(directory, operation, packages);
136
+ if (operation === 'add') {
137
+ for (const specifier of packages) {
138
+ const name = npa(specifier).name;
139
+ context.write(context.redact(escapeDiagnostic(`Added ${name}@${prepared.directVersions[name]}`)) + '\n');
140
+ }
141
+ }
142
+ else {
143
+ for (const name of packages)
144
+ context.write(context.redact(escapeDiagnostic(`Removed ${name}`)) + '\n');
145
+ }
146
+ context.write(context.redact(escapeDiagnostic(join(prepared.directory, 'coderuler.lock'))) + '\n');
147
+ };
148
+ return { manageDependencies };
149
+ },
150
+ },
151
+ };
152
+ return services;
153
+ }
154
+ export async function runCli(argv) {
155
+ const cwd = process.cwd();
156
+ try {
157
+ const versionOnly = argv.length === 1 && (argv[0] === '--version' || argv[0] === '-V');
158
+ if (!versionOnly)
159
+ loadEnvironment(cwd);
160
+ }
161
+ catch (error) {
162
+ const message = error instanceof Error ? error.message : String(error);
163
+ const redact = createRedactor([process.env.TYPESAFE_API_KEY, process.env.GITHUB_TOKEN]);
164
+ process.stderr.write(`coderuler: ${redact(escapeDiagnostic(message))}\n`);
165
+ return EXIT_FAILURE;
166
+ }
167
+ return runProgram(argv, createServices(cwd, process.env));
168
+ }
@@ -0,0 +1,5 @@
1
+ import type { DecisionRuntime } from '../app/ports.js';
2
+ import type { RuleSettings, TokenPricing } from '../app/rule.js';
3
+ type Env = Readonly<Record<string, string | undefined>>;
4
+ export declare function createDecisionResolver(env: Env, thresholdOverride?: number, pricingOverrides?: Partial<TokenPricing>): (settings: RuleSettings) => DecisionRuntime;
5
+ export {};