founder-schema 0.5.0 → 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 (77) hide show
  1. package/README.md +67 -0
  2. package/dist/access/permissions.d.ts +24 -2
  3. package/dist/access/permissions.d.ts.map +1 -1
  4. package/dist/access/permissions.js +9 -0
  5. package/dist/access/permissions.js.map +1 -1
  6. package/dist/core/common.d.ts +44 -0
  7. package/dist/core/common.d.ts.map +1 -1
  8. package/dist/core/common.js +76 -0
  9. package/dist/core/common.js.map +1 -1
  10. package/dist/core/errors.d.ts +29 -1
  11. package/dist/core/errors.d.ts.map +1 -1
  12. package/dist/core/errors.js +52 -1
  13. package/dist/core/errors.js.map +1 -1
  14. package/dist/core/limits.d.ts +53 -0
  15. package/dist/core/limits.d.ts.map +1 -1
  16. package/dist/core/limits.js +57 -0
  17. package/dist/core/limits.js.map +1 -1
  18. package/dist/core/responses.d.ts +43 -0
  19. package/dist/core/responses.d.ts.map +1 -1
  20. package/dist/core/responses.js +3 -1
  21. package/dist/core/responses.js.map +1 -1
  22. package/dist/core/validation.d.ts +14 -0
  23. package/dist/core/validation.d.ts.map +1 -1
  24. package/dist/core/validation.js +14 -0
  25. package/dist/core/validation.js.map +1 -1
  26. package/dist/domains/audit/audit.d.ts +4 -0
  27. package/dist/domains/audit/audit.d.ts.map +1 -1
  28. package/dist/domains/audit/audit.js +6 -3
  29. package/dist/domains/audit/audit.js.map +1 -1
  30. package/dist/domains/contributions/contributions.d.ts +165 -0
  31. package/dist/domains/contributions/contributions.d.ts.map +1 -0
  32. package/dist/domains/contributions/contributions.js +103 -0
  33. package/dist/domains/contributions/contributions.js.map +1 -0
  34. package/dist/domains/discover/discover.d.ts +5 -0
  35. package/dist/domains/discover/discover.d.ts.map +1 -1
  36. package/dist/domains/discover/discover.js +3 -13
  37. package/dist/domains/discover/discover.js.map +1 -1
  38. package/dist/domains/feed/feed.d.ts +231 -7
  39. package/dist/domains/feed/feed.d.ts.map +1 -1
  40. package/dist/domains/feed/feed.js +86 -18
  41. package/dist/domains/feed/feed.js.map +1 -1
  42. package/dist/domains/media/media.d.ts +30 -5
  43. package/dist/domains/media/media.d.ts.map +1 -1
  44. package/dist/domains/media/media.js +32 -6
  45. package/dist/domains/media/media.js.map +1 -1
  46. package/dist/domains/members/members.d.ts +5 -5
  47. package/dist/domains/moderation/moderation.d.ts +53 -3
  48. package/dist/domains/moderation/moderation.d.ts.map +1 -1
  49. package/dist/domains/moderation/moderation.js +47 -8
  50. package/dist/domains/moderation/moderation.js.map +1 -1
  51. package/dist/domains/notifications/notifications.d.ts +212 -0
  52. package/dist/domains/notifications/notifications.d.ts.map +1 -0
  53. package/dist/domains/notifications/notifications.js +148 -0
  54. package/dist/domains/notifications/notifications.js.map +1 -0
  55. package/dist/domains/onboarding/onboarding.d.ts +10 -13
  56. package/dist/domains/onboarding/onboarding.d.ts.map +1 -1
  57. package/dist/domains/onboarding/onboarding.js +6 -5
  58. package/dist/domains/onboarding/onboarding.js.map +1 -1
  59. package/dist/domains/products/products.d.ts +1464 -0
  60. package/dist/domains/products/products.d.ts.map +1 -0
  61. package/dist/domains/products/products.js +474 -0
  62. package/dist/domains/products/products.js.map +1 -0
  63. package/dist/domains/profile/founder-profile.d.ts +43 -0
  64. package/dist/domains/profile/founder-profile.d.ts.map +1 -1
  65. package/dist/domains/profile/founder-profile.js +48 -0
  66. package/dist/domains/profile/founder-profile.js.map +1 -1
  67. package/dist/domains/reviews/reviews.d.ts +184 -0
  68. package/dist/domains/reviews/reviews.d.ts.map +1 -0
  69. package/dist/domains/reviews/reviews.js +99 -0
  70. package/dist/domains/reviews/reviews.js.map +1 -0
  71. package/dist/domains/uploads/uploads.d.ts +2 -0
  72. package/dist/domains/uploads/uploads.d.ts.map +1 -1
  73. package/dist/index.d.ts +10 -6
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +14 -6
  76. package/dist/index.js.map +1 -1
  77. package/package.json +1 -1
@@ -0,0 +1,165 @@
1
+ /**
2
+ * founder-schema — Contribution: the help a member gives, recognised
3
+ *
4
+ * The score counts HELP GIVEN, and only what OTHER members recognise:
5
+ *
6
+ * feedback_helpful a product's team marks your comment "this helped" +10
7
+ * tester_confirmed a product's team confirms you tested their product +15
8
+ * review_helpful another member finds your review helpful +2
9
+ * (at most `REVIEW_HELPFUL_POINTS_MAX` per review)
10
+ *
11
+ * `review_written` is still listed so older rows and clients parse, but it is
12
+ * NEVER awarded (0.7.0): writing a review is your own act, and the score
13
+ * counts only what others recognise. A review earns through Helpful votes.
14
+ *
15
+ * Nothing a member does for their OWN product counts, and nothing their own
16
+ * product receives, so the score cannot be bought with a popular product.
17
+ *
18
+ * Against farming: one member can earn at most `CONTRIBUTION_PAIR_POINTS_MAX`
19
+ * points from any ONE other member over 30 days, so two founders cannot trade
20
+ * Helpful marks into a high score.
21
+ *
22
+ * EVERY POINT IS A ROW (the ledger), so a score can be explained ("how did I
23
+ * earn this?"), checked and re-tuned. A point stands while what earned it
24
+ * stands: unmarking, withdrawing a vote, deleting the review or comment, or
25
+ * staff removing it takes the point back. An hourly job recounts every score
26
+ * from the ledger, so a total can never drift for long.
27
+ *
28
+ * Routes (all signed in):
29
+ * GET /api/v1/contributions/me paginationQuerySchema -> contributionPageSchema
30
+ * GET /api/v1/contributions/leaderboard -> contributionLeaderboardSchema
31
+ *
32
+ * The score itself is on the profile and the card (`contributionSummarySchema`).
33
+ *
34
+ * @module founder-schema/domains/contributions
35
+ */
36
+ import { z } from 'zod';
37
+ /** ⚠ Stored per ledger row: append only. */
38
+ export declare const CONTRIBUTION_KIND_VALUES: readonly ["feedback_helpful", "tester_confirmed", "review_helpful", "review_written"];
39
+ export declare const contributionKindSchema: z.ZodEnum<{
40
+ feedback_helpful: "feedback_helpful";
41
+ tester_confirmed: "tester_confirmed";
42
+ review_helpful: "review_helpful";
43
+ review_written: "review_written";
44
+ }>;
45
+ /**
46
+ * The kinds that are actually awarded (0.7.0): every kind but `review_written`,
47
+ * which is never awarded any more. "How points are earned" lists these.
48
+ */
49
+ export declare const CONTRIBUTION_AWARDED_KIND_VALUES: ("feedback_helpful" | "tester_confirmed" | "review_helpful")[];
50
+ export type ContributionKind = z.infer<typeof contributionKindSchema>;
51
+ /**
52
+ * Points per kind. Exhaustive by type. Changing a number changes FUTURE rows;
53
+ * the hourly recount sums what each row was actually awarded.
54
+ */
55
+ export declare const CONTRIBUTION_POINTS: {
56
+ readonly feedback_helpful: 10;
57
+ readonly tester_confirmed: 15;
58
+ readonly review_helpful: 2;
59
+ readonly review_written: 3;
60
+ };
61
+ /** One line of "how I earned my points". */
62
+ export declare const contributionEntrySchema: z.ZodObject<{
63
+ id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
64
+ kind: z.ZodEnum<{
65
+ feedback_helpful: "feedback_helpful";
66
+ tester_confirmed: "tester_confirmed";
67
+ review_helpful: "review_helpful";
68
+ review_written: "review_written";
69
+ }>;
70
+ points: z.ZodNumber;
71
+ actor: z.ZodNullable<z.ZodObject<{
72
+ userId: z.ZodString;
73
+ username: z.ZodString;
74
+ displayName: z.ZodString;
75
+ avatarUrl: z.ZodOptional<z.ZodURL>;
76
+ headline: z.ZodOptional<z.ZodString>;
77
+ }, z.core.$strip>>;
78
+ product: z.ZodOptional<z.ZodObject<{
79
+ id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
80
+ slug: z.ZodString;
81
+ name: z.ZodString;
82
+ logoUrl: z.ZodOptional<z.ZodURL>;
83
+ }, z.core.$strip>>;
84
+ content: z.ZodOptional<z.ZodObject<{
85
+ kind: z.ZodEnum<{
86
+ post: "post";
87
+ poll: "poll";
88
+ }>;
89
+ id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
90
+ }, z.core.$strip>>;
91
+ commentId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
92
+ reviewId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
93
+ createdAt: z.ZodISODateTime;
94
+ }, z.core.$strip>;
95
+ export type ContributionEntry = z.infer<typeof contributionEntrySchema>;
96
+ export declare const contributionPageSchema: z.ZodObject<{
97
+ items: z.ZodArray<z.ZodObject<{
98
+ id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
99
+ kind: z.ZodEnum<{
100
+ feedback_helpful: "feedback_helpful";
101
+ tester_confirmed: "tester_confirmed";
102
+ review_helpful: "review_helpful";
103
+ review_written: "review_written";
104
+ }>;
105
+ points: z.ZodNumber;
106
+ actor: z.ZodNullable<z.ZodObject<{
107
+ userId: z.ZodString;
108
+ username: z.ZodString;
109
+ displayName: z.ZodString;
110
+ avatarUrl: z.ZodOptional<z.ZodURL>;
111
+ headline: z.ZodOptional<z.ZodString>;
112
+ }, z.core.$strip>>;
113
+ product: z.ZodOptional<z.ZodObject<{
114
+ id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
115
+ slug: z.ZodString;
116
+ name: z.ZodString;
117
+ logoUrl: z.ZodOptional<z.ZodURL>;
118
+ }, z.core.$strip>>;
119
+ content: z.ZodOptional<z.ZodObject<{
120
+ kind: z.ZodEnum<{
121
+ post: "post";
122
+ poll: "poll";
123
+ }>;
124
+ id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
125
+ }, z.core.$strip>>;
126
+ commentId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
127
+ reviewId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
128
+ createdAt: z.ZodISODateTime;
129
+ }, z.core.$strip>>;
130
+ nextCursor: z.ZodNullable<z.ZodString>;
131
+ }, z.core.$strip>;
132
+ export type ContributionPage = z.infer<typeof contributionPageSchema>;
133
+ /** One row of the top helpers of the last `CONTRIBUTION_RECENT_DAYS` days. */
134
+ export declare const contributionLeaderEntrySchema: z.ZodObject<{
135
+ rank: z.ZodNumber;
136
+ member: z.ZodObject<{
137
+ userId: z.ZodString;
138
+ username: z.ZodString;
139
+ displayName: z.ZodString;
140
+ avatarUrl: z.ZodOptional<z.ZodURL>;
141
+ headline: z.ZodOptional<z.ZodString>;
142
+ }, z.core.$strip>;
143
+ points: z.ZodNumber;
144
+ }, z.core.$strip>;
145
+ export type ContributionLeaderEntry = z.infer<typeof contributionLeaderEntrySchema>;
146
+ /**
147
+ * Top helpers, best first, at most `CONTRIBUTION_LEADERBOARD_SIZE`. Recomputed
148
+ * hourly; `computedAt` says when (null before the first run).
149
+ */
150
+ export declare const contributionLeaderboardSchema: z.ZodObject<{
151
+ items: z.ZodArray<z.ZodObject<{
152
+ rank: z.ZodNumber;
153
+ member: z.ZodObject<{
154
+ userId: z.ZodString;
155
+ username: z.ZodString;
156
+ displayName: z.ZodString;
157
+ avatarUrl: z.ZodOptional<z.ZodURL>;
158
+ headline: z.ZodOptional<z.ZodString>;
159
+ }, z.core.$strip>;
160
+ points: z.ZodNumber;
161
+ }, z.core.$strip>>;
162
+ computedAt: z.ZodNullable<z.ZodISODateTime>;
163
+ }, z.core.$strip>;
164
+ export type ContributionLeaderboard = z.infer<typeof contributionLeaderboardSchema>;
165
+ //# sourceMappingURL=contributions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contributions.d.ts","sourceRoot":"","sources":["../../../src/domains/contributions/contributions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,4CAA4C;AAC5C,eAAO,MAAM,wBAAwB,uFAK3B,CAAC;AACX,eAAO,MAAM,sBAAsB;;;;;EAAmC,CAAC;AACvE;;;GAGG;AACH,eAAO,MAAM,gCAAgC,gEAG5C,CAAC;AACF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;CAKqB,CAAC;AAEtD,4CAA4C;AAC5C,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoBlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAA2C,CAAC;AAC/E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,8EAA8E;AAC9E,eAAO,MAAM,6BAA6B;;;;;;;;;;iBAKxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF;;;GAGG;AACH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;iBAGxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * founder-schema — Contribution: the help a member gives, recognised
3
+ *
4
+ * The score counts HELP GIVEN, and only what OTHER members recognise:
5
+ *
6
+ * feedback_helpful a product's team marks your comment "this helped" +10
7
+ * tester_confirmed a product's team confirms you tested their product +15
8
+ * review_helpful another member finds your review helpful +2
9
+ * (at most `REVIEW_HELPFUL_POINTS_MAX` per review)
10
+ *
11
+ * `review_written` is still listed so older rows and clients parse, but it is
12
+ * NEVER awarded (0.7.0): writing a review is your own act, and the score
13
+ * counts only what others recognise. A review earns through Helpful votes.
14
+ *
15
+ * Nothing a member does for their OWN product counts, and nothing their own
16
+ * product receives, so the score cannot be bought with a popular product.
17
+ *
18
+ * Against farming: one member can earn at most `CONTRIBUTION_PAIR_POINTS_MAX`
19
+ * points from any ONE other member over 30 days, so two founders cannot trade
20
+ * Helpful marks into a high score.
21
+ *
22
+ * EVERY POINT IS A ROW (the ledger), so a score can be explained ("how did I
23
+ * earn this?"), checked and re-tuned. A point stands while what earned it
24
+ * stands: unmarking, withdrawing a vote, deleting the review or comment, or
25
+ * staff removing it takes the point back. An hourly job recounts every score
26
+ * from the ledger, so a total can never drift for long.
27
+ *
28
+ * Routes (all signed in):
29
+ * GET /api/v1/contributions/me paginationQuerySchema -> contributionPageSchema
30
+ * GET /api/v1/contributions/leaderboard -> contributionLeaderboardSchema
31
+ *
32
+ * The score itself is on the profile and the card (`contributionSummarySchema`).
33
+ *
34
+ * @module founder-schema/domains/contributions
35
+ */
36
+ import { z } from 'zod';
37
+ import { objectIdSchema } from '../../core/common.js';
38
+ import { paginatedSchema } from '../../core/responses.js';
39
+ import { contentKindSchema } from '../feed/feed.js';
40
+ import { productChipSchema } from '../products/products.js';
41
+ import { memberBylineSchema } from '../profile/founder-profile.js';
42
+ /** ⚠ Stored per ledger row: append only. */
43
+ export const CONTRIBUTION_KIND_VALUES = [
44
+ 'feedback_helpful',
45
+ 'tester_confirmed',
46
+ 'review_helpful',
47
+ 'review_written',
48
+ ];
49
+ export const contributionKindSchema = z.enum(CONTRIBUTION_KIND_VALUES);
50
+ /**
51
+ * The kinds that are actually awarded (0.7.0): every kind but `review_written`,
52
+ * which is never awarded any more. "How points are earned" lists these.
53
+ */
54
+ export const CONTRIBUTION_AWARDED_KIND_VALUES = CONTRIBUTION_KIND_VALUES.filter((k) => k !== 'review_written');
55
+ /**
56
+ * Points per kind. Exhaustive by type. Changing a number changes FUTURE rows;
57
+ * the hourly recount sums what each row was actually awarded.
58
+ */
59
+ export const CONTRIBUTION_POINTS = {
60
+ feedback_helpful: 10,
61
+ tester_confirmed: 15,
62
+ review_helpful: 2,
63
+ review_written: 3,
64
+ };
65
+ /** One line of "how I earned my points". */
66
+ export const contributionEntrySchema = z.object({
67
+ id: objectIdSchema,
68
+ kind: contributionKindSchema,
69
+ /** What it was actually awarded: below the kind's points when a cap applied. */
70
+ points: z.number().int().positive(),
71
+ /**
72
+ * Who recognised it; null for a deleted account, and for `review_helpful`
73
+ * (a Helpful vote is anonymous: the review shows only a count).
74
+ */
75
+ actor: memberBylineSchema.nullable(),
76
+ /** The product it was about, while it exists. */
77
+ product: productChipSchema.optional(),
78
+ /**
79
+ * What earned it, so the row opens it: the post or poll and the comment for
80
+ * `feedback_helpful`, the review for `review_helpful`.
81
+ */
82
+ content: z.object({ kind: contentKindSchema, id: objectIdSchema }).optional(),
83
+ commentId: objectIdSchema.optional(),
84
+ reviewId: objectIdSchema.optional(),
85
+ createdAt: z.iso.datetime(),
86
+ });
87
+ export const contributionPageSchema = paginatedSchema(contributionEntrySchema);
88
+ /** One row of the top helpers of the last `CONTRIBUTION_RECENT_DAYS` days. */
89
+ export const contributionLeaderEntrySchema = z.object({
90
+ rank: z.number().int().positive(),
91
+ member: memberBylineSchema,
92
+ /** Points earned in the window. */
93
+ points: z.number().int().positive(),
94
+ });
95
+ /**
96
+ * Top helpers, best first, at most `CONTRIBUTION_LEADERBOARD_SIZE`. Recomputed
97
+ * hourly; `computedAt` says when (null before the first run).
98
+ */
99
+ export const contributionLeaderboardSchema = z.object({
100
+ items: z.array(contributionLeaderEntrySchema),
101
+ computedAt: z.iso.datetime().nullable(),
102
+ });
103
+ //# sourceMappingURL=contributions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contributions.js","sourceRoot":"","sources":["../../../src/domains/contributions/contributions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAEnE,4CAA4C;AAC5C,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,kBAAkB;IAClB,kBAAkB;IAClB,gBAAgB;IAChB,gBAAgB;CACR,CAAC;AACX,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;AACvE;;;GAGG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,wBAAwB,CAAC,MAAM,CAC7E,CAAC,CAAC,EAA6E,EAAE,CAC/E,CAAC,KAAK,gBAAgB,CACzB,CAAC;AAGF;;;GAGG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,gBAAgB,EAAE,EAAE;IACpB,gBAAgB,EAAE,EAAE;IACpB,cAAc,EAAE,CAAC;IACjB,cAAc,EAAE,CAAC;CACkC,CAAC;AAEtD,4CAA4C;AAC5C,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,EAAE,EAAE,cAAc;IAClB,IAAI,EAAE,sBAAsB;IAC5B,gFAAgF;IAChF,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACnC;;;OAGG;IACH,KAAK,EAAE,kBAAkB,CAAC,QAAQ,EAAE;IACpC,iDAAiD;IACjD,OAAO,EAAE,iBAAiB,CAAC,QAAQ,EAAE;IACrC;;;OAGG;IACH,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,EAAE,EAAE,cAAc,EAAE,CAAC,CAAC,QAAQ,EAAE;IAC7E,SAAS,EAAE,cAAc,CAAC,QAAQ,EAAE;IACpC,QAAQ,EAAE,cAAc,CAAC,QAAQ,EAAE;IACnC,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;CAC5B,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,sBAAsB,GAAG,eAAe,CAAC,uBAAuB,CAAC,CAAC;AAG/E,8EAA8E;AAC9E,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC;IACpD,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACjC,MAAM,EAAE,kBAAkB;IAC1B,mCAAmC;IACnC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;CACpC,CAAC,CAAC;AAGH;;;GAGG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC;IACpD,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,6BAA6B,CAAC;IAC7C,UAAU,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CACxC,CAAC,CAAC","sourcesContent":["/**\n * founder-schema — Contribution: the help a member gives, recognised\n *\n * The score counts HELP GIVEN, and only what OTHER members recognise:\n *\n * feedback_helpful a product's team marks your comment \"this helped\" +10\n * tester_confirmed a product's team confirms you tested their product +15\n * review_helpful another member finds your review helpful +2\n * (at most `REVIEW_HELPFUL_POINTS_MAX` per review)\n *\n * `review_written` is still listed so older rows and clients parse, but it is\n * NEVER awarded (0.7.0): writing a review is your own act, and the score\n * counts only what others recognise. A review earns through Helpful votes.\n *\n * Nothing a member does for their OWN product counts, and nothing their own\n * product receives, so the score cannot be bought with a popular product.\n *\n * Against farming: one member can earn at most `CONTRIBUTION_PAIR_POINTS_MAX`\n * points from any ONE other member over 30 days, so two founders cannot trade\n * Helpful marks into a high score.\n *\n * EVERY POINT IS A ROW (the ledger), so a score can be explained (\"how did I\n * earn this?\"), checked and re-tuned. A point stands while what earned it\n * stands: unmarking, withdrawing a vote, deleting the review or comment, or\n * staff removing it takes the point back. An hourly job recounts every score\n * from the ledger, so a total can never drift for long.\n *\n * Routes (all signed in):\n * GET /api/v1/contributions/me paginationQuerySchema -> contributionPageSchema\n * GET /api/v1/contributions/leaderboard -> contributionLeaderboardSchema\n *\n * The score itself is on the profile and the card (`contributionSummarySchema`).\n *\n * @module founder-schema/domains/contributions\n */\n\nimport { z } from 'zod';\nimport { objectIdSchema } from '../../core/common.js';\nimport { paginatedSchema } from '../../core/responses.js';\nimport { contentKindSchema } from '../feed/feed.js';\nimport { productChipSchema } from '../products/products.js';\nimport { memberBylineSchema } from '../profile/founder-profile.js';\n\n/** ⚠ Stored per ledger row: append only. */\nexport const CONTRIBUTION_KIND_VALUES = [\n 'feedback_helpful',\n 'tester_confirmed',\n 'review_helpful',\n 'review_written',\n] as const;\nexport const contributionKindSchema = z.enum(CONTRIBUTION_KIND_VALUES);\n/**\n * The kinds that are actually awarded (0.7.0): every kind but `review_written`,\n * which is never awarded any more. \"How points are earned\" lists these.\n */\nexport const CONTRIBUTION_AWARDED_KIND_VALUES = CONTRIBUTION_KIND_VALUES.filter(\n (k): k is Exclude<(typeof CONTRIBUTION_KIND_VALUES)[number], 'review_written'> =>\n k !== 'review_written',\n);\nexport type ContributionKind = z.infer<typeof contributionKindSchema>;\n\n/**\n * Points per kind. Exhaustive by type. Changing a number changes FUTURE rows;\n * the hourly recount sums what each row was actually awarded.\n */\nexport const CONTRIBUTION_POINTS = {\n feedback_helpful: 10,\n tester_confirmed: 15,\n review_helpful: 2,\n review_written: 3,\n} as const satisfies Record<ContributionKind, number>;\n\n/** One line of \"how I earned my points\". */\nexport const contributionEntrySchema = z.object({\n id: objectIdSchema,\n kind: contributionKindSchema,\n /** What it was actually awarded: below the kind's points when a cap applied. */\n points: z.number().int().positive(),\n /**\n * Who recognised it; null for a deleted account, and for `review_helpful`\n * (a Helpful vote is anonymous: the review shows only a count).\n */\n actor: memberBylineSchema.nullable(),\n /** The product it was about, while it exists. */\n product: productChipSchema.optional(),\n /**\n * What earned it, so the row opens it: the post or poll and the comment for\n * `feedback_helpful`, the review for `review_helpful`.\n */\n content: z.object({ kind: contentKindSchema, id: objectIdSchema }).optional(),\n commentId: objectIdSchema.optional(),\n reviewId: objectIdSchema.optional(),\n createdAt: z.iso.datetime(),\n});\nexport type ContributionEntry = z.infer<typeof contributionEntrySchema>;\n\nexport const contributionPageSchema = paginatedSchema(contributionEntrySchema);\nexport type ContributionPage = z.infer<typeof contributionPageSchema>;\n\n/** One row of the top helpers of the last `CONTRIBUTION_RECENT_DAYS` days. */\nexport const contributionLeaderEntrySchema = z.object({\n rank: z.number().int().positive(),\n member: memberBylineSchema,\n /** Points earned in the window. */\n points: z.number().int().positive(),\n});\nexport type ContributionLeaderEntry = z.infer<typeof contributionLeaderEntrySchema>;\n\n/**\n * Top helpers, best first, at most `CONTRIBUTION_LEADERBOARD_SIZE`. Recomputed\n * hourly; `computedAt` says when (null before the first run).\n */\nexport const contributionLeaderboardSchema = z.object({\n items: z.array(contributionLeaderEntrySchema),\n computedAt: z.iso.datetime().nullable(),\n});\nexport type ContributionLeaderboard = z.infer<typeof contributionLeaderboardSchema>;\n"]}
@@ -87,9 +87,14 @@ export declare const discoverPageSchema: z.ZodObject<{
87
87
  country: z.ZodString;
88
88
  }, z.core.$strip>>;
89
89
  displayName: z.ZodString;
90
+ userId: z.ZodString;
90
91
  username: z.ZodString;
91
92
  avatarUrl: z.ZodOptional<z.ZodURL>;
92
93
  headline: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
94
+ contribution: z.ZodObject<{
95
+ score: z.ZodNumber;
96
+ helpedCount: z.ZodNumber;
97
+ }, z.core.$strip>;
93
98
  venture: z.ZodOptional<z.ZodObject<{
94
99
  name: z.ZodString;
95
100
  tagline: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../../src/domains/discover/discover.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAyBxB;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAQ9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAErE,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAqC,CAAC;AACrE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAM9D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAK/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
1
+ {"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../../src/domains/discover/discover.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAiBxB;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAQ9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAErE,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAqC,CAAC;AACrE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAM9D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAK/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
@@ -8,22 +8,12 @@
8
8
  * @module founder-schema/domains/discover
9
9
  */
10
10
  import { z } from 'zod';
11
- import { industrySchema, optionalTrimmedString, paginationQuerySchema, skillSchema, startupStageSchema, } from '../../core/common.js';
11
+ import { industrySchema, multiValueQuery, optionalTrimmedString, paginationQuerySchema, skillSchema, startupStageSchema, } from '../../core/common.js';
12
12
  import { DISCOVER_FILTER_VALUES_MAX, DISCOVER_TEXT_MAX } from '../../core/limits.js';
13
13
  import { paginatedSchema } from '../../core/responses.js';
14
14
  import { founderCardSchema } from '../profile/founder-profile.js';
15
- /**
16
- * Accepts a repeated query param (`?skill=growth&skill=sales`) or a single
17
- * value, and always produces an array.
18
- *
19
- * Express gives `string` for one occurrence and `string[]` for several, so
20
- * without this the same URL shape parses two different ways.
21
- */
22
- const multiValue = (inner) => z.preprocess((value) => {
23
- if (value === undefined || value === null)
24
- return undefined;
25
- return Array.isArray(value) ? value : [value];
26
- }, z.array(inner).max(DISCOVER_FILTER_VALUES_MAX).optional());
15
+ /** A Discover multi-select (`?skill=a&skill=b`): see `multiValueQuery`. */
16
+ const multiValue = (inner) => multiValueQuery(inner, DISCOVER_FILTER_VALUES_MAX);
27
17
  /**
28
18
  * Every free-text filter here is `optionalTrimmedString`, so an EMPTY one is
29
19
  * treated as "not provided" rather than as a validation failure.
@@ -1 +1 @@
1
- {"version":3,"file":"discover.js","sourceRoot":"","sources":["../../../src/domains/discover/discover.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAElE;;;;;;GAMG;AACH,MAAM,UAAU,GAAG,CAAsB,KAAQ,EAAE,EAAE,CACnD,CAAC,CAAC,UAAU,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5D,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAChD,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;AAEhE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,qBAAqB,CAAC,MAAM,CAAC;IAC9D,wEAAwE;IACxE,CAAC,EAAE,qBAAqB,CAAC,iBAAiB,CAAC;IAC3C,QAAQ,EAAE,UAAU,CAAC,cAAc,CAAC;IACpC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC;IAC9B,KAAK,EAAE,UAAU,CAAC,kBAAkB,CAAC;IACrC,IAAI,EAAE,qBAAqB,CAAC,iBAAiB,CAAC;IAC9C,OAAO,EAAE,qBAAqB,CAAC,iBAAiB,CAAC;CAClD,CAAC,CAAC;AAIH,4DAA4D;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAC,iBAAiB,CAAC,CAAC;AAGrE,+DAA+D;AAC/D,MAAM,WAAW,GAAG,CAAsB,KAAQ,EAAE,EAAE,CACpD,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;AAE7D;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC;IAChD,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;IACzC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC;IAChD,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;CACzC,CAAC,CAAC","sourcesContent":["/**\n * founder-schema — Discover search and filters\n *\n * Search is by name; the filters are industry, skill, startup stage and\n * location. The community surface the Discover tab will grow later is not\n * modelled yet.\n *\n * @module founder-schema/domains/discover\n */\n\nimport { z } from 'zod';\nimport {\n industrySchema,\n optionalTrimmedString,\n paginationQuerySchema,\n skillSchema,\n startupStageSchema,\n} from '../../core/common.js';\nimport { DISCOVER_FILTER_VALUES_MAX, DISCOVER_TEXT_MAX } from '../../core/limits.js';\nimport { paginatedSchema } from '../../core/responses.js';\nimport { founderCardSchema } from '../profile/founder-profile.js';\n\n/**\n * Accepts a repeated query param (`?skill=growth&skill=sales`) or a single\n * value, and always produces an array.\n *\n * Express gives `string` for one occurrence and `string[]` for several, so\n * without this the same URL shape parses two different ways.\n */\nconst multiValue = <T extends z.ZodType>(inner: T) =>\n z.preprocess((value) => {\n if (value === undefined || value === null) return undefined;\n return Array.isArray(value) ? value : [value];\n }, z.array(inner).max(DISCOVER_FILTER_VALUES_MAX).optional());\n\n/**\n * Every free-text filter here is `optionalTrimmedString`, so an EMPTY one is\n * treated as \"not provided\" rather than as a validation failure.\n *\n * A search box the member has just cleared sends `?q=`, and a select they reset\n * sends `?city=`. Refusing those turns an ordinary interaction into a 400 with\n * nothing useful to show for it: the member asked for fewer filters, not for an\n * error. This is the one place the create-body rule and the patch-body rule\n * agree, because a query has nothing to erase.\n */\nexport const discoverQuerySchema = paginationQuerySchema.extend({\n /** Free-text name search. Matched against display name and username. */\n q: optionalTrimmedString(DISCOVER_TEXT_MAX),\n industry: multiValue(industrySchema),\n skill: multiValue(skillSchema),\n stage: multiValue(startupStageSchema),\n city: optionalTrimmedString(DISCOVER_TEXT_MAX),\n country: optionalTrimmedString(DISCOVER_TEXT_MAX),\n});\nexport type DiscoverQuery = z.infer<typeof discoverQuerySchema>;\nexport type DiscoverQueryInput = z.input<typeof discoverQuerySchema>;\n\n/** A page of founder cards, most recently updated first. */\nexport const discoverPageSchema = paginatedSchema(founderCardSchema);\nexport type DiscoverPage = z.infer<typeof discoverPageSchema>;\n\n/** One filter option and how many listed founders match it. */\nconst facetOption = <T extends z.ZodType>(value: T) =>\n z.object({ value, count: z.number().int().nonnegative() });\n\n/**\n * The filter options the Discover drawer offers, with counts. The VALUES come\n * from the enums here, so the options cannot disagree with what the query\n * accepts.\n *\n * ⚠ NO LABELS. The value IS the translation key (`industries.<value>`,\n * `stages.<value>`, `skills.<value>`), and a client words it in the member's\n * language. Until 0.2.0 the server also sent an English `label` per option,\n * built by capitalising the value (\"Ai ml\"), which no client read and which\n * would have been wrong in Hindi if one had. Cities are member-typed place\n * names, so their value is shown as it is.\n */\nexport const discoverFacetsSchema = z.object({\n industries: z.array(facetOption(industrySchema)),\n skills: z.array(facetOption(skillSchema)),\n stages: z.array(facetOption(startupStageSchema)),\n cities: z.array(facetOption(z.string())),\n});\nexport type DiscoverFacets = z.infer<typeof discoverFacetsSchema>;\n"]}
1
+ {"version":3,"file":"discover.js","sourceRoot":"","sources":["../../../src/domains/discover/discover.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,cAAc,EACd,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAElE,2EAA2E;AAC3E,MAAM,UAAU,GAAG,CAAsB,KAAQ,EAAE,EAAE,CACnD,eAAe,CAAC,KAAK,EAAE,0BAA0B,CAAC,CAAC;AAErD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,qBAAqB,CAAC,MAAM,CAAC;IAC9D,wEAAwE;IACxE,CAAC,EAAE,qBAAqB,CAAC,iBAAiB,CAAC;IAC3C,QAAQ,EAAE,UAAU,CAAC,cAAc,CAAC;IACpC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC;IAC9B,KAAK,EAAE,UAAU,CAAC,kBAAkB,CAAC;IACrC,IAAI,EAAE,qBAAqB,CAAC,iBAAiB,CAAC;IAC9C,OAAO,EAAE,qBAAqB,CAAC,iBAAiB,CAAC;CAClD,CAAC,CAAC;AAIH,4DAA4D;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAC,iBAAiB,CAAC,CAAC;AAGrE,+DAA+D;AAC/D,MAAM,WAAW,GAAG,CAAsB,KAAQ,EAAE,EAAE,CACpD,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;AAE7D;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC;IAChD,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;IACzC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC;IAChD,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;CACzC,CAAC,CAAC","sourcesContent":["/**\n * founder-schema — Discover search and filters\n *\n * Search is by name; the filters are industry, skill, startup stage and\n * location. The community surface the Discover tab will grow later is not\n * modelled yet.\n *\n * @module founder-schema/domains/discover\n */\n\nimport { z } from 'zod';\nimport {\n industrySchema,\n multiValueQuery,\n optionalTrimmedString,\n paginationQuerySchema,\n skillSchema,\n startupStageSchema,\n} from '../../core/common.js';\nimport { DISCOVER_FILTER_VALUES_MAX, DISCOVER_TEXT_MAX } from '../../core/limits.js';\nimport { paginatedSchema } from '../../core/responses.js';\nimport { founderCardSchema } from '../profile/founder-profile.js';\n\n/** A Discover multi-select (`?skill=a&skill=b`): see `multiValueQuery`. */\nconst multiValue = <T extends z.ZodType>(inner: T) =>\n multiValueQuery(inner, DISCOVER_FILTER_VALUES_MAX);\n\n/**\n * Every free-text filter here is `optionalTrimmedString`, so an EMPTY one is\n * treated as \"not provided\" rather than as a validation failure.\n *\n * A search box the member has just cleared sends `?q=`, and a select they reset\n * sends `?city=`. Refusing those turns an ordinary interaction into a 400 with\n * nothing useful to show for it: the member asked for fewer filters, not for an\n * error. This is the one place the create-body rule and the patch-body rule\n * agree, because a query has nothing to erase.\n */\nexport const discoverQuerySchema = paginationQuerySchema.extend({\n /** Free-text name search. Matched against display name and username. */\n q: optionalTrimmedString(DISCOVER_TEXT_MAX),\n industry: multiValue(industrySchema),\n skill: multiValue(skillSchema),\n stage: multiValue(startupStageSchema),\n city: optionalTrimmedString(DISCOVER_TEXT_MAX),\n country: optionalTrimmedString(DISCOVER_TEXT_MAX),\n});\nexport type DiscoverQuery = z.infer<typeof discoverQuerySchema>;\nexport type DiscoverQueryInput = z.input<typeof discoverQuerySchema>;\n\n/** A page of founder cards, most recently updated first. */\nexport const discoverPageSchema = paginatedSchema(founderCardSchema);\nexport type DiscoverPage = z.infer<typeof discoverPageSchema>;\n\n/** One filter option and how many listed founders match it. */\nconst facetOption = <T extends z.ZodType>(value: T) =>\n z.object({ value, count: z.number().int().nonnegative() });\n\n/**\n * The filter options the Discover drawer offers, with counts. The VALUES come\n * from the enums here, so the options cannot disagree with what the query\n * accepts.\n *\n * ⚠ NO LABELS. The value IS the translation key (`industries.<value>`,\n * `stages.<value>`, `skills.<value>`), and a client words it in the member's\n * language. Until 0.2.0 the server also sent an English `label` per option,\n * built by capitalising the value (\"Ai ml\"), which no client read and which\n * would have been wrong in Hindi if one had. Cities are member-typed place\n * names, so their value is shown as it is.\n */\nexport const discoverFacetsSchema = z.object({\n industries: z.array(facetOption(industrySchema)),\n skills: z.array(facetOption(skillSchema)),\n stages: z.array(facetOption(startupStageSchema)),\n cities: z.array(facetOption(z.string())),\n});\nexport type DiscoverFacets = z.infer<typeof discoverFacetsSchema>;\n"]}