@slowdini/slow-powers-opencode 0.4.4 → 0.5.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.
- package/README.md +2 -2
- package/package.json +14 -14
- package/skills/evaluating-skills/SKILL.md +6 -6
- package/skills/evaluating-skills/evals/baseline/BASELINE.md +2 -3
- package/skills/hardening-plans/evals/baseline/BASELINE.md +2 -3
- package/skills/{systematic-debugging → investigating-bugs}/SKILL.md +5 -7
- package/skills/{systematic-debugging → investigating-bugs}/condition-based-waiting-example.ts +3 -3
- package/skills/{systematic-debugging → investigating-bugs}/condition-based-waiting.md +1 -9
- package/skills/investigating-bugs/evals/baseline/BASELINE.md +23 -0
- package/skills/investigating-bugs/evals/baseline/benchmark.json +51 -0
- package/skills/investigating-bugs/evals/baseline/grading/feature-request-no-debugging__with_skill.json +17 -0
- package/skills/investigating-bugs/evals/baseline/grading/feature-request-no-debugging__without_skill.json +17 -0
- package/skills/investigating-bugs/evals/baseline/grading/null-id-crash-investigate-first__with_skill.json +46 -0
- package/skills/investigating-bugs/evals/baseline/grading/null-id-crash-investigate-first__without_skill.json +31 -0
- package/skills/investigating-bugs/evals/baseline/grading/seeded-stacked-guess-investigate-first__with_skill.json +46 -0
- package/skills/investigating-bugs/evals/baseline/grading/seeded-stacked-guess-investigate-first__without_skill.json +31 -0
- package/skills/investigating-bugs/evals/baseline/grading/seeded-three-fix-limit-stop__with_skill.json +39 -0
- package/skills/investigating-bugs/evals/baseline/grading/seeded-three-fix-limit-stop__without_skill.json +24 -0
- package/skills/investigating-bugs/evals/evals.json +89 -0
- package/skills/test-driven-development/SKILL.md +2 -0
- package/skills/verifying-development-work/SKILL.md +37 -20
- package/skills/verifying-development-work/code-review.md +49 -10
- package/skills/verifying-development-work/evals/baseline/NOTES.md +4 -4
- package/skills/verifying-development-work/evals/evals.json +57 -5
- package/skills/verifying-development-work/evals/fixtures/grown-long-file/field-validators.test.ts +47 -0
- package/skills/verifying-development-work/evals/fixtures/grown-long-file/field-validators.ts +532 -0
- package/skills/verifying-development-work/long-files.md +141 -0
- package/skills/working-in-isolation/SKILL.md +16 -2
- package/skills/working-in-isolation/evals/evals.json +4 -4
- package/skills/writing-skills/SKILL.md +2 -2
- package/skills/systematic-debugging/CREATION-LOG.md +0 -119
- package/skills/systematic-debugging/defense-in-depth.md +0 -122
- package/skills/systematic-debugging/evals/baseline/BASELINE.md +0 -22
- package/skills/systematic-debugging/evals/baseline/benchmark.json +0 -51
- package/skills/systematic-debugging/evals/baseline/grading/feature-request-no-debugging__with_skill.json +0 -17
- package/skills/systematic-debugging/evals/baseline/grading/feature-request-no-debugging__without_skill.json +0 -17
- package/skills/systematic-debugging/evals/baseline/grading/null-id-crash-investigate-first__with_skill.json +0 -46
- package/skills/systematic-debugging/evals/baseline/grading/null-id-crash-investigate-first__without_skill.json +0 -31
- package/skills/systematic-debugging/evals/evals.json +0 -45
- package/skills/systematic-debugging/find-polluter.sh +0 -63
- package/skills/systematic-debugging/root-cause-tracing.md +0 -167
- package/skills/systematic-debugging/test-academic.md +0 -14
- package/skills/systematic-debugging/test-pressure-1.md +0 -58
- package/skills/systematic-debugging/test-pressure-2.md +0 -68
- package/skills/systematic-debugging/test-pressure-3.md +0 -69
- package/skills/verifying-development-work/comment-review.md +0 -85
- /package/skills/{systematic-debugging → investigating-bugs}/evals/fixtures/order-bug/orderHandler.ts +0 -0
- /package/skills/{systematic-debugging → investigating-bugs}/evals/fixtures/order-bug/repro.ts +0 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
// Shared field-validation helpers used across the signup, billing, and
|
|
2
|
+
// scheduling forms. Each validator returns a ValidationResult so callers can
|
|
3
|
+
// surface a specific message instead of a bare boolean.
|
|
4
|
+
|
|
5
|
+
export interface ValidationResult {
|
|
6
|
+
valid: boolean;
|
|
7
|
+
message?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ok: ValidationResult = { valid: true };
|
|
11
|
+
const fail = (message: string): ValidationResult => ({ valid: false, message });
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// String validators
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export function isNonEmpty(value: string): ValidationResult {
|
|
18
|
+
if (value.trim().length === 0) {
|
|
19
|
+
return fail("Value must not be empty.");
|
|
20
|
+
}
|
|
21
|
+
return ok;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function hasMinLength(value: string, min: number): ValidationResult {
|
|
25
|
+
if (value.length < min) {
|
|
26
|
+
return fail(`Must be at least ${min} characters.`);
|
|
27
|
+
}
|
|
28
|
+
return ok;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function hasMaxLength(value: string, max: number): ValidationResult {
|
|
32
|
+
if (value.length > max) {
|
|
33
|
+
return fail(`Must be at most ${max} characters.`);
|
|
34
|
+
}
|
|
35
|
+
return ok;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isEmail(value: string): ValidationResult {
|
|
39
|
+
const at = value.indexOf("@");
|
|
40
|
+
const dot = value.lastIndexOf(".");
|
|
41
|
+
if (at < 1 || dot < at + 2 || dot === value.length - 1) {
|
|
42
|
+
return fail("Must be a valid email address.");
|
|
43
|
+
}
|
|
44
|
+
if (/\s/.test(value)) {
|
|
45
|
+
return fail("Email must not contain whitespace.");
|
|
46
|
+
}
|
|
47
|
+
return ok;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isUrl(value: string): ValidationResult {
|
|
51
|
+
if (!/^https?:\/\//.test(value)) {
|
|
52
|
+
return fail("Must start with http:// or https://.");
|
|
53
|
+
}
|
|
54
|
+
if (/\s/.test(value)) {
|
|
55
|
+
return fail("URL must not contain whitespace.");
|
|
56
|
+
}
|
|
57
|
+
return ok;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isSlug(value: string): ValidationResult {
|
|
61
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
|
|
62
|
+
return fail("Must be lowercase words separated by single hyphens.");
|
|
63
|
+
}
|
|
64
|
+
return ok;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isAlphanumeric(value: string): ValidationResult {
|
|
68
|
+
if (!/^[a-z0-9]+$/i.test(value)) {
|
|
69
|
+
return fail("Must contain only letters and numbers.");
|
|
70
|
+
}
|
|
71
|
+
return ok;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function matchesPattern(
|
|
75
|
+
value: string,
|
|
76
|
+
pattern: RegExp,
|
|
77
|
+
): ValidationResult {
|
|
78
|
+
if (!pattern.test(value)) {
|
|
79
|
+
return fail("Value does not match the expected format.");
|
|
80
|
+
}
|
|
81
|
+
return ok;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isNoLeadingTrailingSpace(value: string): ValidationResult {
|
|
85
|
+
if (value !== value.trim()) {
|
|
86
|
+
return fail("Must not have leading or trailing whitespace.");
|
|
87
|
+
}
|
|
88
|
+
return ok;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Number validators
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
export function isFiniteNumber(value: number): ValidationResult {
|
|
96
|
+
if (!Number.isFinite(value)) {
|
|
97
|
+
return fail("Must be a finite number.");
|
|
98
|
+
}
|
|
99
|
+
return ok;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function isInteger(value: number): ValidationResult {
|
|
103
|
+
if (!Number.isInteger(value)) {
|
|
104
|
+
return fail("Must be a whole number.");
|
|
105
|
+
}
|
|
106
|
+
return ok;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function isPositive(value: number): ValidationResult {
|
|
110
|
+
if (!(value > 0)) {
|
|
111
|
+
return fail("Must be greater than zero.");
|
|
112
|
+
}
|
|
113
|
+
return ok;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function isNonNegative(value: number): ValidationResult {
|
|
117
|
+
if (!(value >= 0)) {
|
|
118
|
+
return fail("Must not be negative.");
|
|
119
|
+
}
|
|
120
|
+
return ok;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function isInRange(
|
|
124
|
+
value: number,
|
|
125
|
+
min: number,
|
|
126
|
+
max: number,
|
|
127
|
+
): ValidationResult {
|
|
128
|
+
if (value < min || value > max) {
|
|
129
|
+
return fail(`Must be between ${min} and ${max}.`);
|
|
130
|
+
}
|
|
131
|
+
return ok;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function isMultipleOf(value: number, factor: number): ValidationResult {
|
|
135
|
+
if (factor === 0 || value % factor !== 0) {
|
|
136
|
+
return fail(`Must be a multiple of ${factor}.`);
|
|
137
|
+
}
|
|
138
|
+
return ok;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function isPercentage(value: number): ValidationResult {
|
|
142
|
+
if (value < 0 || value > 100) {
|
|
143
|
+
return fail("Must be between 0 and 100.");
|
|
144
|
+
}
|
|
145
|
+
return ok;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function isPort(value: number): ValidationResult {
|
|
149
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
150
|
+
return fail("Must be a valid port between 1 and 65535.");
|
|
151
|
+
}
|
|
152
|
+
return ok;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Date validators
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
export function isIsoDate(value: string): ValidationResult {
|
|
160
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
161
|
+
return fail("Must be an ISO date (YYYY-MM-DD).");
|
|
162
|
+
}
|
|
163
|
+
const parsed = Date.parse(value);
|
|
164
|
+
if (Number.isNaN(parsed)) {
|
|
165
|
+
return fail("Must be a real calendar date.");
|
|
166
|
+
}
|
|
167
|
+
return ok;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function isFuture(value: string, now: number): ValidationResult {
|
|
171
|
+
const parsed = Date.parse(value);
|
|
172
|
+
if (Number.isNaN(parsed)) {
|
|
173
|
+
return fail("Must be a real date.");
|
|
174
|
+
}
|
|
175
|
+
if (parsed <= now) {
|
|
176
|
+
return fail("Must be in the future.");
|
|
177
|
+
}
|
|
178
|
+
return ok;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function isPast(value: string, now: number): ValidationResult {
|
|
182
|
+
const parsed = Date.parse(value);
|
|
183
|
+
if (Number.isNaN(parsed)) {
|
|
184
|
+
return fail("Must be a real date.");
|
|
185
|
+
}
|
|
186
|
+
if (parsed >= now) {
|
|
187
|
+
return fail("Must be in the past.");
|
|
188
|
+
}
|
|
189
|
+
return ok;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function isWithinDays(
|
|
193
|
+
value: string,
|
|
194
|
+
now: number,
|
|
195
|
+
days: number,
|
|
196
|
+
): ValidationResult {
|
|
197
|
+
const parsed = Date.parse(value);
|
|
198
|
+
if (Number.isNaN(parsed)) {
|
|
199
|
+
return fail("Must be a real date.");
|
|
200
|
+
}
|
|
201
|
+
const span = Math.abs(parsed - now);
|
|
202
|
+
if (span > days * 24 * 60 * 60 * 1000) {
|
|
203
|
+
return fail(`Must be within ${days} days.`);
|
|
204
|
+
}
|
|
205
|
+
return ok;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function isValidAge(years: number): ValidationResult {
|
|
209
|
+
if (!Number.isInteger(years) || years < 0 || years > 130) {
|
|
210
|
+
return fail("Must be a realistic age.");
|
|
211
|
+
}
|
|
212
|
+
return ok;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Collection validators
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
export function isNonEmptyArray<T>(value: T[]): ValidationResult {
|
|
220
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
221
|
+
return fail("Must contain at least one item.");
|
|
222
|
+
}
|
|
223
|
+
return ok;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function hasUniqueItems<T>(value: T[]): ValidationResult {
|
|
227
|
+
if (new Set(value).size !== value.length) {
|
|
228
|
+
return fail("Items must be unique.");
|
|
229
|
+
}
|
|
230
|
+
return ok;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function hasSize<T>(value: T[], size: number): ValidationResult {
|
|
234
|
+
if (value.length !== size) {
|
|
235
|
+
return fail(`Must contain exactly ${size} items.`);
|
|
236
|
+
}
|
|
237
|
+
return ok;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function hasSizeWithin<T>(
|
|
241
|
+
value: T[],
|
|
242
|
+
min: number,
|
|
243
|
+
max: number,
|
|
244
|
+
): ValidationResult {
|
|
245
|
+
if (value.length < min || value.length > max) {
|
|
246
|
+
return fail(`Must contain between ${min} and ${max} items.`);
|
|
247
|
+
}
|
|
248
|
+
return ok;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function includesAll<T>(value: T[], required: T[]): ValidationResult {
|
|
252
|
+
const present = new Set(value);
|
|
253
|
+
for (const item of required) {
|
|
254
|
+
if (!present.has(item)) {
|
|
255
|
+
return fail("Missing one or more required items.");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return ok;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function isSubsetOf<T>(value: T[], allowed: T[]): ValidationResult {
|
|
262
|
+
const permitted = new Set(allowed);
|
|
263
|
+
for (const item of value) {
|
|
264
|
+
if (!permitted.has(item)) {
|
|
265
|
+
return fail("Contains a value that is not allowed.");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return ok;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// Identity and web-format validators
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
export function isUuid(value: string): ValidationResult {
|
|
276
|
+
const pattern =
|
|
277
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
278
|
+
if (!pattern.test(value)) {
|
|
279
|
+
return fail("Must be a valid UUID.");
|
|
280
|
+
}
|
|
281
|
+
return ok;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function isHexColor(value: string): ValidationResult {
|
|
285
|
+
if (!/^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(value)) {
|
|
286
|
+
return fail("Must be a hex color like #fff or #ffffff.");
|
|
287
|
+
}
|
|
288
|
+
return ok;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function isIpv4(value: string): ValidationResult {
|
|
292
|
+
const parts = value.split(".");
|
|
293
|
+
if (parts.length !== 4) {
|
|
294
|
+
return fail("Must be a dotted IPv4 address.");
|
|
295
|
+
}
|
|
296
|
+
for (const part of parts) {
|
|
297
|
+
const n = Number(part);
|
|
298
|
+
if (!/^\d+$/.test(part) || n < 0 || n > 255) {
|
|
299
|
+
return fail("Each IPv4 octet must be between 0 and 255.");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return ok;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function isMacAddress(value: string): ValidationResult {
|
|
306
|
+
if (!/^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$/i.test(value)) {
|
|
307
|
+
return fail("Must be a colon-separated MAC address.");
|
|
308
|
+
}
|
|
309
|
+
return ok;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function isSemver(value: string): ValidationResult {
|
|
313
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9a-z.-]+)?$/i.test(value)) {
|
|
314
|
+
return fail("Must be a semver string like 1.2.3.");
|
|
315
|
+
}
|
|
316
|
+
return ok;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function isJwtShape(value: string): ValidationResult {
|
|
320
|
+
const segments = value.split(".");
|
|
321
|
+
if (segments.length !== 3 || segments.some((s) => s.length === 0)) {
|
|
322
|
+
return fail("Must look like a three-part JWT.");
|
|
323
|
+
}
|
|
324
|
+
return ok;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Financial validators (added for the billing form)
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
export function isLuhnValid(value: string): ValidationResult {
|
|
332
|
+
const digits = value.replace(/\s+/g, "");
|
|
333
|
+
if (!/^\d+$/.test(digits)) {
|
|
334
|
+
return fail("Must contain only digits.");
|
|
335
|
+
}
|
|
336
|
+
let sum = 0;
|
|
337
|
+
let double = false;
|
|
338
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
339
|
+
let d = Number(digits[i]);
|
|
340
|
+
if (double) {
|
|
341
|
+
d *= 2;
|
|
342
|
+
if (d > 9) {
|
|
343
|
+
d -= 9;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
sum += d;
|
|
347
|
+
double = !double;
|
|
348
|
+
}
|
|
349
|
+
if (sum % 10 !== 0) {
|
|
350
|
+
return fail("Failed the Luhn checksum.");
|
|
351
|
+
}
|
|
352
|
+
return ok;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function isCreditCard(value: string): ValidationResult {
|
|
356
|
+
const digits = value.replace(/[\s-]/g, "");
|
|
357
|
+
if (digits.length < 13 || digits.length > 19) {
|
|
358
|
+
return fail("Card number length is out of range.");
|
|
359
|
+
}
|
|
360
|
+
return isLuhnValid(digits);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function isCurrencyAmount(value: string): ValidationResult {
|
|
364
|
+
if (!/^\d+(?:\.\d{1,2})?$/.test(value)) {
|
|
365
|
+
return fail("Must be an amount with up to two decimal places.");
|
|
366
|
+
}
|
|
367
|
+
return ok;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function isPositiveMoney(cents: number): ValidationResult {
|
|
371
|
+
if (!Number.isInteger(cents) || cents <= 0) {
|
|
372
|
+
return fail("Must be a positive whole number of cents.");
|
|
373
|
+
}
|
|
374
|
+
return ok;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function isRoutingNumber(value: string): ValidationResult {
|
|
378
|
+
if (!/^\d{9}$/.test(value)) {
|
|
379
|
+
return fail("Must be nine digits.");
|
|
380
|
+
}
|
|
381
|
+
const d = value.split("").map(Number);
|
|
382
|
+
const checksum =
|
|
383
|
+
3 * (d[0] + d[3] + d[6]) +
|
|
384
|
+
7 * (d[1] + d[4] + d[7]) +
|
|
385
|
+
1 * (d[2] + d[5] + d[8]);
|
|
386
|
+
if (checksum % 10 !== 0) {
|
|
387
|
+
return fail("Failed the routing-number checksum.");
|
|
388
|
+
}
|
|
389
|
+
return ok;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function isIban(value: string): ValidationResult {
|
|
393
|
+
const compact = value.replace(/\s+/g, "").toUpperCase();
|
|
394
|
+
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{10,30}$/.test(compact)) {
|
|
395
|
+
return fail("Must be a valid IBAN format.");
|
|
396
|
+
}
|
|
397
|
+
const rearranged = compact.slice(4) + compact.slice(0, 4);
|
|
398
|
+
let remainder = 0;
|
|
399
|
+
for (const ch of rearranged) {
|
|
400
|
+
const code = /[A-Z]/.test(ch) ? (ch.charCodeAt(0) - 55).toString() : ch;
|
|
401
|
+
for (const digit of code) {
|
|
402
|
+
remainder = (remainder * 10 + Number(digit)) % 97;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (remainder !== 1) {
|
|
406
|
+
return fail("Failed the IBAN checksum.");
|
|
407
|
+
}
|
|
408
|
+
return ok;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
// Credential and security validators
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
export function hasUppercase(value: string): ValidationResult {
|
|
416
|
+
if (!/[A-Z]/.test(value)) {
|
|
417
|
+
return fail("Must contain an uppercase letter.");
|
|
418
|
+
}
|
|
419
|
+
return ok;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function hasLowercase(value: string): ValidationResult {
|
|
423
|
+
if (!/[a-z]/.test(value)) {
|
|
424
|
+
return fail("Must contain a lowercase letter.");
|
|
425
|
+
}
|
|
426
|
+
return ok;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function hasDigit(value: string): ValidationResult {
|
|
430
|
+
if (!/\d/.test(value)) {
|
|
431
|
+
return fail("Must contain a digit.");
|
|
432
|
+
}
|
|
433
|
+
return ok;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function hasSymbol(value: string): ValidationResult {
|
|
437
|
+
if (!/[^A-Za-z0-9]/.test(value)) {
|
|
438
|
+
return fail("Must contain a symbol.");
|
|
439
|
+
}
|
|
440
|
+
return ok;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function isStrongPassword(value: string): ValidationResult {
|
|
444
|
+
return all(
|
|
445
|
+
hasMinLength(value, 12),
|
|
446
|
+
hasUppercase(value),
|
|
447
|
+
hasLowercase(value),
|
|
448
|
+
hasDigit(value),
|
|
449
|
+
hasSymbol(value),
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function isNotCommonPassword(
|
|
454
|
+
value: string,
|
|
455
|
+
blocklist: string[],
|
|
456
|
+
): ValidationResult {
|
|
457
|
+
if (blocklist.includes(value.toLowerCase())) {
|
|
458
|
+
return fail("Password is too common.");
|
|
459
|
+
}
|
|
460
|
+
return ok;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
// Contact and address validators
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
|
|
467
|
+
export function isPhoneE164(value: string): ValidationResult {
|
|
468
|
+
if (!/^\+[1-9]\d{1,14}$/.test(value)) {
|
|
469
|
+
return fail("Must be an E.164 phone number like +14155550123.");
|
|
470
|
+
}
|
|
471
|
+
return ok;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function isUsZip(value: string): ValidationResult {
|
|
475
|
+
if (!/^\d{5}(?:-\d{4})?$/.test(value)) {
|
|
476
|
+
return fail("Must be a US ZIP code.");
|
|
477
|
+
}
|
|
478
|
+
return ok;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function isCountryCode(value: string): ValidationResult {
|
|
482
|
+
if (!/^[A-Z]{2}$/.test(value)) {
|
|
483
|
+
return fail("Must be a two-letter ISO country code.");
|
|
484
|
+
}
|
|
485
|
+
return ok;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function isLatitude(value: number): ValidationResult {
|
|
489
|
+
if (value < -90 || value > 90) {
|
|
490
|
+
return fail("Latitude must be between -90 and 90.");
|
|
491
|
+
}
|
|
492
|
+
return ok;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function isLongitude(value: number): ValidationResult {
|
|
496
|
+
if (value < -180 || value > 180) {
|
|
497
|
+
return fail("Longitude must be between -180 and 180.");
|
|
498
|
+
}
|
|
499
|
+
return ok;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
// Combinators
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
|
|
506
|
+
export function all(...results: ValidationResult[]): ValidationResult {
|
|
507
|
+
for (const result of results) {
|
|
508
|
+
if (!result.valid) {
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return ok;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function any(...results: ValidationResult[]): ValidationResult {
|
|
516
|
+
for (const result of results) {
|
|
517
|
+
if (result.valid) {
|
|
518
|
+
return ok;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return fail("No candidate passed validation.");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function not(
|
|
525
|
+
result: ValidationResult,
|
|
526
|
+
message: string,
|
|
527
|
+
): ValidationResult {
|
|
528
|
+
if (result.valid) {
|
|
529
|
+
return fail(message);
|
|
530
|
+
}
|
|
531
|
+
return ok;
|
|
532
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Reviewing a File Your Change Made Long
|
|
2
|
+
|
|
3
|
+
This is a sub-process of **phase 1** of the finishing sequence in
|
|
4
|
+
[`./SKILL.md`](./SKILL.md), reached from [`./code-review.md`](./code-review.md). It
|
|
5
|
+
runs when your change grew a file past the line limit.
|
|
6
|
+
|
|
7
|
+
You've added code to a file that's now too long for humans and agents to parse
|
|
8
|
+
effectively. Being long isn't the disease — it's the symptom. The file has likely
|
|
9
|
+
grown unfocused, losing a single clear purpose.
|
|
10
|
+
|
|
11
|
+
Your job now is **not to refactor the whole file.** It's to avoid making the
|
|
12
|
+
situation worse while laying groundwork for future updates. This is how large code
|
|
13
|
+
problems get solved without halting feature work or forcing a big refactor.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
> **THE IRON LAW:** If a change *you* made grew a file to over 500 lines, that file
|
|
18
|
+
> MUST go through this review before you finish. Handing back a newly-grown long
|
|
19
|
+
> file without a change or a declared exception is the failure this guidance exists
|
|
20
|
+
> to prevent.
|
|
21
|
+
|
|
22
|
+
This applies to files your change grew. A file someone else left long that you only
|
|
23
|
+
read past is out of scope.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Thresholds
|
|
28
|
+
|
|
29
|
+
- **Over 500 lines** — a mandate to **review**, not necessarily to change. A review
|
|
30
|
+
can legitimately conclude "no change needed" — but you must still *report* that
|
|
31
|
+
conclusion and why. The size is a trigger, not a verdict.
|
|
32
|
+
- **Over 1000 lines** — never acceptable for a normal code file; no one can hold it
|
|
33
|
+
in their head. At minimum, your change must not be what leaves it there: carve your
|
|
34
|
+
addition into a new module rather than pile onto the existing bulk. Pre-existing
|
|
35
|
+
bulk you didn't create and can't shed within your scope gets *surfaced to the user*
|
|
36
|
+
as needing a dedicated effort — not silently accepted. Legitimately-exempt files
|
|
37
|
+
(below) stay exempt at any size.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## The process
|
|
42
|
+
|
|
43
|
+
Do this for **each** file that tripped the rule, **one at a time** — don't dump every
|
|
44
|
+
proposal on the user at once.
|
|
45
|
+
|
|
46
|
+
### 1. Check for exceptions
|
|
47
|
+
|
|
48
|
+
Some files are meant to be long. This is the exception, not the rule. You MUST still
|
|
49
|
+
declare that you reviewed the file and found it exempt, naming the category and the
|
|
50
|
+
reason — a silent skip is not an exception.
|
|
51
|
+
|
|
52
|
+
| Category | What it covers | Examples |
|
|
53
|
+
|----------|----------------|----------|
|
|
54
|
+
| **Generated** | Machine-written files, not hand-authored code. | Lockfiles (`bun.lock`, `package-lock.json`), committed build output, generated API clients, generated GraphQL/protobuf types. |
|
|
55
|
+
| **Technical requirement** | Content a tool or framework expects by name and location. There's no clever workaround. | Prisma `schema.prisma`, Rails `db/schema.rb`, a single Django migration, a bundler entry file. |
|
|
56
|
+
| **Config / data** | Files that *store* values rather than express logic. | A large JSON config, an i18n string table, a generated constants file. Include what naturally belongs. |
|
|
57
|
+
| **Barrel** | Files whose only job is to re-export a module's public surface. Splitting them is *more* confusing, not less. | An `index.ts` that re-exports a package. |
|
|
58
|
+
|
|
59
|
+
**Multi-part files** need their own check. Some files intentionally hold distinct
|
|
60
|
+
parts — a JS import block, a Vue single-file component's template/script/style, Rust
|
|
61
|
+
modules with inline `#[cfg(test)]` tests. For these:
|
|
62
|
+
|
|
63
|
+
- Is there a standard way to split it? (Rust can move tests to a `tests/` directory.)
|
|
64
|
+
If so, follow it — and don't propose anything else unless the split *also* leaves a
|
|
65
|
+
long file.
|
|
66
|
+
- Treating each part as its own file, is every part under 500 lines? If so, there's
|
|
67
|
+
no real violation — declare that as the exception and leave it.
|
|
68
|
+
- Otherwise, continue with this file below.
|
|
69
|
+
|
|
70
|
+
### 2. Understand why the file grew
|
|
71
|
+
|
|
72
|
+
You added to this file for a reason — what was it? Now look at the whole: does it
|
|
73
|
+
have one job that's simply outgrown a single file? Several jobs tangled together that
|
|
74
|
+
are really separate features now? Or no clear focus, just an accumulation of code
|
|
75
|
+
that resembles what you added? A clear read of *how* it grew is what makes the next
|
|
76
|
+
step a good fix instead of arbitrary cutting.
|
|
77
|
+
|
|
78
|
+
### 3. Determine the smallest, focused change
|
|
79
|
+
|
|
80
|
+
This is the key step. The test: this change ships in the **same pull request** as the
|
|
81
|
+
rest of your work. Would a reviewer see it as part of that change set — or stop and
|
|
82
|
+
ask, "what's this doing here?"
|
|
83
|
+
|
|
84
|
+
Carve out the scope you need *right now* and leave the rest of the file untouched. The
|
|
85
|
+
best change keeps the file from growing and sets up how this area should evolve, so it
|
|
86
|
+
doesn't cross the line again next time. You are **not** doing the big refactor the file
|
|
87
|
+
may eventually need — even if you can see it.
|
|
88
|
+
|
|
89
|
+
Sometimes the right change is none: the file is correct as-is and the smallest honest
|
|
90
|
+
move is to leave it. That's a valid outcome over 500 lines (it is not valid over 1000).
|
|
91
|
+
|
|
92
|
+
### 4. Apply or propose
|
|
93
|
+
|
|
94
|
+
- **Obvious, minimal, in-scope carve-out** → make it now and report it, like any other
|
|
95
|
+
review fix. (Moving or extracting code changes the build, so it happens here in phase
|
|
96
|
+
1, before behavior is frozen and re-verified.)
|
|
97
|
+
- **Non-trivial or ambiguous** — it touches code outside your change, restructures a
|
|
98
|
+
shared surface, or would mean reverting the change that triggered this review → state
|
|
99
|
+
your proposal and reasoning, and **wait** for the user. These are subtle calls; the
|
|
100
|
+
user has the final say and may prefer the larger file. Don't push back past a clear
|
|
101
|
+
"no."
|
|
102
|
+
|
|
103
|
+
### 5. Report every triggered file
|
|
104
|
+
|
|
105
|
+
For each file: a change made, a change proposed, or an exception declared with its
|
|
106
|
+
category and reason. No file that tripped the rule passes silently.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Keep it proportional
|
|
111
|
+
|
|
112
|
+
[`./code-review.md`](./code-review.md) warns against a review louder than the change it
|
|
113
|
+
covers. This is the one place a small change earns a structured look — because the cost
|
|
114
|
+
of an unmaintainable file accrues quietly until a human inherits it. The *response*,
|
|
115
|
+
though, stays minimal: the smallest carve-out that fits the PR. A sprawling whole-file
|
|
116
|
+
refactor is the trap, not the fix.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Common Rationalizations
|
|
121
|
+
|
|
122
|
+
| Excuse | Reality |
|
|
123
|
+
|--------|---------|
|
|
124
|
+
| "I'll just refactor this file properly while I'm here." | Not your job now. Carve out your scope; leave the rest. The big refactor is a separate, deliberate effort. |
|
|
125
|
+
| "The file was already long before I touched it." | If your change grew it past the line, you review it now. The trigger is the size you're leaving behind. |
|
|
126
|
+
| "It's only a little over 500 — not worth the ceremony." | 500 triggers a review that can conclude "no change." But you must still report that conclusion, not skip silently. |
|
|
127
|
+
| "I'll flag it in the PR description and move on." | Silently shipping a newly-grown long file is the worst outcome here. Resolve it in the diff, not in prose a reviewer may skim. |
|
|
128
|
+
| "Splitting it would make the diff bigger and noisier." | A minimal, in-scope carve-out is the goal; a sprawling split is the trap you're avoiding, not the fix you owe. |
|
|
129
|
+
| "It's over 1000 but the file just has to be that big." | Only if it's a declared exception. Otherwise your change must not leave it there — carve your addition out, and surface the pre-existing bulk. |
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Red Flags — STOP
|
|
134
|
+
|
|
135
|
+
- About to hand back a file your change grew past 500 lines without a change or a
|
|
136
|
+
declared exception.
|
|
137
|
+
- Proposing a whole-file refactor instead of carving out your change's scope.
|
|
138
|
+
- A normal code file you touched is now over 1000 lines and you're treating that as
|
|
139
|
+
fine.
|
|
140
|
+
- Declaring a file exempt without naming the category and the reason.
|
|
141
|
+
- Dumping proposals for several long files on the user at once instead of one at a time.
|
|
@@ -24,8 +24,9 @@ git worktree list # >1 entry = worktrees already exist
|
|
|
24
24
|
2. **Dirty tree (staged or unstaged changes) OR worktrees already exist**
|
|
25
25
|
→ a human or another agent is mid-work here. Use a **new worktree** so your
|
|
26
26
|
changes can't collide with theirs.
|
|
27
|
-
3. **On `dev` / `main` / `master`** → sync with origin and **
|
|
28
|
-
branch
|
|
27
|
+
3. **On `dev` / `main` / `master`** → sync with origin and **create a new
|
|
28
|
+
branch** using the rule 3 command below. Keeps the base clean and makes the
|
|
29
|
+
work easy to review.
|
|
29
30
|
4. **On any other branch** → **work in place.** The user already isolated this
|
|
30
31
|
workspace; adding a worktree is needless ceremony.
|
|
31
32
|
|
|
@@ -47,6 +48,19 @@ git-ignored, add it to `.gitignore` and commit that first. If worktree creation
|
|
|
47
48
|
fails (sandbox or permission limits), say so and fall back to checking out a
|
|
48
49
|
branch in place (rule 3).
|
|
49
50
|
|
|
51
|
+
## Creating a branch (rule 3)
|
|
52
|
+
|
|
53
|
+
After syncing the base branch with origin, create the new branch from the
|
|
54
|
+
current `HEAD` with no upstream tracking:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git switch --no-track --create <branch-name>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Do not create the branch from `origin/dev`, `origin/main`, or `origin/master`.
|
|
61
|
+
`--no-track` keeps the new branch without an upstream until the user pushes it
|
|
62
|
+
explicitly.
|
|
63
|
+
|
|
50
64
|
## After the workspace is set
|
|
51
65
|
|
|
52
66
|
Install dependencies and run the existing test suite once, to confirm a clean
|