jansathi-community-schema 0.50.0 → 0.50.1

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.
@@ -73,12 +73,26 @@ export interface ScoreRule {
73
73
  verifierGated: boolean;
74
74
  /** Human-facing fallback label rendered on the breakdown history. */
75
75
  label: string;
76
+ /**
77
+ * i18n key (socialScore namespace) for the "How you earn points"
78
+ * catalog row. Every awarding rule carries one so the catalog is
79
+ * DERIVED from this table (see `buildScoreCatalog`) and can never
80
+ * drift out of sync with what actually earns points.
81
+ */
82
+ catalogKey: string;
83
+ /** Show a "New" chip on the catalog row. */
84
+ isNew?: boolean;
85
+ /**
86
+ * Exclude this rule from the user-facing catalog while STILL awarding
87
+ * points (e.g. an internal/automatic award). Default: shown.
88
+ */
89
+ catalogHidden?: boolean;
76
90
  }
77
91
  export declare const SCORE_RULES: Record<ScoreKind, ScoreRule>;
78
92
  export interface ScoreRuleRef {
79
93
  /** i18n key (socialScore namespace) for the action description. */
80
94
  labelKey: string;
81
- /** Display reward, e.g. "+5" or a multi-step "+10 / +20 / +10". */
95
+ /** Display reward derived from the rule, e.g. "+5". */
82
96
  points: string;
83
97
  /** Marks a freshly-added rule with a "New" chip. */
84
98
  isNew?: boolean;
@@ -89,5 +103,12 @@ export interface ScoreRuleGroup {
89
103
  groupKey: string;
90
104
  rules: ScoreRuleRef[];
91
105
  }
106
+ /**
107
+ * Build the "How you earn points" catalog by projecting SCORE_RULES into
108
+ * display sections. Every non-hidden rule becomes a row under its
109
+ * category, so the catalog is guaranteed to stay in lockstep with the
110
+ * award table — add a rule and it appears automatically.
111
+ */
112
+ export declare function buildScoreCatalog(): ScoreRuleGroup[];
92
113
  export declare const SCORE_RULE_CATALOG: ScoreRuleGroup[];
93
114
  //# sourceMappingURL=rules.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/social-score/rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAI5C;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAI7C,MAAM,MAAM,QAAQ,GAChB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,iBAAiB,CAAA;CAAE,GAC3B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnC,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,aAAa,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,QAAQ,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,SAAS,EAAE,SAAS,CA8OpD,CAAC;AAQF,MAAM,WAAW,YAAY;IAC3B,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,aAAa,CAAC;IACxB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB;AAED,eAAO,MAAM,kBAAkB,EAAE,cAAc,EAkC9C,CAAC"}
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/social-score/rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAI5C;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAI7C,MAAM,MAAM,QAAQ,GAChB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,iBAAiB,CAAA;CAAE,GAC3B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnC,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,aAAa,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,QAAQ,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,SAAS,EAAE,SAAS,CAwQpD,CAAC;AASF,MAAM,WAAW,YAAY;IAC3B,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,aAAa,CAAC;IACxB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB;AAaD;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,EAAE,CAiBpD;AAED,eAAO,MAAM,kBAAkB,EAAE,cAAc,EAAwB,CAAC"}
@@ -65,6 +65,7 @@ export const SCORE_RULES = {
65
65
  cap: { kind: 'once' },
66
66
  verifierGated: true,
67
67
  label: 'Profile verified',
68
+ catalogKey: 'ruleProfileComplete',
68
69
  },
69
70
  // ─── Civic contribution ───────────────────────────────────────────────
70
71
  crowdfunding_donation_verified: {
@@ -73,6 +74,7 @@ export const SCORE_RULES = {
73
74
  cap: { kind: 'daily', max: 10 },
74
75
  verifierGated: true,
75
76
  label: 'Verified donation',
77
+ catalogKey: 'ruleDonateFundraiser',
76
78
  },
77
79
  crowdfunding_campaign_succeeded: {
78
80
  category: 'civic',
@@ -80,6 +82,7 @@ export const SCORE_RULES = {
80
82
  cap: { kind: 'once-per-source' },
81
83
  verifierGated: true,
82
84
  label: 'Campaign succeeded',
85
+ catalogKey: 'ruleFundraiserSucceeded',
83
86
  },
84
87
  item_donated: {
85
88
  category: 'civic',
@@ -90,6 +93,7 @@ export const SCORE_RULES = {
90
93
  cap: { kind: 'daily', max: 5 },
91
94
  verifierGated: true,
92
95
  label: 'Item donated',
96
+ catalogKey: 'ruleDonate',
93
97
  },
94
98
  lost_found_resolved: {
95
99
  category: 'civic',
@@ -102,6 +106,7 @@ export const SCORE_RULES = {
102
106
  cap: { kind: 'daily', max: 3 },
103
107
  verifierGated: false,
104
108
  label: 'Lost & found resolved',
109
+ catalogKey: 'ruleLostFoundResolved',
105
110
  },
106
111
  suggestions_complaints_submitted: {
107
112
  category: 'civic',
@@ -109,6 +114,7 @@ export const SCORE_RULES = {
109
114
  cap: { kind: 'daily', max: 3 },
110
115
  verifierGated: false,
111
116
  label: 'Suggestions & complaints report',
117
+ catalogKey: 'ruleSubmitReport',
112
118
  },
113
119
  suggestions_complaints_resolved: {
114
120
  category: 'civic',
@@ -116,6 +122,7 @@ export const SCORE_RULES = {
116
122
  cap: { kind: 'once-per-source' },
117
123
  verifierGated: true,
118
124
  label: 'Suggestions & complaints resolved',
125
+ catalogKey: 'ruleReportResolved',
119
126
  },
120
127
  // A lender rated a returned item well (overall >= 4★). Daily-capped to
121
128
  // throttle farming; idempotent per borrow-request via the source id.
@@ -125,6 +132,7 @@ export const SCORE_RULES = {
125
132
  cap: { kind: 'daily', max: 5 },
126
133
  verifierGated: false,
127
134
  label: 'Returned a borrowed item well',
135
+ catalogKey: 'ruleReturnResource',
128
136
  },
129
137
  // ─── Civic — volunteer work helping OTHER users ───────────────────────
130
138
  // Credited to the VOLUNTEER (the actor), not the donation owner, when
@@ -138,6 +146,7 @@ export const SCORE_RULES = {
138
146
  cap: { kind: 'once-per-source' },
139
147
  verifierGated: false,
140
148
  label: 'Delivered a donation to the centre',
149
+ catalogKey: 'ruleDeliver',
141
150
  },
142
151
  // Credited to the VOLUNTEER who starts someone else's detailed
143
152
  // profile in survey/fill mode. once-per-source keyed on the target
@@ -148,6 +157,7 @@ export const SCORE_RULES = {
148
157
  cap: { kind: 'once-per-source' },
149
158
  verifierGated: false,
150
159
  label: 'Started a profile for someone',
160
+ catalogKey: 'ruleUdpStart',
151
161
  },
152
162
  // Credited to the VOLUNTEER who submits a completed profile they
153
163
  // filled for someone else (PENDING_APPROVAL transition in fill mode).
@@ -157,6 +167,7 @@ export const SCORE_RULES = {
157
167
  cap: { kind: 'once-per-source' },
158
168
  verifierGated: false,
159
169
  label: 'Completed a profile for someone',
170
+ catalogKey: 'ruleUdpFinish',
160
171
  },
161
172
  // Credited to the VERIFIER (leader/official) who approves a detailed
162
173
  // profile. Distinct from `udp_approved`, which credits the profile
@@ -167,6 +178,7 @@ export const SCORE_RULES = {
167
178
  cap: { kind: 'once-per-source' },
168
179
  verifierGated: true,
169
180
  label: 'Verified a profile',
181
+ catalogKey: 'ruleUdpVerify',
170
182
  },
171
183
  // ─── Engagement ───────────────────────────────────────────────────────
172
184
  event_organized: {
@@ -179,6 +191,7 @@ export const SCORE_RULES = {
179
191
  cap: { kind: 'daily', max: 2 },
180
192
  verifierGated: false,
181
193
  label: 'Event organized',
194
+ catalogKey: 'ruleEventOrganized',
182
195
  },
183
196
  poll_created: {
184
197
  category: 'engagement',
@@ -186,6 +199,7 @@ export const SCORE_RULES = {
186
199
  cap: { kind: 'daily', max: 3 },
187
200
  verifierGated: false,
188
201
  label: 'Poll created',
202
+ catalogKey: 'rulePollCreated',
189
203
  },
190
204
  poll_voted: {
191
205
  category: 'engagement',
@@ -193,6 +207,7 @@ export const SCORE_RULES = {
193
207
  cap: { kind: 'daily', max: 10 },
194
208
  verifierGated: false,
195
209
  label: 'Poll vote',
210
+ catalogKey: 'rulePollVoted',
196
211
  },
197
212
  // ─── Engagement — content the community upvoted past the bar ──────────
198
213
  // These fire ONCE per content item, the first time its net vote
@@ -206,6 +221,7 @@ export const SCORE_RULES = {
206
221
  cap: { kind: 'once-per-source' },
207
222
  verifierGated: false,
208
223
  label: 'Post the community backed',
224
+ catalogKey: 'rulePostBacked',
209
225
  },
210
226
  poll_qualified: {
211
227
  category: 'engagement',
@@ -213,6 +229,7 @@ export const SCORE_RULES = {
213
229
  cap: { kind: 'once-per-source' },
214
230
  verifierGated: false,
215
231
  label: 'Poll the community backed',
232
+ catalogKey: 'rulePollBacked',
216
233
  },
217
234
  crowdfunding_qualified: {
218
235
  category: 'civic',
@@ -220,6 +237,7 @@ export const SCORE_RULES = {
220
237
  cap: { kind: 'once-per-source' },
221
238
  verifierGated: false,
222
239
  label: 'Fundraiser the community backed',
240
+ catalogKey: 'ruleFundraiserBacked',
223
241
  },
224
242
  suggestions_complaints_qualified: {
225
243
  category: 'civic',
@@ -227,6 +245,7 @@ export const SCORE_RULES = {
227
245
  cap: { kind: 'once-per-source' },
228
246
  verifierGated: false,
229
247
  label: 'Suggestion/complaint the community backed',
248
+ catalogKey: 'ruleSuggestionBacked',
230
249
  },
231
250
  // A comment the community upvoted past QUALIFYING_UPVOTES_COMMENT (5).
232
251
  // Smaller unit than a post, lower bar, awarded to the comment author
@@ -237,6 +256,8 @@ export const SCORE_RULES = {
237
256
  cap: { kind: 'once-per-source' },
238
257
  verifierGated: false,
239
258
  label: 'Comment the community backed',
259
+ catalogKey: 'ruleCommentBacked',
260
+ isNew: true,
240
261
  },
241
262
  // ─── Leadership ───────────────────────────────────────────────────────
242
263
  // Fires when a user's reform role is elevated through any of the
@@ -250,6 +271,7 @@ export const SCORE_RULES = {
250
271
  cap: { kind: 'once-per-source' },
251
272
  verifierGated: true,
252
273
  label: 'Volunteer role granted',
274
+ catalogKey: 'ruleVolunteer',
253
275
  },
254
276
  // Promotion to a LEADER role. Distinct from `reform_role_granted`
255
277
  // (volunteer, 200) so the two milestones carry their own weight and
@@ -261,6 +283,7 @@ export const SCORE_RULES = {
261
283
  cap: { kind: 'once-per-source' },
262
284
  verifierGated: true,
263
285
  label: 'Leader role granted',
286
+ catalogKey: 'ruleLeader',
264
287
  },
265
288
  // Credited to the LEADER each time a new volunteer is added in their
266
289
  // area. once-per-source keyed on the recruited volunteer's user id —
@@ -271,6 +294,7 @@ export const SCORE_RULES = {
271
294
  cap: { kind: 'once-per-source' },
272
295
  verifierGated: false,
273
296
  label: 'Recruited a volunteer',
297
+ catalogKey: 'ruleRecruit',
274
298
  },
275
299
  // Credited to the recruiting LEADER when a volunteer they added
276
300
  // reaches MENTEE_MILESTONE_SCORE. Fired by the cross-user milestone
@@ -282,6 +306,7 @@ export const SCORE_RULES = {
282
306
  cap: { kind: 'once-per-source' },
283
307
  verifierGated: false,
284
308
  label: 'Mentored a rising volunteer',
309
+ catalogKey: 'ruleMentee',
285
310
  },
286
311
  official_community_run: {
287
312
  category: 'leadership',
@@ -289,41 +314,42 @@ export const SCORE_RULES = {
289
314
  cap: { kind: 'once-per-source' },
290
315
  verifierGated: true,
291
316
  label: 'Official community',
317
+ catalogKey: 'ruleOfficialCommunity',
292
318
  },
293
319
  };
294
- export const SCORE_RULE_CATALOG = [
295
- {
296
- category: 'profile',
297
- groupKey: 'grpProfile',
298
- rules: [{ labelKey: 'ruleProfileComplete', points: '+100' }],
299
- },
300
- {
301
- category: 'engagement',
302
- groupKey: 'grpCommunity',
303
- rules: [
304
- { labelKey: 'rulePostBacked', points: '+5' },
305
- { labelKey: 'ruleCommentBacked', points: '+5', isNew: true },
306
- ],
307
- },
308
- {
309
- category: 'civic',
310
- groupKey: 'grpCivic',
311
- rules: [
312
- { labelKey: 'ruleSuggestionBacked', points: '+7' },
313
- { labelKey: 'ruleDonate', points: '+10' },
314
- { labelKey: 'ruleDeliver', points: '+50' },
315
- { labelKey: 'ruleUdpHelp', points: '+10 / +20 / +10' },
316
- ],
317
- },
318
- {
319
- category: 'leadership',
320
- groupKey: 'grpLeadership',
321
- rules: [
322
- { labelKey: 'ruleVolunteer', points: '+200' },
323
- { labelKey: 'ruleLeader', points: '+500' },
324
- { labelKey: 'ruleRecruit', points: '+10' },
325
- { labelKey: 'ruleMentee', points: '+100' },
326
- ],
327
- },
328
- ];
320
+ /** Display order of the catalog sections (Profile, Community, Civic, Leadership). */
321
+ const CATALOG_GROUP_ORDER = ['profile', 'engagement', 'civic', 'leadership'];
322
+ /** Category → section-heading i18n key. */
323
+ const CATEGORY_GROUP_KEY = {
324
+ profile: 'grpProfile',
325
+ engagement: 'grpCommunity',
326
+ civic: 'grpCivic',
327
+ leadership: 'grpLeadership',
328
+ };
329
+ /**
330
+ * Build the "How you earn points" catalog by projecting SCORE_RULES into
331
+ * display sections. Every non-hidden rule becomes a row under its
332
+ * category, so the catalog is guaranteed to stay in lockstep with the
333
+ * award table — add a rule and it appears automatically.
334
+ */
335
+ export function buildScoreCatalog() {
336
+ const groups = [];
337
+ for (const category of CATALOG_GROUP_ORDER) {
338
+ const rules = [];
339
+ for (const rule of Object.values(SCORE_RULES)) {
340
+ if (rule.category !== category || rule.catalogHidden)
341
+ continue;
342
+ rules.push({
343
+ labelKey: rule.catalogKey,
344
+ points: `+${rule.points}`,
345
+ ...(rule.isNew ? { isNew: true } : {}),
346
+ });
347
+ }
348
+ if (rules.length > 0) {
349
+ groups.push({ category, groupKey: CATEGORY_GROUP_KEY[category], rules });
350
+ }
351
+ }
352
+ return groups;
353
+ }
354
+ export const SCORE_RULE_CATALOG = buildScoreCatalog();
329
355
  //# sourceMappingURL=rules.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rules.js","sourceRoot":"","sources":["../../src/social-score/rules.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Social-score RULES — the single source of truth for every point\n * award across the platform.\n *\n * Why this lives in the schema package\n * ────────────────────────────────────\n * The backend emitter reads `SCORE_RULES[kind]` for the weight,\n * category and cap; it NEVER hardcodes a point value. The frontend\n * \"How you earn points\" reference renders from `SCORE_RULE_CATALOG`.\n * Shipping both from one package means a weight change and the user-\n * facing guidance can't drift across a deploy — bump the package, both\n * consumers pick up the new values on `npm install`.\n *\n * What's in a rule\n * ────────────────\n * category — which breakdown bucket the points roll into.\n * points — base award per emission.\n * cap — optional anti-gaming bound:\n * • `once` — first emission per (user, kind).\n * • `once-per-source` — at most once per (user, kind,\n * sourceRef).\n * • `daily, max: N` — at most N awarding emissions per\n * UTC day; excess still records an event (audit /\n * replay) but awards 0.\n * • undefined → no cap.\n * verifierGated — when true the action MUST be independently verified\n * before emitting (belt-and-braces guard at the rule\n * level so a wiring mistake can't leak points).\n *\n * @module community-schema/social-score/rules\n */\n\nimport type { ScoreCategory } from './categories.js';\nimport type { ScoreKind } from './kinds.js';\n\n// ─── Anti-gaming constants ─────────────────────────────────────────────────\n\n/**\n * Net-upvote bar a piece of community content must clear before its\n * author is awarded a one-time \"qualified\" point bundle. The vote\n * service maintains a denormalised signed `score` (upvotes − downvotes)\n * on every content doc; the engagement hook fires the award the first\n * time that `score` crosses this threshold upward.\n */\nexport const QUALIFYING_UPVOTES = 20;\n\n/**\n * Lower net-upvote bar for COMMENTS — a comment is a smaller unit of\n * contribution than a top-level post, so it qualifies its author at a\n * gentler threshold. Separate constant so the two bars move\n * independently.\n */\nexport const QUALIFYING_UPVOTES_COMMENT = 5;\n\n/**\n * Score a recruited volunteer must reach for the leader who added them\n * to earn the mentoring milestone bonus.\n */\nexport const MENTEE_MILESTONE_SCORE = 500;\n\n/**\n * Minimum total social score an applicant must hold to QUALIFY for a\n * leader role on merit. A manager/superadmin can still approve a\n * sub-threshold applicant (discretionary override) — this gate only\n * blocks a *leader* approving a peer who hasn't earned it.\n */\nexport const LEADER_ELIGIBILITY_SCORE = 1000;\n\n// ─── Rule shape ─────────────────────────────────────────────────────────────\n\nexport type ScoreCap =\n | { kind: 'once' }\n | { kind: 'once-per-source' }\n | { kind: 'daily'; max: number };\n\nexport interface ScoreRule {\n category: ScoreCategory;\n points: number;\n cap?: ScoreCap;\n verifierGated: boolean;\n /** Human-facing fallback label rendered on the breakdown history. */\n label: string;\n}\n\nexport const SCORE_RULES: Record<ScoreKind, ScoreRule> = {\n // ─── Profile ──────────────────────────────────────────────────────────\n udp_approved: {\n category: 'profile',\n points: 100,\n cap: { kind: 'once' },\n verifierGated: true,\n label: 'Profile verified',\n },\n\n // ─── Civic contribution ───────────────────────────────────────────────\n crowdfunding_donation_verified: {\n category: 'civic',\n points: 25,\n cap: { kind: 'daily', max: 10 },\n verifierGated: true,\n label: 'Verified donation',\n },\n crowdfunding_campaign_succeeded: {\n category: 'civic',\n points: 100,\n cap: { kind: 'once-per-source' },\n verifierGated: true,\n label: 'Campaign succeeded',\n },\n item_donated: {\n category: 'civic',\n // Awarded to the DONOR when their item completes the journey to a\n // recipient (terminal DONATED state). The social worth is the\n // completed handover, not upvotes — so this is NOT upvote-gated.\n points: 10,\n cap: { kind: 'daily', max: 5 },\n verifierGated: true,\n label: 'Item donated',\n },\n lost_found_resolved: {\n category: 'civic',\n // Resolution is creator-driven (the report owner approves a\n // claim) — no third-party signal. The daily cap throttles mass-\n // create + self-approve farming via a colluding \"claimant\". The\n // unique idempotency index on (userId, kind, sourceId) still\n // enforces once-per-report (no double-counting on toggle).\n points: 50,\n cap: { kind: 'daily', max: 3 },\n verifierGated: false,\n label: 'Lost & found resolved',\n },\n suggestions_complaints_submitted: {\n category: 'civic',\n points: 10,\n cap: { kind: 'daily', max: 3 },\n verifierGated: false,\n label: 'Suggestions & complaints report',\n },\n suggestions_complaints_resolved: {\n category: 'civic',\n points: 75,\n cap: { kind: 'once-per-source' },\n verifierGated: true,\n label: 'Suggestions & complaints resolved',\n },\n // A lender rated a returned item well (overall >= 4★). Daily-capped to\n // throttle farming; idempotent per borrow-request via the source id.\n resource_returned_well: {\n category: 'civic',\n points: 15,\n cap: { kind: 'daily', max: 5 },\n verifierGated: false,\n label: 'Returned a borrowed item well',\n },\n\n // ─── Civic — volunteer work helping OTHER users ───────────────────────\n // Credited to the VOLUNTEER (the actor), not the donation owner, when\n // they physically deliver a collected donation to the centre\n // (AT_CENTER transition). Gated by that verified transition, so no\n // separate verifierGated flag is needed; once-per-source = one award\n // per donation moved.\n item_pickup_to_center: {\n category: 'civic',\n points: 50,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Delivered a donation to the centre',\n },\n // Credited to the VOLUNTEER who starts someone else's detailed\n // profile in survey/fill mode. once-per-source keyed on the target\n // profile so re-opening the same profile doesn't re-award.\n udp_initiated: {\n category: 'civic',\n points: 10,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Started a profile for someone',\n },\n // Credited to the VOLUNTEER who submits a completed profile they\n // filled for someone else (PENDING_APPROVAL transition in fill mode).\n udp_completed: {\n category: 'civic',\n points: 20,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Completed a profile for someone',\n },\n // Credited to the VERIFIER (leader/official) who approves a detailed\n // profile. Distinct from `udp_approved`, which credits the profile\n // OWNER. once-per-source keyed on the approved profile.\n udp_verified: {\n category: 'civic',\n points: 10,\n cap: { kind: 'once-per-source' },\n verifierGated: true,\n label: 'Verified a profile',\n },\n\n // ─── Engagement ───────────────────────────────────────────────────────\n event_organized: {\n category: 'engagement',\n // Awarded at event creation (no attendance signal yet — that\n // would need an event-completion flow). Daily cap of 2 throttles\n // mass-create farming; per-source uniqueness still enforced by\n // the idempotency index so editing an event isn't a second emit.\n points: 50,\n cap: { kind: 'daily', max: 2 },\n verifierGated: false,\n label: 'Event organized',\n },\n poll_created: {\n category: 'engagement',\n points: 10,\n cap: { kind: 'daily', max: 3 },\n verifierGated: false,\n label: 'Poll created',\n },\n poll_voted: {\n category: 'engagement',\n points: 2,\n cap: { kind: 'daily', max: 10 },\n verifierGated: false,\n label: 'Poll vote',\n },\n\n // ─── Engagement — content the community upvoted past the bar ──────────\n // These fire ONCE per content item, the first time its net vote\n // `score` crosses QUALIFYING_UPVOTES upward (see engagement.service\n // setVote hook). Awarded to the content's author. Anti-farming is\n // structural: you can't earn by self-posting — the community has to\n // actually upvote you. once-per-source keyed on the content id.\n post_qualified: {\n category: 'engagement',\n points: 5,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Post the community backed',\n },\n poll_qualified: {\n category: 'engagement',\n points: 5,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Poll the community backed',\n },\n crowdfunding_qualified: {\n category: 'civic',\n points: 5,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Fundraiser the community backed',\n },\n suggestions_complaints_qualified: {\n category: 'civic',\n points: 7,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Suggestion/complaint the community backed',\n },\n // A comment the community upvoted past QUALIFYING_UPVOTES_COMMENT (5).\n // Smaller unit than a post, lower bar, awarded to the comment author\n // once per comment.\n comment_qualified: {\n category: 'engagement',\n points: 5,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Comment the community backed',\n },\n\n // ─── Leadership ───────────────────────────────────────────────────────\n // Fires when a user's reform role is elevated through any of the\n // application flows (volunteer-application approval, leader\n // promotion). The label stays generic so the breakdown page reads\n // sensibly regardless of which level was actually granted; the\n // metadata field on the event row carries the specific role.\n reform_role_granted: {\n category: 'leadership',\n points: 200,\n cap: { kind: 'once-per-source' },\n verifierGated: true,\n label: 'Volunteer role granted',\n },\n // Promotion to a LEADER role. Distinct from `reform_role_granted`\n // (volunteer, 200) so the two milestones carry their own weight and\n // breakdown line. Eligibility (>= LEADER_ELIGIBILITY_SCORE) is\n // enforced in the leader-application flow, not here.\n leader_role_granted: {\n category: 'leadership',\n points: 500,\n cap: { kind: 'once-per-source' },\n verifierGated: true,\n label: 'Leader role granted',\n },\n // Credited to the LEADER each time a new volunteer is added in their\n // area. once-per-source keyed on the recruited volunteer's user id —\n // one award per person recruited.\n leader_added_volunteer: {\n category: 'leadership',\n points: 10,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Recruited a volunteer',\n },\n // Credited to the recruiting LEADER when a volunteer they added\n // reaches MENTEE_MILESTONE_SCORE. Fired by the cross-user milestone\n // check in the emitter. once-per-source keyed on the mentee's user\n // id — one bonus per mentee who crosses the bar.\n leader_mentee_reached_milestone: {\n category: 'leadership',\n points: 100,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Mentored a rising volunteer',\n },\n official_community_run: {\n category: 'leadership',\n points: 100,\n cap: { kind: 'once-per-source' },\n verifierGated: true,\n label: 'Official community',\n },\n};\n\n// ─── Display catalog (\"How you earn points\") ────────────────────────────────\n// User-facing reference rendered on /profile/score. Mirrors the weights\n// above but groups them for display and carries i18n KEYS (resolved in\n// the `socialScore` namespace) instead of copy. The point figures are\n// numbers-as-data (language-neutral) so they stay here, not in messages.\n\nexport interface ScoreRuleRef {\n /** i18n key (socialScore namespace) for the action description. */\n labelKey: string;\n /** Display reward, e.g. \"+5\" or a multi-step \"+10 / +20 / +10\". */\n points: string;\n /** Marks a freshly-added rule with a \"New\" chip. */\n isNew?: boolean;\n}\n\nexport interface ScoreRuleGroup {\n category: ScoreCategory;\n /** i18n key (socialScore namespace) for the section heading. */\n groupKey: string;\n rules: ScoreRuleRef[];\n}\n\nexport const SCORE_RULE_CATALOG: ScoreRuleGroup[] = [\n {\n category: 'profile',\n groupKey: 'grpProfile',\n rules: [{ labelKey: 'ruleProfileComplete', points: '+100' }],\n },\n {\n category: 'engagement',\n groupKey: 'grpCommunity',\n rules: [\n { labelKey: 'rulePostBacked', points: '+5' },\n { labelKey: 'ruleCommentBacked', points: '+5', isNew: true },\n ],\n },\n {\n category: 'civic',\n groupKey: 'grpCivic',\n rules: [\n { labelKey: 'ruleSuggestionBacked', points: '+7' },\n { labelKey: 'ruleDonate', points: '+10' },\n { labelKey: 'ruleDeliver', points: '+50' },\n { labelKey: 'ruleUdpHelp', points: '+10 / +20 / +10' },\n ],\n },\n {\n category: 'leadership',\n groupKey: 'grpLeadership',\n rules: [\n { labelKey: 'ruleVolunteer', points: '+200' },\n { labelKey: 'ruleLeader', points: '+500' },\n { labelKey: 'ruleRecruit', points: '+10' },\n { labelKey: 'ruleMentee', points: '+100' },\n ],\n },\n];\n"]}
1
+ {"version":3,"file":"rules.js","sourceRoot":"","sources":["../../src/social-score/rules.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Social-score RULES — the single source of truth for every point\n * award across the platform.\n *\n * Why this lives in the schema package\n * ────────────────────────────────────\n * The backend emitter reads `SCORE_RULES[kind]` for the weight,\n * category and cap; it NEVER hardcodes a point value. The frontend\n * \"How you earn points\" reference renders from `SCORE_RULE_CATALOG`.\n * Shipping both from one package means a weight change and the user-\n * facing guidance can't drift across a deploy — bump the package, both\n * consumers pick up the new values on `npm install`.\n *\n * What's in a rule\n * ────────────────\n * category — which breakdown bucket the points roll into.\n * points — base award per emission.\n * cap — optional anti-gaming bound:\n * • `once` — first emission per (user, kind).\n * • `once-per-source` — at most once per (user, kind,\n * sourceRef).\n * • `daily, max: N` — at most N awarding emissions per\n * UTC day; excess still records an event (audit /\n * replay) but awards 0.\n * • undefined → no cap.\n * verifierGated — when true the action MUST be independently verified\n * before emitting (belt-and-braces guard at the rule\n * level so a wiring mistake can't leak points).\n *\n * @module community-schema/social-score/rules\n */\n\nimport type { ScoreCategory } from './categories.js';\nimport type { ScoreKind } from './kinds.js';\n\n// ─── Anti-gaming constants ─────────────────────────────────────────────────\n\n/**\n * Net-upvote bar a piece of community content must clear before its\n * author is awarded a one-time \"qualified\" point bundle. The vote\n * service maintains a denormalised signed `score` (upvotes − downvotes)\n * on every content doc; the engagement hook fires the award the first\n * time that `score` crosses this threshold upward.\n */\nexport const QUALIFYING_UPVOTES = 20;\n\n/**\n * Lower net-upvote bar for COMMENTS — a comment is a smaller unit of\n * contribution than a top-level post, so it qualifies its author at a\n * gentler threshold. Separate constant so the two bars move\n * independently.\n */\nexport const QUALIFYING_UPVOTES_COMMENT = 5;\n\n/**\n * Score a recruited volunteer must reach for the leader who added them\n * to earn the mentoring milestone bonus.\n */\nexport const MENTEE_MILESTONE_SCORE = 500;\n\n/**\n * Minimum total social score an applicant must hold to QUALIFY for a\n * leader role on merit. A manager/superadmin can still approve a\n * sub-threshold applicant (discretionary override) — this gate only\n * blocks a *leader* approving a peer who hasn't earned it.\n */\nexport const LEADER_ELIGIBILITY_SCORE = 1000;\n\n// ─── Rule shape ─────────────────────────────────────────────────────────────\n\nexport type ScoreCap =\n | { kind: 'once' }\n | { kind: 'once-per-source' }\n | { kind: 'daily'; max: number };\n\nexport interface ScoreRule {\n category: ScoreCategory;\n points: number;\n cap?: ScoreCap;\n verifierGated: boolean;\n /** Human-facing fallback label rendered on the breakdown history. */\n label: string;\n /**\n * i18n key (socialScore namespace) for the \"How you earn points\"\n * catalog row. Every awarding rule carries one so the catalog is\n * DERIVED from this table (see `buildScoreCatalog`) and can never\n * drift out of sync with what actually earns points.\n */\n catalogKey: string;\n /** Show a \"New\" chip on the catalog row. */\n isNew?: boolean;\n /**\n * Exclude this rule from the user-facing catalog while STILL awarding\n * points (e.g. an internal/automatic award). Default: shown.\n */\n catalogHidden?: boolean;\n}\n\nexport const SCORE_RULES: Record<ScoreKind, ScoreRule> = {\n // ─── Profile ──────────────────────────────────────────────────────────\n udp_approved: {\n category: 'profile',\n points: 100,\n cap: { kind: 'once' },\n verifierGated: true,\n label: 'Profile verified',\n catalogKey: 'ruleProfileComplete',\n },\n\n // ─── Civic contribution ───────────────────────────────────────────────\n crowdfunding_donation_verified: {\n category: 'civic',\n points: 25,\n cap: { kind: 'daily', max: 10 },\n verifierGated: true,\n label: 'Verified donation',\n catalogKey: 'ruleDonateFundraiser',\n },\n crowdfunding_campaign_succeeded: {\n category: 'civic',\n points: 100,\n cap: { kind: 'once-per-source' },\n verifierGated: true,\n label: 'Campaign succeeded',\n catalogKey: 'ruleFundraiserSucceeded',\n },\n item_donated: {\n category: 'civic',\n // Awarded to the DONOR when their item completes the journey to a\n // recipient (terminal DONATED state). The social worth is the\n // completed handover, not upvotes — so this is NOT upvote-gated.\n points: 10,\n cap: { kind: 'daily', max: 5 },\n verifierGated: true,\n label: 'Item donated',\n catalogKey: 'ruleDonate',\n },\n lost_found_resolved: {\n category: 'civic',\n // Resolution is creator-driven (the report owner approves a\n // claim) — no third-party signal. The daily cap throttles mass-\n // create + self-approve farming via a colluding \"claimant\". The\n // unique idempotency index on (userId, kind, sourceId) still\n // enforces once-per-report (no double-counting on toggle).\n points: 50,\n cap: { kind: 'daily', max: 3 },\n verifierGated: false,\n label: 'Lost & found resolved',\n catalogKey: 'ruleLostFoundResolved',\n },\n suggestions_complaints_submitted: {\n category: 'civic',\n points: 10,\n cap: { kind: 'daily', max: 3 },\n verifierGated: false,\n label: 'Suggestions & complaints report',\n catalogKey: 'ruleSubmitReport',\n },\n suggestions_complaints_resolved: {\n category: 'civic',\n points: 75,\n cap: { kind: 'once-per-source' },\n verifierGated: true,\n label: 'Suggestions & complaints resolved',\n catalogKey: 'ruleReportResolved',\n },\n // A lender rated a returned item well (overall >= 4★). Daily-capped to\n // throttle farming; idempotent per borrow-request via the source id.\n resource_returned_well: {\n category: 'civic',\n points: 15,\n cap: { kind: 'daily', max: 5 },\n verifierGated: false,\n label: 'Returned a borrowed item well',\n catalogKey: 'ruleReturnResource',\n },\n\n // ─── Civic — volunteer work helping OTHER users ───────────────────────\n // Credited to the VOLUNTEER (the actor), not the donation owner, when\n // they physically deliver a collected donation to the centre\n // (AT_CENTER transition). Gated by that verified transition, so no\n // separate verifierGated flag is needed; once-per-source = one award\n // per donation moved.\n item_pickup_to_center: {\n category: 'civic',\n points: 50,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Delivered a donation to the centre',\n catalogKey: 'ruleDeliver',\n },\n // Credited to the VOLUNTEER who starts someone else's detailed\n // profile in survey/fill mode. once-per-source keyed on the target\n // profile so re-opening the same profile doesn't re-award.\n udp_initiated: {\n category: 'civic',\n points: 10,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Started a profile for someone',\n catalogKey: 'ruleUdpStart',\n },\n // Credited to the VOLUNTEER who submits a completed profile they\n // filled for someone else (PENDING_APPROVAL transition in fill mode).\n udp_completed: {\n category: 'civic',\n points: 20,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Completed a profile for someone',\n catalogKey: 'ruleUdpFinish',\n },\n // Credited to the VERIFIER (leader/official) who approves a detailed\n // profile. Distinct from `udp_approved`, which credits the profile\n // OWNER. once-per-source keyed on the approved profile.\n udp_verified: {\n category: 'civic',\n points: 10,\n cap: { kind: 'once-per-source' },\n verifierGated: true,\n label: 'Verified a profile',\n catalogKey: 'ruleUdpVerify',\n },\n\n // ─── Engagement ───────────────────────────────────────────────────────\n event_organized: {\n category: 'engagement',\n // Awarded at event creation (no attendance signal yet — that\n // would need an event-completion flow). Daily cap of 2 throttles\n // mass-create farming; per-source uniqueness still enforced by\n // the idempotency index so editing an event isn't a second emit.\n points: 50,\n cap: { kind: 'daily', max: 2 },\n verifierGated: false,\n label: 'Event organized',\n catalogKey: 'ruleEventOrganized',\n },\n poll_created: {\n category: 'engagement',\n points: 10,\n cap: { kind: 'daily', max: 3 },\n verifierGated: false,\n label: 'Poll created',\n catalogKey: 'rulePollCreated',\n },\n poll_voted: {\n category: 'engagement',\n points: 2,\n cap: { kind: 'daily', max: 10 },\n verifierGated: false,\n label: 'Poll vote',\n catalogKey: 'rulePollVoted',\n },\n\n // ─── Engagement — content the community upvoted past the bar ──────────\n // These fire ONCE per content item, the first time its net vote\n // `score` crosses QUALIFYING_UPVOTES upward (see engagement.service\n // setVote hook). Awarded to the content's author. Anti-farming is\n // structural: you can't earn by self-posting — the community has to\n // actually upvote you. once-per-source keyed on the content id.\n post_qualified: {\n category: 'engagement',\n points: 5,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Post the community backed',\n catalogKey: 'rulePostBacked',\n },\n poll_qualified: {\n category: 'engagement',\n points: 5,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Poll the community backed',\n catalogKey: 'rulePollBacked',\n },\n crowdfunding_qualified: {\n category: 'civic',\n points: 5,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Fundraiser the community backed',\n catalogKey: 'ruleFundraiserBacked',\n },\n suggestions_complaints_qualified: {\n category: 'civic',\n points: 7,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Suggestion/complaint the community backed',\n catalogKey: 'ruleSuggestionBacked',\n },\n // A comment the community upvoted past QUALIFYING_UPVOTES_COMMENT (5).\n // Smaller unit than a post, lower bar, awarded to the comment author\n // once per comment.\n comment_qualified: {\n category: 'engagement',\n points: 5,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Comment the community backed',\n catalogKey: 'ruleCommentBacked',\n isNew: true,\n },\n\n // ─── Leadership ───────────────────────────────────────────────────────\n // Fires when a user's reform role is elevated through any of the\n // application flows (volunteer-application approval, leader\n // promotion). The label stays generic so the breakdown page reads\n // sensibly regardless of which level was actually granted; the\n // metadata field on the event row carries the specific role.\n reform_role_granted: {\n category: 'leadership',\n points: 200,\n cap: { kind: 'once-per-source' },\n verifierGated: true,\n label: 'Volunteer role granted',\n catalogKey: 'ruleVolunteer',\n },\n // Promotion to a LEADER role. Distinct from `reform_role_granted`\n // (volunteer, 200) so the two milestones carry their own weight and\n // breakdown line. Eligibility (>= LEADER_ELIGIBILITY_SCORE) is\n // enforced in the leader-application flow, not here.\n leader_role_granted: {\n category: 'leadership',\n points: 500,\n cap: { kind: 'once-per-source' },\n verifierGated: true,\n label: 'Leader role granted',\n catalogKey: 'ruleLeader',\n },\n // Credited to the LEADER each time a new volunteer is added in their\n // area. once-per-source keyed on the recruited volunteer's user id —\n // one award per person recruited.\n leader_added_volunteer: {\n category: 'leadership',\n points: 10,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Recruited a volunteer',\n catalogKey: 'ruleRecruit',\n },\n // Credited to the recruiting LEADER when a volunteer they added\n // reaches MENTEE_MILESTONE_SCORE. Fired by the cross-user milestone\n // check in the emitter. once-per-source keyed on the mentee's user\n // id — one bonus per mentee who crosses the bar.\n leader_mentee_reached_milestone: {\n category: 'leadership',\n points: 100,\n cap: { kind: 'once-per-source' },\n verifierGated: false,\n label: 'Mentored a rising volunteer',\n catalogKey: 'ruleMentee',\n },\n official_community_run: {\n category: 'leadership',\n points: 100,\n cap: { kind: 'once-per-source' },\n verifierGated: true,\n label: 'Official community',\n catalogKey: 'ruleOfficialCommunity',\n },\n};\n\n// ─── Display catalog (\"How you earn points\") ────────────────────────────────\n// DERIVED from SCORE_RULES (see `buildScoreCatalog`) so the user-facing\n// \"How you earn points\" surface lists EVERY rule that actually awards\n// points — it can never silently omit an earning path. Labels are i18n\n// KEYS (resolved in the `socialScore` namespace); point figures are\n// numbers-as-data derived straight from each rule's `points`.\n\nexport interface ScoreRuleRef {\n /** i18n key (socialScore namespace) for the action description. */\n labelKey: string;\n /** Display reward derived from the rule, e.g. \"+5\". */\n points: string;\n /** Marks a freshly-added rule with a \"New\" chip. */\n isNew?: boolean;\n}\n\nexport interface ScoreRuleGroup {\n category: ScoreCategory;\n /** i18n key (socialScore namespace) for the section heading. */\n groupKey: string;\n rules: ScoreRuleRef[];\n}\n\n/** Display order of the catalog sections (Profile, Community, Civic, Leadership). */\nconst CATALOG_GROUP_ORDER: ScoreCategory[] = ['profile', 'engagement', 'civic', 'leadership'];\n\n/** Category → section-heading i18n key. */\nconst CATEGORY_GROUP_KEY: Record<ScoreCategory, string> = {\n profile: 'grpProfile',\n engagement: 'grpCommunity',\n civic: 'grpCivic',\n leadership: 'grpLeadership',\n};\n\n/**\n * Build the \"How you earn points\" catalog by projecting SCORE_RULES into\n * display sections. Every non-hidden rule becomes a row under its\n * category, so the catalog is guaranteed to stay in lockstep with the\n * award table — add a rule and it appears automatically.\n */\nexport function buildScoreCatalog(): ScoreRuleGroup[] {\n const groups: ScoreRuleGroup[] = [];\n for (const category of CATALOG_GROUP_ORDER) {\n const rules: ScoreRuleRef[] = [];\n for (const rule of Object.values(SCORE_RULES)) {\n if (rule.category !== category || rule.catalogHidden) continue;\n rules.push({\n labelKey: rule.catalogKey,\n points: `+${rule.points}`,\n ...(rule.isNew ? { isNew: true } : {}),\n });\n }\n if (rules.length > 0) {\n groups.push({ category, groupKey: CATEGORY_GROUP_KEY[category], rules });\n }\n }\n return groups;\n}\n\nexport const SCORE_RULE_CATALOG: ScoreRuleGroup[] = buildScoreCatalog();\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jansathi-community-schema",
3
- "version": "0.50.0",
3
+ "version": "0.50.1",
4
4
  "description": "Shared Zod schemas + TypeScript types for the Jansathi hyperlocal community feature (feed, posts, engagement, social graph, groups, communities).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",