@forwardimpact/libsyntheticrender 0.1.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.
package/validate.js ADDED
@@ -0,0 +1,535 @@
1
+ /**
2
+ * Cross-content validation — pure function over the entity graph.
3
+ *
4
+ * @module libuniverse/validate
5
+ */
6
+
7
+ /**
8
+ * Validate cross-content integrity of generated entities.
9
+ * @param {object} entities
10
+ * @returns {{passed: boolean, total: number, failures: number, checks: object[]}}
11
+ */
12
+ export function validateCrossContent(entities) {
13
+ const checks = [
14
+ checkPeopleCoverage(entities),
15
+ checkPathwayValidity(entities),
16
+ checkRosterCompleteness(entities),
17
+ checkTeamAssignments(entities),
18
+ checkManagerReferences(entities),
19
+ checkGithubUsernames(entities),
20
+ checkWebhookPayloadSchemas(entities),
21
+ checkWebhookDeliveryIds(entities),
22
+ checkWebhookSenderUsernames(entities),
23
+ checkGetDXTeamsResponse(entities),
24
+ checkGetDXSnapshotsListResponse(entities),
25
+ checkGetDXSnapshotsInfoResponses(entities),
26
+ checkSnapshotScoreDriverIds(entities),
27
+ checkScoreTrajectories(entities),
28
+ checkEvidenceProficiency(entities),
29
+ checkEvidenceSkillIds(entities),
30
+ checkInitiativeScorecardRefs(entities),
31
+ checkInitiativeOwnerEmails(entities),
32
+ checkInitiativeDriverRefs(entities),
33
+ checkCommentSnapshotRefs(entities),
34
+ checkCommentEmailRefs(entities),
35
+ checkCommentTeamRefs(entities),
36
+ checkScorecardCheckIds(entities),
37
+ checkRosterSnapshotQuarters(entities),
38
+ checkProjectTeamEmails(entities),
39
+ ];
40
+
41
+ const failures = checks.filter((c) => !c.passed);
42
+ return {
43
+ passed: failures.length === 0,
44
+ total: checks.length,
45
+ failures: failures.length,
46
+ checks,
47
+ };
48
+ }
49
+
50
+ // ─── Check functions ─────────────────────────────
51
+
52
+ function checkPeopleCoverage(entities) {
53
+ const teamIds = new Set(entities.teams.map((t) => t.id));
54
+ const uncovered = entities.people.filter((p) => !teamIds.has(p.team_id));
55
+ return {
56
+ name: "people_coverage",
57
+ passed: uncovered.length === 0,
58
+ message:
59
+ uncovered.length === 0
60
+ ? "All people assigned to valid teams"
61
+ : `${uncovered.length} people assigned to unknown teams`,
62
+ };
63
+ }
64
+
65
+ function checkPathwayValidity(entities) {
66
+ const fw = entities.framework;
67
+ const hasFramework = fw && fw.proficiencies && fw.proficiencies.length > 0;
68
+
69
+ // Check for extended pathway structure (capabilities as objects with skills)
70
+ const hasPathwayEntities =
71
+ fw?.capabilities?.length > 0 && typeof fw.capabilities[0] === "object";
72
+
73
+ if (!hasPathwayEntities) {
74
+ // Legacy flat-array format — just check proficiencies exist
75
+ return {
76
+ name: "pathway_validity",
77
+ passed: !!hasFramework,
78
+ message: hasFramework
79
+ ? "Framework config present with proficiencies"
80
+ : "Missing framework configuration or proficiencies",
81
+ };
82
+ }
83
+
84
+ // Extended pathway — validate cross-references
85
+ const errors = [];
86
+ const skillIds = new Set(fw.capabilities.flatMap((c) => c.skills || []));
87
+ const behaviourIds = new Set(fw.behaviours.map((b) => b.id));
88
+
89
+ // Check discipline skill references
90
+ for (const disc of fw.disciplines || []) {
91
+ for (const skillId of [
92
+ ...(disc.core || []),
93
+ ...(disc.supporting || []),
94
+ ...(disc.broad || []),
95
+ ]) {
96
+ if (!skillIds.has(skillId)) {
97
+ errors.push(
98
+ `Discipline '${disc.id}' references unknown skill '${skillId}'`,
99
+ );
100
+ }
101
+ }
102
+ }
103
+
104
+ // Check driver skill/behaviour references
105
+ for (const driver of fw.drivers || []) {
106
+ for (const skillId of driver.skills || []) {
107
+ if (!skillIds.has(skillId)) {
108
+ errors.push(
109
+ `Driver '${driver.id}' references unknown skill '${skillId}'`,
110
+ );
111
+ }
112
+ }
113
+ for (const behId of driver.behaviours || []) {
114
+ if (!behaviourIds.has(behId)) {
115
+ errors.push(
116
+ `Driver '${driver.id}' references unknown behaviour '${behId}'`,
117
+ );
118
+ }
119
+ }
120
+ }
121
+
122
+ return {
123
+ name: "pathway_validity",
124
+ passed: errors.length === 0,
125
+ message:
126
+ errors.length === 0
127
+ ? "Pathway data structure valid with cross-references"
128
+ : `Pathway errors: ${errors.join("; ")}`,
129
+ };
130
+ }
131
+
132
+ function checkRosterCompleteness(entities) {
133
+ const roster = entities.activity?.roster || [];
134
+ const hasAll = entities.people.every((p) =>
135
+ roster.some((r) => r.email === p.email),
136
+ );
137
+ return {
138
+ name: "roster_completeness",
139
+ passed: hasAll,
140
+ message: hasAll
141
+ ? "All people present in activity roster"
142
+ : "Some people missing from activity roster",
143
+ };
144
+ }
145
+
146
+ function checkTeamAssignments(entities) {
147
+ const teamSizes = new Map();
148
+ for (const person of entities.people) {
149
+ teamSizes.set(person.team_id, (teamSizes.get(person.team_id) || 0) + 1);
150
+ }
151
+ const emptyTeams = entities.teams.filter(
152
+ (t) => (teamSizes.get(t.id) || 0) === 0,
153
+ );
154
+ return {
155
+ name: "team_assignments",
156
+ passed: emptyTeams.length === 0,
157
+ message:
158
+ emptyTeams.length === 0
159
+ ? "All teams have at least one member"
160
+ : `${emptyTeams.length} teams have no members`,
161
+ };
162
+ }
163
+
164
+ function checkManagerReferences(entities) {
165
+ const managers = entities.people.filter((p) => p.is_manager);
166
+ const teamIds = new Set(entities.teams.map((t) => t.id));
167
+ const orphaned = managers.filter((m) => !teamIds.has(m.team_id));
168
+ return {
169
+ name: "manager_references",
170
+ passed: orphaned.length === 0,
171
+ message:
172
+ orphaned.length === 0
173
+ ? "All managers reference valid teams"
174
+ : `${orphaned.length} managers reference unknown teams`,
175
+ };
176
+ }
177
+
178
+ function checkGithubUsernames(entities) {
179
+ const usernames = entities.people.map((p) => p.github).filter(Boolean);
180
+ const unique = new Set(usernames);
181
+ return {
182
+ name: "github_usernames",
183
+ passed: unique.size === usernames.length,
184
+ message:
185
+ unique.size === usernames.length
186
+ ? "All GitHub usernames are unique"
187
+ : `${usernames.length - unique.size} duplicate GitHub usernames`,
188
+ };
189
+ }
190
+
191
+ function checkWebhookPayloadSchemas(entities) {
192
+ const webhooks = entities.activity?.webhooks || [];
193
+ const invalid = webhooks.filter(
194
+ (w) =>
195
+ !w.delivery_id ||
196
+ !w.event_type ||
197
+ !w.payload?.repository ||
198
+ !w.payload?.sender,
199
+ );
200
+ return {
201
+ name: "webhook_payload_schemas",
202
+ passed: invalid.length === 0,
203
+ message:
204
+ invalid.length === 0
205
+ ? `All ${webhooks.length} webhooks have valid schemas`
206
+ : `${invalid.length} webhooks missing required fields`,
207
+ };
208
+ }
209
+
210
+ function checkWebhookDeliveryIds(entities) {
211
+ const webhooks = entities.activity?.webhooks || [];
212
+ const ids = webhooks.map((w) => w.delivery_id);
213
+ const unique = new Set(ids);
214
+ return {
215
+ name: "webhook_delivery_ids",
216
+ passed: unique.size === ids.length,
217
+ message:
218
+ unique.size === ids.length
219
+ ? "All webhook delivery IDs are unique"
220
+ : `${ids.length - unique.size} duplicate webhook delivery IDs`,
221
+ };
222
+ }
223
+
224
+ function checkWebhookSenderUsernames(entities) {
225
+ const webhooks = entities.activity?.webhooks || [];
226
+ const knownUsernames = new Set(entities.people.map((p) => p.github));
227
+ const unknown = webhooks.filter(
228
+ (w) =>
229
+ w.payload?.sender?.login && !knownUsernames.has(w.payload.sender.login),
230
+ );
231
+ return {
232
+ name: "webhook_sender_usernames",
233
+ passed: unknown.length === 0,
234
+ message:
235
+ unknown.length === 0
236
+ ? "All webhook senders are known users"
237
+ : `${unknown.length} webhooks from unknown senders`,
238
+ };
239
+ }
240
+
241
+ function checkGetDXTeamsResponse(entities) {
242
+ const teams = entities.activity?.activityTeams || [];
243
+ const hasRequired = teams.every((t) => t.getdx_team_id && t.name);
244
+ return {
245
+ name: "getdx_teams_response",
246
+ passed: hasRequired && teams.length > 0,
247
+ message:
248
+ hasRequired && teams.length > 0
249
+ ? `${teams.length} GetDX teams with valid structure`
250
+ : "GetDX teams response missing or invalid",
251
+ };
252
+ }
253
+
254
+ function checkGetDXSnapshotsListResponse(entities) {
255
+ const snapshots = entities.activity?.snapshots || [];
256
+ const hasRequired = snapshots.every(
257
+ (s) => s.snapshot_id && s.scheduled_for && s.completed_at,
258
+ );
259
+ return {
260
+ name: "getdx_snapshots_list_response",
261
+ passed: hasRequired && snapshots.length > 0,
262
+ message:
263
+ hasRequired && snapshots.length > 0
264
+ ? `${snapshots.length} snapshots with valid structure`
265
+ : "GetDX snapshots list response missing or invalid",
266
+ };
267
+ }
268
+
269
+ function checkGetDXSnapshotsInfoResponses(entities) {
270
+ const scores = entities.activity?.scores || [];
271
+ const hasRequired = scores.every(
272
+ (s) =>
273
+ s.snapshot_id &&
274
+ s.getdx_team_id &&
275
+ s.item_id &&
276
+ typeof s.score === "number",
277
+ );
278
+ return {
279
+ name: "getdx_snapshots_info_responses",
280
+ passed: hasRequired && scores.length > 0,
281
+ message:
282
+ hasRequired && scores.length > 0
283
+ ? `${scores.length} snapshot scores with valid structure`
284
+ : "GetDX snapshot scores missing or invalid",
285
+ };
286
+ }
287
+
288
+ function checkSnapshotScoreDriverIds(entities) {
289
+ const scores = entities.activity?.scores || [];
290
+ const VALID_DRIVERS = new Set([
291
+ "clear_direction",
292
+ "say_on_priorities",
293
+ "requirements_quality",
294
+ "ease_of_release",
295
+ "test_efficiency",
296
+ "managing_tech_debt",
297
+ "code_review",
298
+ "documentation",
299
+ "codebase_experience",
300
+ "incident_response",
301
+ "learning_culture",
302
+ "experimentation",
303
+ "connectedness",
304
+ "efficient_processes",
305
+ "deep_work",
306
+ "leveraging_user_feedback",
307
+ ]);
308
+ const invalid = scores.filter((s) => !VALID_DRIVERS.has(s.item_id));
309
+ return {
310
+ name: "snapshot_score_driver_ids",
311
+ passed: invalid.length === 0,
312
+ message:
313
+ invalid.length === 0
314
+ ? "All score driver IDs are valid"
315
+ : `${invalid.length} scores with unknown driver IDs`,
316
+ };
317
+ }
318
+
319
+ function checkScoreTrajectories(entities) {
320
+ const scores = entities.activity?.scores || [];
321
+ const outOfRange = scores.filter((s) => s.score < 0 || s.score > 100);
322
+ return {
323
+ name: "score_trajectories",
324
+ passed: outOfRange.length === 0,
325
+ message:
326
+ outOfRange.length === 0
327
+ ? "All scores within 0–100 range"
328
+ : `${outOfRange.length} scores out of 0–100 range`,
329
+ };
330
+ }
331
+
332
+ function checkEvidenceProficiency(entities) {
333
+ const evidence = entities.activity?.evidence || [];
334
+ const VALID_PROFICIENCIES = new Set([
335
+ "awareness",
336
+ "foundational",
337
+ "working",
338
+ "practitioner",
339
+ "expert",
340
+ ]);
341
+ const invalid = evidence.filter(
342
+ (e) => e.proficiency && !VALID_PROFICIENCIES.has(e.proficiency),
343
+ );
344
+ return {
345
+ name: "evidence_proficiency",
346
+ passed: invalid.length === 0,
347
+ message:
348
+ invalid.length === 0
349
+ ? "All evidence proficiency levels are valid"
350
+ : `${invalid.length} evidence entries with invalid proficiency`,
351
+ };
352
+ }
353
+
354
+ function checkEvidenceSkillIds(entities) {
355
+ const evidence = entities.activity?.evidence || [];
356
+ const hasIds = evidence.every((e) => e.skill_id);
357
+ return {
358
+ name: "evidence_skill_ids",
359
+ passed: hasIds || evidence.length === 0,
360
+ message:
361
+ hasIds || evidence.length === 0
362
+ ? "All evidence entries have skill IDs"
363
+ : "Some evidence entries missing skill IDs",
364
+ };
365
+ }
366
+
367
+ function checkInitiativeScorecardRefs(entities) {
368
+ const initiatives = entities.activity?.initiatives || [];
369
+ const scorecardIds = new Set(
370
+ (entities.activity?.scorecards || []).map((s) => s.id),
371
+ );
372
+ const invalid = initiatives.filter(
373
+ (i) => i.scorecard_id && !scorecardIds.has(i.scorecard_id),
374
+ );
375
+ return {
376
+ name: "initiative_scorecard_refs",
377
+ passed: invalid.length === 0,
378
+ message:
379
+ invalid.length === 0
380
+ ? "All initiative scorecard references are valid"
381
+ : `${invalid.length} initiatives reference unknown scorecards`,
382
+ };
383
+ }
384
+
385
+ function checkInitiativeOwnerEmails(entities) {
386
+ const initiatives = entities.activity?.initiatives || [];
387
+ const emails = new Set(entities.people.map((p) => p.email));
388
+ const invalid = initiatives.filter(
389
+ (i) => i.owner?.email && !emails.has(i.owner.email),
390
+ );
391
+ return {
392
+ name: "initiative_owner_emails",
393
+ passed: invalid.length === 0,
394
+ message:
395
+ invalid.length === 0
396
+ ? "All initiative owners are known people"
397
+ : `${invalid.length} initiatives with unknown owner emails`,
398
+ };
399
+ }
400
+
401
+ function checkInitiativeDriverRefs(entities) {
402
+ const initiatives = entities.activity?.initiatives || [];
403
+ const driverIds = new Set(
404
+ (entities.framework?.drivers || []).map((d) => d.id),
405
+ );
406
+ const invalid = initiatives.filter(
407
+ (i) => i._driver_id && !driverIds.has(i._driver_id),
408
+ );
409
+ return {
410
+ name: "initiative_driver_refs",
411
+ passed: invalid.length === 0,
412
+ message:
413
+ invalid.length === 0
414
+ ? "All initiative driver references are valid"
415
+ : `${invalid.length} initiatives reference unknown drivers`,
416
+ };
417
+ }
418
+
419
+ function checkCommentSnapshotRefs(entities) {
420
+ const comments = entities.activity?.commentKeys || [];
421
+ const snapshotIds = new Set(
422
+ (entities.activity?.snapshots || []).map((s) => s.snapshot_id),
423
+ );
424
+ const invalid = comments.filter(
425
+ (c) => c.snapshot_id && !snapshotIds.has(c.snapshot_id),
426
+ );
427
+ return {
428
+ name: "comment_snapshot_refs",
429
+ passed: invalid.length === 0,
430
+ message:
431
+ invalid.length === 0
432
+ ? "All comment snapshot references are valid"
433
+ : `${invalid.length} comments reference unknown snapshots`,
434
+ };
435
+ }
436
+
437
+ function checkCommentEmailRefs(entities) {
438
+ const comments = entities.activity?.commentKeys || [];
439
+ const emails = new Set(entities.people.map((p) => p.email));
440
+ const invalid = comments.filter((c) => c.email && !emails.has(c.email));
441
+ return {
442
+ name: "comment_email_refs",
443
+ passed: invalid.length === 0,
444
+ message:
445
+ invalid.length === 0
446
+ ? "All comment respondent emails are known people"
447
+ : `${invalid.length} comments from unknown emails`,
448
+ };
449
+ }
450
+
451
+ function checkCommentTeamRefs(entities) {
452
+ const comments = entities.activity?.commentKeys || [];
453
+ const teamIds = new Set(entities.teams.map((t) => t.id));
454
+ const invalid = comments.filter((c) => c.team_id && !teamIds.has(c.team_id));
455
+ return {
456
+ name: "comment_team_refs",
457
+ passed: invalid.length === 0,
458
+ message:
459
+ invalid.length === 0
460
+ ? "All comment team references are valid"
461
+ : `${invalid.length} comments reference unknown teams`,
462
+ };
463
+ }
464
+
465
+ function checkScorecardCheckIds(entities) {
466
+ const scorecards = entities.activity?.scorecards || [];
467
+ const allCheckIds = scorecards.flatMap((s) =>
468
+ (s.checks || []).map((c) => c.id),
469
+ );
470
+ const unique = new Set(allCheckIds);
471
+ return {
472
+ name: "scorecard_check_ids",
473
+ passed: unique.size === allCheckIds.length,
474
+ message:
475
+ unique.size === allCheckIds.length
476
+ ? "All scorecard check IDs are unique"
477
+ : `${allCheckIds.length - unique.size} duplicate scorecard check IDs`,
478
+ };
479
+ }
480
+
481
+ function checkRosterSnapshotQuarters(entities) {
482
+ const rosterSnapshots = entities.activity?.rosterSnapshots || [];
483
+ const snapshotIds = new Set(
484
+ (entities.activity?.snapshots || []).map((s) => s.snapshot_id),
485
+ );
486
+ const invalid = rosterSnapshots.filter(
487
+ (rs) => rs.snapshot_id && !snapshotIds.has(rs.snapshot_id),
488
+ );
489
+ return {
490
+ name: "roster_snapshot_quarters",
491
+ passed: invalid.length === 0,
492
+ message:
493
+ invalid.length === 0
494
+ ? "All roster snapshots align with survey snapshots"
495
+ : `${invalid.length} roster snapshots without matching survey snapshots`,
496
+ };
497
+ }
498
+
499
+ function checkProjectTeamEmails(entities) {
500
+ const projectTeams = entities.activity?.projectTeams || [];
501
+ const emails = new Set(entities.people.map((p) => p.email));
502
+ const invalid = projectTeams.flatMap((pt) =>
503
+ pt.members.filter((m) => m.email && !emails.has(m.email)),
504
+ );
505
+ return {
506
+ name: "project_team_emails",
507
+ passed: invalid.length === 0,
508
+ message:
509
+ invalid.length === 0
510
+ ? "All project team member emails are known people"
511
+ : `${invalid.length} project team members with unknown emails`,
512
+ };
513
+ }
514
+
515
+ /**
516
+ * Content validator class with DI.
517
+ */
518
+ export class ContentValidator {
519
+ /**
520
+ * @param {object} logger - Logger instance
521
+ */
522
+ constructor(logger) {
523
+ if (!logger) throw new Error("logger is required");
524
+ this.logger = logger;
525
+ }
526
+
527
+ /**
528
+ * Validate cross-content integrity of generated entities.
529
+ * @param {object} entities
530
+ * @returns {{passed: boolean, total: number, failures: number, checks: object[]}}
531
+ */
532
+ validate(entities) {
533
+ return validateCrossContent(entities);
534
+ }
535
+ }