kyd-shared-badge 0.2.32 → 0.2.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyd-shared-badge",
3
- "version": "0.2.32",
3
+ "version": "0.2.34",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -479,6 +479,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
479
479
  sources={graphInsights.business_rules_all as any}
480
480
  searchedAt={updatedAt}
481
481
  developerName={developerName || 'this developer'}
482
+ genreMapping={genreMapping as Record<string, string[]>}
482
483
  />
483
484
  </div>
484
485
  </Reveal>
@@ -36,6 +36,8 @@ interface AppendixTableProps {
36
36
  sources: (string | DomainCSVRow | SanctionSource | BusinessRuleRow)[];
37
37
  searchedAt: string;
38
38
  developerName: string;
39
+ // Used for business_rules: map genres (e.g., Technical/Risk) to category arrays
40
+ genreMapping?: Record<string, string[]>;
39
41
  }
40
42
 
41
43
  const SanctionsRow = ({
@@ -125,7 +127,7 @@ const DomainRow = ({ source, searchedAt, developerName }: { source: DomainSource
125
127
  );
126
128
 
127
129
 
128
- const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedAt, developerName }) => {
130
+ const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedAt, developerName, genreMapping }) => {
129
131
  const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
130
132
  const [expanded, setExpanded] = useState<{ [k: number]: boolean }>({});
131
133
 
@@ -160,7 +162,22 @@ const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedA
160
162
  ? ["Issuing Entity", "List Name", "List Searched On", "Value", "Findings"]
161
163
  : type === 'domains'
162
164
  ? ["Country", "Entity Type", "Entity/Domain", "Searched On", "Value", "Findings"]
163
- : ["Platform", "Category", "Observation", "Imapct"];
165
+ : ["Platform", "KYD Pillar", "Category", "Observation", "Weight Impact"];
166
+
167
+ // Build quick lookup: category -> KYD Pillar (e.g., Technical or Risk)
168
+ const categoryToPillar: Record<string, string> = (() => {
169
+ const map: Record<string, string> = {};
170
+ try {
171
+ const gm = genreMapping || {};
172
+ Object.keys(gm || {}).forEach((genre) => {
173
+ const cats = (gm as Record<string, string[]>)[genre] || [];
174
+ cats.forEach((c) => {
175
+ if (typeof c === 'string') map[c] = genre;
176
+ });
177
+ });
178
+ } catch {}
179
+ return map;
180
+ })();
164
181
 
165
182
  const parsedSources = type === 'sanctions'
166
183
  ? (sources).map((src) => {
@@ -179,18 +196,32 @@ const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedA
179
196
  }))
180
197
  : (sources as BusinessRuleRow[]).map(s => ({
181
198
  provider: s.provider,
199
+ // Keep original category label; pillar derived from genreMapping
182
200
  category: s.category || '',
183
201
  label: s.label || '',
184
202
  likert_label: s.likert_label || '',
185
203
  likert_value: s.likert_value || 0,
186
204
  weight: s.weight || 0,
187
205
  uid: s.uid || '',
206
+ pillar: categoryToPillar[(s.category || '')] || '',
188
207
  }));
189
208
 
190
209
  const sortedParsedSources = (type === 'sanctions')
191
210
  ? (parsedSources as SanctionSource[]).slice().sort((a, b) => (b.matched === true ? 1 : 0) - (a.matched === true ? 1 : 0))
192
211
  : type === 'business_rules'
193
- ? (parsedSources as BusinessRuleRow[]).slice().sort((a, b) => (Number(b.weight || 0) - Number(a.weight || 0)))
212
+ ? (() => {
213
+ const orderForPillar = (p: string) => (p === 'Technical' ? 0 : p === 'Risk' ? 1 : 2);
214
+ return (parsedSources as (BusinessRuleRow & { pillar?: string })[]).slice().sort((a, b) => {
215
+ const ap = (a as any).pillar || '';
216
+ const bp = (b as any).pillar || '';
217
+ const cmpPillar = orderForPillar(ap) - orderForPillar(bp);
218
+ if (cmpPillar !== 0) return cmpPillar;
219
+ const ac = (a.category || '').localeCompare(b.category || '');
220
+ if (ac !== 0) return ac;
221
+ // Within same category, sort by descending weight for usefulness
222
+ return Number(b.weight || 0) - Number(a.weight || 0);
223
+ });
224
+ })()
194
225
  : parsedSources;
195
226
 
196
227
  const visibleParsedSources = (type === 'business_rules')
@@ -242,7 +273,7 @@ const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedA
242
273
  if (type === 'domains') {
243
274
  return <DomainRow key={index} source={source as DomainSource} searchedAt={formattedDate} developerName={developerName} />
244
275
  }
245
- const br = source as BusinessRuleRow;
276
+ const br = source as BusinessRuleRow & { pillar?: string };
246
277
  const anchorId = br.uid ? `rule-${br.uid}` : undefined;
247
278
  const ProviderIcon = ({ name }: { name?: string }) => {
248
279
  const n = (name || '').toLowerCase();
@@ -258,18 +289,18 @@ const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedA
258
289
  };
259
290
  return (
260
291
  <tr id={anchorId} key={index} className={'transition-colors hover:bg-black/5'}>
261
- <td className={'px-4 py-4 whitespace-normal text-sm w-1/4'} style={{ color: 'var(--text-secondary)' }}>
292
+ <td className={'px-4 py-4 whitespace-normal text-sm'} style={{ color: 'var(--text-secondary)' }}>
262
293
  <span title={br.provider || ''} className={'inline-flex items-center text-4xl'} style={{ color: 'var(--text-main)' }}>
263
294
  <ProviderIcon name={br.provider} />
264
295
  </span>
265
296
  </td>
266
- <td className={'px-4 py-4 whitespace-normal text-sm w-1/4'} style={{ color: 'var(--text-secondary)' }}>{br.category || '—'}</td>
267
- <td className={'px-4 py-4 whitespace-normal text-sm font-medium w-1/4'} style={{ color: 'var(--text-main)' }}>{br.label || '—'}</td>
268
- <td className={'px-4 py-4 whitespace-normal text-sm w-1/4'} style={{ color: 'var(--text-secondary)' }}>{(() => {
269
- const weight = br.weight || 0;
270
- if (weight > 0) return 'Positive';
271
- if (weight < 0) return 'Negative';
272
- return '';
297
+ <td className={'px-4 py-4 whitespace-normal text-sm'} style={{ color: 'var(--text-secondary)' }}>{br.pillar || '—'}</td>
298
+ <td className={'px-4 py-4 whitespace-normal text-sm'} style={{ color: 'var(--text-secondary)' }}>{br.category || '—'}</td>
299
+ <td className={'px-4 py-4 whitespace-normal text-sm font-medium'} style={{ color: 'var(--text-main)' }}>{br.label || '—'}</td>
300
+ <td className={'px-4 py-4 whitespace-normal text-sm'} style={{ color: 'var(--text-secondary)' }}>{(() => {
301
+ const weight = Number(br.weight || 0);
302
+ if (!Number.isFinite(weight)) return '';
303
+ return weight.toFixed(3).replace(/\.000$/, '.0');
273
304
  })()}</td>
274
305
  </tr>
275
306
  );
@@ -67,7 +67,7 @@ const CategoryBars: React.FC<CategoryBarsProps> = ({
67
67
  }}
68
68
  >
69
69
  {/* signed fill originating from center */}
70
- <div className="absolute top-0 h-full" style={{ left, width: `${fillWidth}%`, backgroundColor: 'var(--bubble-foreground)' }} />
70
+ <div className="absolute top-0 h-full" style={{ left, width: `${fillWidth}%`, backgroundColor: isNegative ? 'var(--status-negative, #EC6662)' : 'var(--status-positive, #02a389)' }} />
71
71
  </div>
72
72
  <div className="hidden group-hover:block absolute z-30 left-1/2 -translate-x-1/2 top-full mt-2 w-80">
73
73
  <div