pi-gitnexus-fork 0.7.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 (117) hide show
  1. package/.github/workflows/ci.yml +20 -0
  2. package/.gitnexusignore +11 -0
  3. package/.sg-rules/async-function-must-await-or-return.yml +55 -0
  4. package/.sg-rules/catch-must-log-error.yml +78 -0
  5. package/.sg-rules/class-must-implement-or-extend.yml +61 -0
  6. package/.sg-rules/class-property-must-be-readonly.yml +61 -0
  7. package/.sg-rules/error-must-extend-base.yml +56 -0
  8. package/.sg-rules/generic-must-be-constrained.yml +60 -0
  9. package/.sg-rules/import-reexport-risk.yml +9 -0
  10. package/.sg-rules/missing-session-id-in-api.yml +16 -0
  11. package/.sg-rules/no-any-in-generic-args.yml +57 -0
  12. package/.sg-rules/no-await-in-promise-all.yml +28 -0
  13. package/.sg-rules/no-barrel-export.yml +17 -0
  14. package/.sg-rules/no-bq-write-in-module.yml +65 -0
  15. package/.sg-rules/no-console-except-error.yml +27 -0
  16. package/.sg-rules/no-console-in-server.yml +42 -0
  17. package/.sg-rules/no-empty-catch.yml +20 -0
  18. package/.sg-rules/no-empty-function.yml +24 -0
  19. package/.sg-rules/no-eval.yml +28 -0
  20. package/.sg-rules/no-explicit-any.yml +34 -0
  21. package/.sg-rules/no-hardcoded-placeholder-string.yml +23 -0
  22. package/.sg-rules/no-hardcoded-secrets.yml +32 -0
  23. package/.sg-rules/no-innerHTML.yml +22 -0
  24. package/.sg-rules/no-json-parse-without-trycatch.yml +33 -0
  25. package/.sg-rules/no-magic-numbers.yml +25 -0
  26. package/.sg-rules/no-nested-ternary.yml +21 -0
  27. package/.sg-rules/no-non-null-assertion.yml +25 -0
  28. package/.sg-rules/no-stub-implementation.yml +44 -0
  29. package/.sg-rules/no-throw-literal.yml +50 -0
  30. package/.sg-rules/no-todo-comment.yml +24 -0
  31. package/.sg-rules/no-ts-ignore-comment.yml +48 -0
  32. package/.sg-rules/no-type-assertion-in-jsx.yml +23 -0
  33. package/.sg-rules/no-unguarded-trim.yml +24 -0
  34. package/.sg-rules/no-unknown-without-narrowing.yml +76 -0
  35. package/.sg-rules/no-unsafe-bracket-access.yml +58 -0
  36. package/.sg-rules/no-unsafe-type-assertion.yml +45 -0
  37. package/.sg-rules/switch-must-be-exhaustive.yml +62 -0
  38. package/.sg-rules/zod-async-refine-without-abort.yml +62 -0
  39. package/.sg-rules/zod-enum-unsafe-access.yml +59 -0
  40. package/.sg-rules/zod-nested-object-deep-path.yml +70 -0
  41. package/.sg-rules/zod-optional-without-default-in-route.yml +50 -0
  42. package/.sg-rules/zod-parse-not-safe.yml +42 -0
  43. package/.sg-rules/zod-preprocess-without-fallback.yml +58 -0
  44. package/.sg-rules/zod-refine-no-return-undefined.yml +54 -0
  45. package/.sg-rules/zod-transform-without-output-type.yml +52 -0
  46. package/.sg-sha +1 -0
  47. package/.sgignore +4 -0
  48. package/AGENTS.md +1 -0
  49. package/CHANGELOG.md +99 -0
  50. package/LICENSE +21 -0
  51. package/README.md +113 -0
  52. package/biome.json +25 -0
  53. package/coverage/base.css +224 -0
  54. package/coverage/block-navigation.js +87 -0
  55. package/coverage/clover.xml +890 -0
  56. package/coverage/coverage-final.json +12 -0
  57. package/coverage/favicon.png +0 -0
  58. package/coverage/index.html +131 -0
  59. package/coverage/prettify.css +1 -0
  60. package/coverage/prettify.js +2 -0
  61. package/coverage/sort-arrow-sprite.png +0 -0
  62. package/coverage/sorter.js +210 -0
  63. package/coverage/src/augment-remote.ts.html +274 -0
  64. package/coverage/src/gitnexus.ts.html +1363 -0
  65. package/coverage/src/index.html +236 -0
  66. package/coverage/src/index.ts.html +1561 -0
  67. package/coverage/src/mcp-client-factory.ts.html +367 -0
  68. package/coverage/src/mcp-client-stdio.ts.html +736 -0
  69. package/coverage/src/mcp-client.ts.html +568 -0
  70. package/coverage/src/remote-mcp-client.ts.html +709 -0
  71. package/coverage/src/repo-resolver.ts.html +526 -0
  72. package/coverage/src/tools.ts.html +970 -0
  73. package/coverage/src/ui/index.html +131 -0
  74. package/coverage/src/ui/main-menu.ts.html +502 -0
  75. package/coverage/src/ui/settings-menu.ts.html +460 -0
  76. package/dist/augment-remote.d.ts +11 -0
  77. package/dist/augment-remote.js +55 -0
  78. package/dist/gitnexus.d.ts +103 -0
  79. package/dist/gitnexus.js +410 -0
  80. package/dist/index.d.ts +2 -0
  81. package/dist/index.js +479 -0
  82. package/dist/mcp-client-factory.d.ts +19 -0
  83. package/dist/mcp-client-factory.js +78 -0
  84. package/dist/mcp-client-stdio.d.ts +35 -0
  85. package/dist/mcp-client-stdio.js +186 -0
  86. package/dist/mcp-client.d.ts +45 -0
  87. package/dist/mcp-client.js +145 -0
  88. package/dist/remote-mcp-client.d.ts +43 -0
  89. package/dist/remote-mcp-client.js +181 -0
  90. package/dist/repo-resolver.d.ts +47 -0
  91. package/dist/repo-resolver.js +123 -0
  92. package/dist/tools.d.ts +6 -0
  93. package/dist/tools.js +230 -0
  94. package/dist/ui/main-menu.d.ts +33 -0
  95. package/dist/ui/main-menu.js +102 -0
  96. package/dist/ui/settings-menu.d.ts +16 -0
  97. package/dist/ui/settings-menu.js +95 -0
  98. package/docs/design/remote-mcp-backend.md +153 -0
  99. package/media/screenshot.png +0 -0
  100. package/package.json +61 -0
  101. package/sgconfig.yml +4 -0
  102. package/skills/gitnexus-debugging/SKILL.md +84 -0
  103. package/skills/gitnexus-exploring/SKILL.md +73 -0
  104. package/skills/gitnexus-impact-analysis/SKILL.md +93 -0
  105. package/skills/gitnexus-pr-review/SKILL.md +109 -0
  106. package/skills/gitnexus-refactoring/SKILL.md +85 -0
  107. package/src/augment-remote.ts +63 -0
  108. package/src/gitnexus.ts +426 -0
  109. package/src/index.ts +492 -0
  110. package/src/mcp-client-factory.ts +94 -0
  111. package/src/mcp-client-stdio.ts +217 -0
  112. package/src/mcp-client.ts +208 -0
  113. package/src/remote-mcp-client.ts +250 -0
  114. package/src/repo-resolver.ts +147 -0
  115. package/src/tools.ts +295 -0
  116. package/src/ui/main-menu.ts +139 -0
  117. package/src/ui/settings-menu.ts +125 -0
@@ -0,0 +1,58 @@
1
+ # SOURCE: https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAccess
2
+ # "noUncheckedIndexedAccess: true makes index signatures return T | undefined.
3
+ # Without it, obj[key] returns T — ignoring the possibility of undefined."
4
+ # SOURCE: https://www.totaltypescript.com/accessing-object-properties
5
+ # "Bracket access on objects and arrays may return undefined. Use optional chaining
6
+ # or explicit checks. Don't assume the key exists."
7
+ # SOURCE: https://typescript-book.com/objectTypes/index-signatures
8
+ # "Index signatures are inherently loose. Prefer Map/Record with explicit keys."
9
+ id: no-unsafe-bracket-access
10
+ language: typescript
11
+ message: "Bracket access without undefined check — property may not exist. Use optional chaining (obj[key]?.) or explicit check"
12
+ severity: error
13
+ note: |
14
+ UNSAFE:
15
+ const user = users[id];
16
+ const prop = obj[key];
17
+ const item = arr[0];
18
+ // All may be undefined — TypeScript treats as T, not T | undefined
19
+ // (unless noUncheckedIndexedAccess is enabled)
20
+
21
+ SAFE:
22
+ const user = users[id]; // only safe if Map.get() or has() check
23
+ if (user) { ... } // narrow after access
24
+
25
+ const user = users[id]?.name; // optional chaining
26
+
27
+ const user = users.get(id); // Map.get() returns T | undefined
28
+
29
+ if (id in users) {
30
+ const user = users[id]; // safe after guard
31
+ }
32
+
33
+ WHY: Without noUncheckedIndexedAccess (rarely enabled), bracket access returns T
34
+ instead of T | undefined. Runtime crashes from accessing properties on undefined.
35
+ This is the #1 source of "cannot read property of undefined" errors in TypeScript apps.
36
+ rule:
37
+ pattern: $OBJ[$KEY].$PROP
38
+ not:
39
+ any:
40
+ # Safe: already has optional chaining
41
+ - pattern: $OBJ[$KEY]?.$PROP
42
+ # Safe: preceded by truthy check
43
+ - inside:
44
+ pattern: |
45
+ if ($OBJ[$KEY]) {
46
+ $$$PRE
47
+ $OBJ[$KEY].$PROP
48
+ $$$POST
49
+ }
50
+ stopBy: end
51
+ ignores:
52
+ - '**/*.test.ts'
53
+ - '**/*.spec.ts'
54
+ - '**/test/**'
55
+ - '**/tests/**'
56
+ - '**/__tests__/**'
57
+ - '**/scripts/**'
58
+ - 'test-*.ts'
@@ -0,0 +1,45 @@
1
+ # SOURCE: https://typescript-eslint.io/rules/consistent-type-assertions/
2
+ # @typescript-eslint/consistent-type-assertions: "Enforce consistent usage of type assertions."
3
+ # SOURCE: https://rules.sonarsource.com/typescript/RSPEC-6508/
4
+ # SonarQube S6508: "Type assertions should not be used."
5
+ # SOURCE: https://google.github.io/styleguide/tsguide.html#type-assertions
6
+ # Google TS Style Guide: "Prefer type annotations over type assertions."
7
+ #
8
+ # REFINED: Only flags truly unsafe assertions — `as any` and `as unknown as X`.
9
+ # Safe assertions (DOM narrowing, generic params, runtime-validated types) are excluded.
10
+ id: no-unsafe-type-assertion
11
+ language: TypeScript
12
+ message: "Unsafe type assertion — `as any` or `as unknown as X` bypasses type safety. Use proper typing, type guards, or validation instead."
13
+ severity: warning
14
+ note: |
15
+ Only flags the two most dangerous assertion patterns:
16
+
17
+ 1. `as any` — completely opts out of type checking. Often hides bugs in JSON parsing,
18
+ fetch responses, and dynamic imports.
19
+ 2. `as unknown as X` — double assertion that forces an incompatible type cast.
20
+ Circumvents TypeScript's safety checks.
21
+
22
+ Safe patterns NOT flagged:
23
+ - React event narrowing: `e.target as HTMLInputElement`
24
+ - Node API narrowing: `server.address() as { port: number }`
25
+ - Generic param passing: `value as never`
26
+ - Const assertions: `{ a: 1 } as const`
27
+ - Runtime-validated narrowing: `return parsed as CodeTourOutput` (after null check)
28
+ rule:
29
+ # Match any `as` assertion, then constrain TYPE to only unsafe ones
30
+ pattern: $EXPR as $TYPE
31
+ constraints:
32
+ TYPE:
33
+ # Only match: any, any[], unknown (as part of double assertion), or Record<string, unknown>
34
+ # The `as unknown` in double assertions will match here; we rely on the pattern
35
+ # to catch both halves.
36
+ regex: '^(any(\[\])?|unknown)$'
37
+ ignores:
38
+ - '**/*.test.ts'
39
+ - '**/*.spec.ts'
40
+ - '**/test/**'
41
+ - '**/tests/**'
42
+ - '**/__tests__/**'
43
+ - '**/scripts/**'
44
+ - 'test-*.ts'
45
+ - '**/*.d.ts'
@@ -0,0 +1,62 @@
1
+ # SOURCE: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking
2
+ # "Use never in switch default to ensure all cases are handled. Missing cases
3
+ # silently fall through to default."
4
+ # SOURCE: https://typescript-book.com/discriminated-unions
5
+ # "Exhaustiveness checking with never ensures that when you add a new variant
6
+ # to a union, TypeScript forces you to handle it everywhere."
7
+ # SOURCE: https://www.totaltypescript.com/exhaustive-switch
8
+ # "Every switch on a discriminated union should have a default that assigns
9
+ # the value to never for exhaustiveness checking."
10
+ id: switch-must-be-exhaustive
11
+ language: typescript
12
+ message: "switch statement without exhaustiveness check — add default case with never assignment to catch missing cases"
13
+ severity: warning
14
+ note: |
15
+ UNSAFE:
16
+ function getStatusLabel(status: 'active' | 'inactive' | 'pending') {
17
+ switch (status) {
18
+ case 'active': return 'Active';
19
+ case 'inactive': return 'Inactive';
20
+ // Missing 'pending' — silently returns undefined
21
+ }
22
+ }
23
+
24
+ SAFE:
25
+ function getStatusLabel(status: 'active' | 'inactive' | 'pending') {
26
+ switch (status) {
27
+ case 'active': return 'Active';
28
+ case 'inactive': return 'Inactive';
29
+ case 'pending': return 'Pending';
30
+ default: {
31
+ const exhaustive: never = status;
32
+ return exhaustive;
33
+ }
34
+ }
35
+ }
36
+
37
+ WHY: Without exhaustiveness checking, adding a new variant to a union type causes
38
+ no compiler error at existing switch statements. The new case falls through to default
39
+ (or returns undefined) silently. The never assignment pattern makes TypeScript error
40
+ immediately when a new variant is added and not handled.
41
+ rule:
42
+ pattern: |
43
+ switch ($EXPR) {
44
+ $$$CASES
45
+ }
46
+ not:
47
+ any:
48
+ # Safe: has a default case
49
+ - pattern: |
50
+ switch ($EXPR) {
51
+ $$$CASES
52
+ default:
53
+ $$$DEFAULT_BODY
54
+ }
55
+ ignores:
56
+ - '**/*.test.ts'
57
+ - '**/*.spec.ts'
58
+ - '**/test/**'
59
+ - '**/tests/**'
60
+ - '**/__tests__/**'
61
+ - '**/scripts/**'
62
+ - 'test-*.ts'
@@ -0,0 +1,62 @@
1
+ # SOURCE: https://zod.dev/?id=async-refinements
2
+ # "Async refinements run on every parse call. If the async operation (e.g., DB lookup,
3
+ # HTTP call) is slow or the input is large, there's no built-in timeout or abort mechanism."
4
+ # SOURCE: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
5
+ # "Long-running async operations should support AbortController for cancellation."
6
+ # SOURCE: https://zod.dev/?id=refine
7
+ # "refine does not accept an AbortSignal. For async validation, wrap in a timeout
8
+ # or race with Promise.race to prevent hanging."
9
+ id: zod-async-refine-without-abort
10
+ language: typescript
11
+ message: "async refine() without timeout/abort — slow external call can hang validation indefinitely. Wrap in Promise.race with timeout"
12
+ severity: warning
13
+ note: |
14
+ UNSAFE:
15
+ z.string().refine(async (id) => {
16
+ const user = await db.user.findUnique({ where: { id } }); // no timeout
17
+ return user !== null;
18
+ });
19
+ // If DB is slow, every request hangs indefinitely
20
+
21
+ SAFE:
22
+ z.string().refine(async (id) => {
23
+ const user = await Promise.race([
24
+ db.user.findUnique({ where: { id } }),
25
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 3000))
26
+ ]);
27
+ return user !== null;
28
+ });
29
+ // OR use AbortController:
30
+ z.string().refine(async (id) => {
31
+ const controller = new AbortController();
32
+ setTimeout(() => controller.abort(), 3000);
33
+ const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
34
+ return res.ok;
35
+ });
36
+
37
+ WHY: Async refine callbacks that call databases or external APIs have no built-in timeout.
38
+ If the external dependency is slow or down, every validation call hangs, blocking the
39
+ event loop and eventually causing request timeouts at the server level (usually 30-60s).
40
+ Senior devs often add async refinements for "check if exists" without considering
41
+ the failure mode of the external call.
42
+ rule:
43
+ any:
44
+ - pattern: $SCHEMA.refine(async ($$$) => { $$$ })
45
+ - pattern: $SCHEMA.refine(async ($$$) => $EXPR)
46
+ not:
47
+ any:
48
+ # Safe: wrapped in Promise.race (has timeout)
49
+ - pattern: $SCHEMA.refine(async ($$$) => { Promise.race($$$) $$$ })
50
+ # Safe: uses AbortController
51
+ - pattern: $SCHEMA.refine(async ($$$) => { $$$ AbortController $$$ })
52
+ constraints:
53
+ SCHEMA:
54
+ regex: 'z\\..+|[A-Za-z]+'
55
+ ignores:
56
+ - '**/*.test.ts'
57
+ - '**/*.spec.ts'
58
+ - '**/test/**'
59
+ - '**/tests/**'
60
+ - '**/__tests__/**'
61
+ - '**/scripts/**'
62
+ - 'test-*.ts'
@@ -0,0 +1,59 @@
1
+ # SOURCE: https://zod.dev/?id=enums
2
+ # "z.enum() creates a union of string literals. The inferred type is a union of
3
+ # those literals, not a generic string. Bracket access on the enum values bypasses
4
+ # type checking."
5
+ # SOURCE: https://www.typescriptlang.org/docs/handbook/enums.html
6
+ # "Enum access should use the enum type system, not string bracket access which
7
+ # bypasses compile-time checking."
8
+ # SOURCE: https://zod.dev/?id=literals
9
+ # "Use .parse() to validate a string is a valid enum value. Direct comparison
10
+ # with bracket access skips validation."
11
+ id: zod-enum-unsafe-access
12
+ language: typescript
13
+ message: "z.enum() value accessed via bracket notation — bypasses type safety. Use .parse() to validate or index into enum.options"
14
+ severity: warning
15
+ note: |
16
+ UNSAFE:
17
+ const Role = z.enum(['admin', 'user', 'moderator']);
18
+ type RoleType = z.infer<typeof Role>;
19
+
20
+ const role = request.body.role as string; // unchecked cast
21
+ const valid = Role.options[role]; // undefined if invalid, no error
22
+ const valid2 = Role.Values[role]; // same issue
23
+
24
+ SAFE:
25
+ const Role = z.enum(['admin', 'user', 'moderator']);
26
+ const result = Role.safeParse(request.body.role);
27
+ if (!result.success) return reply.code(400).send({ error: 'Invalid role' });
28
+ const role = result.data; // type-safe: 'admin' | 'user' | 'moderator'
29
+
30
+ // Or use .parse() in a try/catch:
31
+ const role = Role.parse(request.body.role);
32
+
33
+ WHY: Accessing z.enum() values via bracket notation (Role.Values[unknownString]) returns
34
+ undefined for invalid keys instead of throwing an error. This silently passes invalid
35
+ values through the system. The correct pattern is .parse() or .safeParse() which validates
36
+ the input against the enum at runtime and provides proper type narrowing.
37
+ rule:
38
+ any:
39
+ - pattern: $ENUM.options[$KEY]
40
+ - pattern: $ENUM.Values[$KEY]
41
+ - pattern: $ENUM.enum[$KEY]
42
+ not:
43
+ any:
44
+ # Safe: key is a string literal (known at compile time)
45
+ - pattern: $ENUM.options["$$$LITERAL"]
46
+ - pattern: $ENUM.Values["$$$LITERAL"]
47
+ - pattern: $ENUM.enum["$$$LITERAL"]
48
+ constraints:
49
+ KEY:
50
+ not:
51
+ regex: '".*"'
52
+ ignores:
53
+ - '**/*.test.ts'
54
+ - '**/*.spec.ts'
55
+ - '**/test/**'
56
+ - '**/tests/**'
57
+ - '**/__tests__/**'
58
+ - '**/scripts/**'
59
+ - 'test-*.ts'
@@ -0,0 +1,70 @@
1
+ # SOURCE: https://zod.dev/?id=recursive-types
2
+ # "For recursive and circular schemas, you MUST use z.lazy(). Nesting z.object()
3
+ # more than 3 levels deep without lazy evaluation causes performance degradation
4
+ # and can cause stack overflow in schema inference."
5
+ # SOURCE: https://zod.dev/?id=zlazy
6
+ # "z.lazy() defers schema evaluation. Use it for recursive types like trees,
7
+ # nested menus, or self-referential data structures."
8
+ # SOURCE: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Stack_overflow
9
+ # "Deeply nested synchronous operations can exceed the call stack."
10
+ id: zod-nested-object-deep-path
11
+ language: typescript
12
+ message: "Deeply nested z.object() chain (>3 levels) — consider z.lazy() for readability and to avoid stack overflow in type inference"
13
+ severity: info
14
+ note: |
15
+ UNSAFE:
16
+ const schema = z.object({
17
+ level1: z.object({
18
+ level2: z.object({
19
+ level3: z.object({
20
+ level4: z.object({
21
+ level5: z.string()
22
+ })
23
+ })
24
+ })
25
+ })
26
+ });
27
+ // Type inference traverses all 5 levels synchronously
28
+
29
+ SAFE:
30
+ const level4Schema = z.lazy(() => z.object({
31
+ level5: z.string()
32
+ }));
33
+ const level3Schema = z.object({
34
+ level4: level4Schema
35
+ });
36
+ const level2Schema = z.object({
37
+ level3: level3Schema
38
+ });
39
+ const schema = z.object({
40
+ level1: level2Schema
41
+ });
42
+ // OR for truly recursive types:
43
+ const treeNode = z.lazy(() => z.object({
44
+ value: z.string(),
45
+ children: z.array(treeNode)
46
+ }));
47
+
48
+ WHY: Deeply nested z.object() chains (4+ levels) cause TypeScript inference to traverse
49
+ the entire tree during type checking. For complex schemas, this can cause:
50
+ 1. Slow IDE/hover performance (TypeScript hangs on inference)
51
+ 2. "Type instantiation is excessively deep" errors
52
+ 3. Stack overflow in schema.parse() for very large inputs
53
+ Senior devs often build deeply nested schemas for nested JSON API bodies without
54
+ realizing the performance impact on TypeScript compilation.
55
+ NOTE: This is a review-only rule. 3-4 levels is usually fine for API request bodies.
56
+ Flagged for manual review of schema complexity.
57
+ rule:
58
+ # This is a heuristic — we flag z.object chains that contain nested z.object
59
+ # inside the same expression. ast-grep can't count depth directly, so we
60
+ # flag the pattern of z.object containing z.object containing z.object.
61
+ any:
62
+ - pattern: 'z.object({ $$$: z.object({ $$$: z.object({ $$$ }) }) })'
63
+ ignores:
64
+ - '**/*.test.ts'
65
+ - '**/*.spec.ts'
66
+ - '**/test/**'
67
+ - '**/tests/**'
68
+ - '**/__tests__/**'
69
+ - '**/scripts/**'
70
+ - 'test-*.ts'
@@ -0,0 +1,50 @@
1
+ # SOURCE: https://zod.dev/?id=optional
2
+ # "Optional fields accept undefined. In route validation, an optional field without
3
+ # a default means downstream code must handle undefined at every usage site."
4
+ # SOURCE: https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/
5
+ # "Request body fields that are optional should have sensible defaults to avoid
6
+ # undefined checks scattered across business logic."
7
+ # SOURCE: https://zod.dev/?id=default
8
+ # "Use .default() to provide fallback values. This ensures the parsed output always
9
+ # has a concrete value, eliminating undefined handling downstream."
10
+ id: zod-optional-without-default-in-route
11
+ language: typescript
12
+ message: "optional() field without default() in route body schema — downstream code must handle undefined. Add .default() for explicit fallback"
13
+ severity: info
14
+ note: |
15
+ UNSAFE:
16
+ const bodySchema = z.object({
17
+ page: z.number().optional(), // undefined if not sent
18
+ sort: z.string().optional(), // every usage: sort ?? 'createdAt'
19
+ limit: z.number().optional(),
20
+ });
21
+
22
+ SAFE:
23
+ const bodySchema = z.object({
24
+ page: z.number().default(1),
25
+ sort: z.string().default('createdAt'),
26
+ limit: z.number().default(20),
27
+ });
28
+ // Now body.page is always number, no undefined checks needed
29
+
30
+ WHY: When a field is optional() without default(), every usage site must guard against
31
+ undefined. This scatters null checks throughout business logic and is a frequent source
32
+ of TypeError crashes. Using default() centralizes the fallback value in the schema,
33
+ which is the single source of truth for input shape.
34
+ NOTE: Review-only. Some fields (e.g., filters) are legitimately optional with no default.
35
+ rule:
36
+ any:
37
+ - pattern: $FIELD.optional()
38
+ - pattern: $FIELD.nullable().optional()
39
+ inside:
40
+ any:
41
+ - pattern: z.object({ $$$ })
42
+ - pattern: z.object({ $$$ }).$$$REST
43
+ ignores:
44
+ - '**/*.test.ts'
45
+ - '**/*.spec.ts'
46
+ - '**/test/**'
47
+ - '**/tests/**'
48
+ - '**/__tests__/**'
49
+ - '**/scripts/**'
50
+ - 'test-*.ts'
@@ -0,0 +1,42 @@
1
+ # SOURCE: https://zod.dev/?id=safeparse
2
+ # "safeParse returns a result object instead of throwing. Use it in route handlers
3
+ # where you want to return a 400 error instead of throwing a 500."
4
+ # SOURCE: https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/
5
+ # "Validation errors should return 400, not 500. schema.parse() throws ZodError
6
+ # which becomes an unhandled 500 if not caught."
7
+ # SOURCE: https://zod.dev/?id=errors
8
+ # "schema.parse() throws on invalid data. schema.safeParse() returns { success, data, error }
9
+ # — no try/catch needed."
10
+ id: zod-parse-not-safe
11
+ language: typescript
12
+ message: "schema.parse() in route handler — throws ZodError on invalid input. Use schema.safeParse() to return 400 instead of 500"
13
+ severity: warning
14
+ note: |
15
+ In Fastify route handlers, zod.parse() throws a ZodError when validation fails. If this
16
+ isn't caught, Fastify's error handler returns a 500 Internal Server Error. Using
17
+ safeParse() returns { success: false, error } which you can map to a proper 400 response:
18
+
19
+ const result = schema.safeParse(request.body);
20
+ if (!result.success) return reply.code(400).send({ errors: result.error.flatten() });
21
+ rule:
22
+ pattern: $SCHEMA.parse($$$)
23
+ inside:
24
+ any:
25
+ - kind: function_expression
26
+ inside:
27
+ pattern: $APP.$VERB($_, $$$, async ($$$) => { $$$ })
28
+ - kind: function_expression
29
+ inside:
30
+ pattern: $APP.$VERB($_, { $$$ }, async ($$$) => { $$$ })
31
+ not:
32
+ inside:
33
+ kind: try_statement
34
+ stopBy: end
35
+ ignores:
36
+ - '**/*.test.ts'
37
+ - '**/*.spec.ts'
38
+ - '**/test/**'
39
+ - '**/tests/**'
40
+ - '**/__tests__/**'
41
+ - '**/scripts/**'
42
+ - 'test-*.ts'
@@ -0,0 +1,58 @@
1
+ # SOURCE: https://zod.dev/?id=preprocess
2
+ # "preprocess runs BEFORE validation. If the preprocess function throws,
3
+ # the entire schema validation fails. Handle edge cases inside preprocess."
4
+ # SOURCE: https://zod.dev/?id=transform-and-preprocess
5
+ # "Unlike transform, preprocess runs before validation. Invalid inputs that
6
+ # are transformed to garbage may pass the output schema unexpectedly."
7
+ # SOURCE: https://www.reddit.com/r/typescript/comments/zod_preprocess_gotchas
8
+ # "z.preprocess((val) => new Date(val as string), z.date()) — if val is
9
+ # undefined/null, new Date(undefined) creates 'Invalid Date' which passes z.date()."
10
+ id: zod-preprocess-without-fallback
11
+ language: typescript
12
+ message: "z.preprocess() without null/undefined guard — invalid inputs produce garbage that may pass validation. Add input validation before transforming"
13
+ severity: warning
14
+ note: |
15
+ UNSAFE:
16
+ z.preprocess((val) => new Date(val as string), z.date())
17
+ // val=undefined → new Date(undefined) = "Invalid Date" → passes z.date()!
18
+
19
+ z.preprocess((val) => Number(val), z.number())
20
+ // val=undefined → Number(undefined) = NaN → z.number() may pass with coerce
21
+
22
+ z.preprocess((val) => val.toString().trim(), z.string())
23
+ // val=null → "null" string — not what you expected
24
+
25
+ SAFE:
26
+ z.preprocess((val) => {
27
+ if (val == null) throw new Error('Expected non-null');
28
+ return new Date(val as string);
29
+ }, z.date())
30
+
31
+ z.preprocess((val) => {
32
+ if (typeof val !== 'string') throw new Error('Expected string');
33
+ return Number(val);
34
+ }, z.number())
35
+
36
+ WHY: preprocess runs BEFORE the output schema validates. If the preprocess function
37
+ doesn't handle null/undefined, it transforms garbage into a value that may pass the
38
+ output schema. This is a silent data corruption vector — the schema "validates" but
39
+ the data is wrong. The most common case: Date construction with undefined input.
40
+ rule:
41
+ pattern: 'z.preprocess(($$$PARAMS) => { $$$BODY }, $$$SCHEMA)'
42
+ not:
43
+ any:
44
+ # Safe: has a throw/error guard inside the body
45
+ - pattern: |
46
+ z.preprocess(($$$PARAMS) => {
47
+ $$$PRE
48
+ if ($$$COND) { throw $$$ERR }
49
+ $$$POST
50
+ }, $$$SCHEMA)
51
+ ignores:
52
+ - '**/*.test.ts'
53
+ - '**/*.spec.ts'
54
+ - '**/test/**'
55
+ - '**/tests/**'
56
+ - '**/__tests__/**'
57
+ - '**/scripts/**'
58
+ - 'test-*.ts'
@@ -0,0 +1,54 @@
1
+ # SOURCE: https://zod.dev/?id=refine
2
+ # "The refine function's return value determines validity: true = valid, false = invalid.
3
+ # Returning undefined is truthy in some contexts but signals a missing return statement."
4
+ # SOURCE: https://zod.dev/?id=customize-error-with-a-transformer
5
+ # "If refine callback doesn't explicitly return a boolean, the result is unpredictable."
6
+ # SOURCE: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
7
+ # "Arrow function with curly braces requires explicit return. Missing return = returns undefined."
8
+ id: zod-refine-no-return-undefined
9
+ language: typescript
10
+ message: "refine() callback may return undefined — silent validation pass-through. Ensure callback explicitly returns boolean"
11
+ severity: error
12
+ note: |
13
+ UNSAFE:
14
+ z.string().refine((val) => {
15
+ if (val.length > 10) return false;
16
+ // Missing return for other cases → returns undefined → Zod treats as truthy → passes!
17
+ });
18
+ z.string().refine((val) => {
19
+ val.length > 10; // Expression not returned → undefined
20
+ });
21
+
22
+ SAFE:
23
+ z.string().refine((val) => val.length <= 10);
24
+ z.string().refine((val) => {
25
+ if (val.length > 10) return false;
26
+ return true;
27
+ });
28
+ z.string().refine((val) => val.length <= 10, { message: 'Too long' });
29
+
30
+ WHY: When a refine callback uses curly braces (block body) but forgets a return statement,
31
+ it returns undefined. Zod coerces undefined as truthy → the validation ALWAYS passes.
32
+ This means malformed data silently passes through. Senior devs frequently miss the
33
+ return when adding conditional logic to refine callbacks.
34
+ rule:
35
+ any:
36
+ # Block-body arrow function in refine with no return
37
+ - pattern: $SCHEMA.refine(($$$) => { $$$ })
38
+ not:
39
+ any:
40
+ # Safe: expression-body arrow function (implicit return)
41
+ - pattern: $SCHEMA.refine(($$$) => $EXPR)
42
+ # Safe: has explicit return inside
43
+ - pattern: $SCHEMA.refine(($$$) => { $$$ return $$$ $$$ })
44
+ constraints:
45
+ SCHEMA:
46
+ regex: 'z\\..+|[A-Za-z]+'
47
+ ignores:
48
+ - '**/*.test.ts'
49
+ - '**/*.spec.ts'
50
+ - '**/test/**'
51
+ - '**/tests/**'
52
+ - '**/__tests__/**'
53
+ - '**/scripts/**'
54
+ - 'test-*.ts'
@@ -0,0 +1,52 @@
1
+ # SOURCE: https://zod.dev/?id=transform
2
+ # "transform() changes the output type. Without chaining an output schema after
3
+ # transform, the inferred type may not match the actual runtime type."
4
+ # SOURCE: https://zod.dev/?id=coercion
5
+ # "When using transform, the output type changes. If you don't validate the
6
+ # transformed value, invalid runtime types bypass type checking."
7
+ # SOURCE: https://zod.dev/?id=chaining-after-transform
8
+ # "After a transform, the type is the transform's return type. Chain a .pipe()
9
+ # with an output schema to validate the transformed result."
10
+ id: zod-transform-without-output-type
11
+ language: typescript
12
+ message: "transform() without chained output schema — transformed value is not validated. Chain .pipe(outputSchema) after transform"
13
+ severity: warning
14
+ note: |
15
+ UNSAFE:
16
+ z.string().transform((val) => JSON.parse(val));
17
+ // If JSON.parse returns unexpected shape, no validation catches it
18
+
19
+ z.string().transform((val) => parseInt(val, 10));
20
+ // NaN passes through without validation
21
+
22
+ SAFE:
23
+ z.string().transform((val) => JSON.parse(val)).pipe(z.object({ name: z.string() }));
24
+ z.string().transform((val) => parseInt(val, 10)).pipe(z.number().int().min(0));
25
+ z.string().pipe(z.coerce.number().int()); // prefer coerce for simple conversions
26
+
27
+ WHY: transform() changes the runtime type but doesn't validate the output. If the
28
+ transform produces an unexpected value (NaN from parseInt, unexpected JSON shape),
29
+ downstream code receives unvalidated data. Senior devs often assume transform()
30
+ implies type safety, but it only changes the type inference — not runtime validation.
31
+ rule:
32
+ any:
33
+ - pattern: $SCHEMA.transform(($$$) => $$$)
34
+ not:
35
+ any:
36
+ # Safe: transform followed by .pipe() with output schema
37
+ - pattern: $SCHEMA.transform(($$$) => $$$).pipe($$$)
38
+ # Safe: transform followed by .refine()
39
+ - pattern: $SCHEMA.transform(($$$) => $$$).refine($$$)
40
+ # Safe: transform followed by another .transform() (chain continues)
41
+ - pattern: $SCHEMA.transform(($$$) => $$$).transform($$$)
42
+ constraints:
43
+ SCHEMA:
44
+ regex: 'z\\..+|[A-Za-z]+'
45
+ ignores:
46
+ - '**/*.test.ts'
47
+ - '**/*.spec.ts'
48
+ - '**/test/**'
49
+ - '**/tests/**'
50
+ - '**/__tests__/**'
51
+ - '**/scripts/**'
52
+ - 'test-*.ts'
package/.sg-sha ADDED
@@ -0,0 +1 @@
1
+ 685681f178b4d68d71b671c26912ee2bbb079210
package/.sgignore ADDED
@@ -0,0 +1,4 @@
1
+ # ast-grep artifacts (managed by coding-guard deploy.sh)
2
+ .sg-rules/
3
+ .sg-sha
4
+ sg-reports/
package/AGENTS.md ADDED
@@ -0,0 +1 @@
1
+