monora-ai 2.0.0 → 2.1.3

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 (202) hide show
  1. package/README.md +441 -150
  2. package/dist/aims_governance.d.ts +238 -0
  3. package/dist/aims_governance.d.ts.map +1 -0
  4. package/dist/aims_governance.js +922 -0
  5. package/dist/alerts.d.ts +16 -0
  6. package/dist/alerts.d.ts.map +1 -1
  7. package/dist/alerts.js +16 -0
  8. package/dist/api.d.ts +6 -0
  9. package/dist/api.d.ts.map +1 -1
  10. package/dist/api.js +6 -0
  11. package/dist/assessment.d.ts +269 -0
  12. package/dist/assessment.d.ts.map +1 -0
  13. package/dist/assessment.js +1232 -0
  14. package/dist/attestation.js +23 -1
  15. package/dist/attribution.d.ts +349 -0
  16. package/dist/attribution.d.ts.map +1 -0
  17. package/dist/attribution.js +987 -0
  18. package/dist/autodetect.d.ts +69 -1
  19. package/dist/autodetect.d.ts.map +1 -1
  20. package/dist/autodetect.js +644 -1
  21. package/dist/bias.d.ts +130 -0
  22. package/dist/bias.d.ts.map +1 -0
  23. package/dist/bias.js +223 -0
  24. package/dist/circuit_breaker.js +3 -3
  25. package/dist/cli/diagnostics.d.ts +5 -1
  26. package/dist/cli/diagnostics.d.ts.map +1 -1
  27. package/dist/cli/diagnostics.js +31 -8
  28. package/dist/cli/doctor.d.ts +25 -0
  29. package/dist/cli/doctor.d.ts.map +1 -0
  30. package/dist/cli/doctor.js +381 -0
  31. package/dist/cli/fix.d.ts +16 -0
  32. package/dist/cli/fix.d.ts.map +1 -0
  33. package/dist/cli/fix.js +284 -0
  34. package/dist/cli/init.d.ts +57 -0
  35. package/dist/cli/init.d.ts.map +1 -0
  36. package/dist/cli/init.js +205 -0
  37. package/dist/cli.js +1611 -126
  38. package/dist/complianceTargets.d.ts +111 -0
  39. package/dist/complianceTargets.d.ts.map +1 -0
  40. package/dist/complianceTargets.js +521 -0
  41. package/dist/config.d.ts +301 -17
  42. package/dist/config.d.ts.map +1 -1
  43. package/dist/config.js +428 -36
  44. package/dist/config_migrations.d.ts +41 -0
  45. package/dist/config_migrations.d.ts.map +1 -1
  46. package/dist/config_migrations.js +205 -0
  47. package/dist/config_schema.d.ts +2900 -731
  48. package/dist/config_schema.d.ts.map +1 -1
  49. package/dist/config_schema.js +257 -55
  50. package/dist/context.d.ts +34 -0
  51. package/dist/context.d.ts.map +1 -1
  52. package/dist/context.js +118 -7
  53. package/dist/control_backbone.d.ts +122 -0
  54. package/dist/control_backbone.d.ts.map +1 -0
  55. package/dist/control_backbone.js +698 -0
  56. package/dist/data-governance.d.ts +187 -0
  57. package/dist/data-governance.d.ts.map +1 -0
  58. package/dist/data-governance.js +424 -0
  59. package/dist/dataResidency.d.ts +44 -0
  60. package/dist/dataResidency.d.ts.map +1 -0
  61. package/dist/dataResidency.js +203 -0
  62. package/dist/dispatcher.d.ts +32 -0
  63. package/dist/dispatcher.d.ts.map +1 -1
  64. package/dist/dispatcher.js +91 -4
  65. package/dist/events.d.ts.map +1 -1
  66. package/dist/events.js +38 -0
  67. package/dist/evidence_store.d.ts +103 -0
  68. package/dist/evidence_store.d.ts.map +1 -0
  69. package/dist/evidence_store.js +459 -0
  70. package/dist/executiveSummary.d.ts +65 -8
  71. package/dist/executiveSummary.d.ts.map +1 -1
  72. package/dist/executiveSummary.js +289 -26
  73. package/dist/identity.d.ts +143 -0
  74. package/dist/identity.d.ts.map +1 -0
  75. package/dist/identity.js +231 -0
  76. package/dist/impact-assessment.d.ts +350 -0
  77. package/dist/impact-assessment.d.ts.map +1 -0
  78. package/dist/impact-assessment.js +580 -0
  79. package/dist/index.d.ts +25 -5
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +300 -4
  82. package/dist/instrumentation.d.ts +1 -1
  83. package/dist/instrumentation.d.ts.map +1 -1
  84. package/dist/instrumentation.js +243 -27
  85. package/dist/integrations/anthropic.d.ts +3 -0
  86. package/dist/integrations/anthropic.d.ts.map +1 -1
  87. package/dist/integrations/anthropic.js +284 -79
  88. package/dist/integrations/governance.d.ts +33 -0
  89. package/dist/integrations/governance.d.ts.map +1 -0
  90. package/dist/integrations/governance.js +208 -0
  91. package/dist/integrations/langchain.d.ts +7 -0
  92. package/dist/integrations/langchain.d.ts.map +1 -1
  93. package/dist/integrations/langchain.js +387 -143
  94. package/dist/integrations/openai.d.ts +9 -0
  95. package/dist/integrations/openai.d.ts.map +1 -1
  96. package/dist/integrations/openai.js +673 -73
  97. package/dist/iso42001_consolidation.d.ts +16 -0
  98. package/dist/iso42001_consolidation.d.ts.map +1 -0
  99. package/dist/iso42001_consolidation.js +413 -0
  100. package/dist/iso42001_workflows.d.ts +263 -0
  101. package/dist/iso42001_workflows.d.ts.map +1 -0
  102. package/dist/iso42001_workflows.js +781 -0
  103. package/dist/lifecycle.d.ts +299 -0
  104. package/dist/lifecycle.d.ts.map +1 -0
  105. package/dist/lifecycle.js +624 -0
  106. package/dist/lineage.d.ts +2 -2
  107. package/dist/lineage.d.ts.map +1 -1
  108. package/dist/lineage.js +12 -17
  109. package/dist/middleware/express.d.ts.map +1 -1
  110. package/dist/middleware/express.js +33 -3
  111. package/dist/middleware/nextjs.d.ts.map +1 -1
  112. package/dist/middleware/nextjs.js +42 -68
  113. package/dist/model.d.ts +143 -0
  114. package/dist/model.d.ts.map +1 -0
  115. package/dist/model.js +371 -0
  116. package/dist/onboarding.d.ts +42 -0
  117. package/dist/onboarding.d.ts.map +1 -0
  118. package/dist/onboarding.js +1022 -0
  119. package/dist/oversight.d.ts +264 -0
  120. package/dist/oversight.d.ts.map +1 -0
  121. package/dist/oversight.js +497 -0
  122. package/dist/pdf_report.d.ts.map +1 -1
  123. package/dist/pdf_report.js +42 -21
  124. package/dist/presets.d.ts +88 -0
  125. package/dist/presets.d.ts.map +1 -0
  126. package/dist/presets.js +520 -0
  127. package/dist/propagation.d.ts.map +1 -1
  128. package/dist/propagation.js +34 -2
  129. package/dist/quotas.d.ts +171 -0
  130. package/dist/quotas.d.ts.map +1 -0
  131. package/dist/quotas.js +259 -0
  132. package/dist/register.d.ts +13 -0
  133. package/dist/register.d.ts.map +1 -0
  134. package/dist/register.js +99 -0
  135. package/dist/registry.d.ts +1 -0
  136. package/dist/registry.d.ts.map +1 -1
  137. package/dist/registry.js +7 -0
  138. package/dist/registryData.json +43 -6
  139. package/dist/report.d.ts +2 -1
  140. package/dist/report.d.ts.map +1 -1
  141. package/dist/report.js +189 -2
  142. package/dist/reporting.d.ts +125 -0
  143. package/dist/reporting.d.ts.map +1 -1
  144. package/dist/reporting.js +196 -5
  145. package/dist/resources.d.ts +285 -0
  146. package/dist/resources.d.ts.map +1 -0
  147. package/dist/resources.js +643 -0
  148. package/dist/risk.d.ts +120 -0
  149. package/dist/risk.d.ts.map +1 -0
  150. package/dist/risk.js +220 -0
  151. package/dist/runtime.d.ts +74 -1
  152. package/dist/runtime.d.ts.map +1 -1
  153. package/dist/runtime.js +598 -22
  154. package/dist/schemaInference.d.ts +92 -0
  155. package/dist/schemaInference.d.ts.map +1 -0
  156. package/dist/schemaInference.js +466 -0
  157. package/dist/schema_validation.js +2 -2
  158. package/dist/schemas/config.schema.json +169 -6
  159. package/dist/schemas/event.schema.json +4 -0
  160. package/dist/security_report.js +4 -4
  161. package/dist/signing.d.ts +1 -1
  162. package/dist/signing.d.ts.map +1 -1
  163. package/dist/signing.js +4 -0
  164. package/dist/sinks/file.d.ts +19 -1
  165. package/dist/sinks/file.d.ts.map +1 -1
  166. package/dist/sinks/file.js +82 -13
  167. package/dist/sinks/https.d.ts +10 -0
  168. package/dist/sinks/https.d.ts.map +1 -1
  169. package/dist/sinks/https.js +76 -16
  170. package/dist/sinks/stdout.d.ts +1 -0
  171. package/dist/sinks/stdout.d.ts.map +1 -1
  172. package/dist/sinks/stdout.js +12 -1
  173. package/dist/spec.d.ts +159 -0
  174. package/dist/spec.d.ts.map +1 -0
  175. package/dist/spec.js +391 -0
  176. package/dist/stakeholders.d.ts +199 -0
  177. package/dist/stakeholders.d.ts.map +1 -0
  178. package/dist/stakeholders.js +398 -0
  179. package/dist/standards.d.ts.map +1 -1
  180. package/dist/standards.js +160 -2
  181. package/dist/standards_ingest.d.ts +2 -2
  182. package/dist/standards_ingest.d.ts.map +1 -1
  183. package/dist/standards_ingest.js +105 -23
  184. package/dist/streaming.d.ts.map +1 -1
  185. package/dist/streaming.js +7 -2
  186. package/dist/telemetry.d.ts +16 -2
  187. package/dist/telemetry.d.ts.map +1 -1
  188. package/dist/telemetry.js +79 -14
  189. package/dist/templates/controls/iso42001_control_catalog.json +1443 -0
  190. package/dist/traced_emitter.d.ts +3 -0
  191. package/dist/traced_emitter.d.ts.map +1 -1
  192. package/dist/traced_emitter.js +142 -25
  193. package/dist/trust_package.d.ts +21 -1
  194. package/dist/trust_package.d.ts.map +1 -1
  195. package/dist/trust_package.js +101 -4
  196. package/dist/verify.d.ts.map +1 -1
  197. package/dist/verify.js +9 -2
  198. package/dist/wal.d.ts.map +1 -1
  199. package/dist/wal.js +2 -1
  200. package/package.json +14 -1
  201. package/scripts/postinstall.js +119 -97
  202. package/templates/controls/iso42001_control_catalog.json +1443 -0
@@ -0,0 +1,497 @@
1
+ "use strict";
2
+ /**
3
+ * Human oversight tracking for EU AI Act Art.14 and ISO 42001 8.3 compliance.
4
+ *
5
+ * This module provides human review and override tracking for AI decisions,
6
+ * supporting EU AI Act Article 14 human oversight requirements and ISO 42001
7
+ * control 8.3.
8
+ *
9
+ * Cross-SDK Parity:
10
+ * Both Python and Node.js SDKs provide identical human oversight APIs:
11
+ * - recordHumanReview() / record_human_review()
12
+ * - recordHumanOverride() / record_human_override()
13
+ * - getPendingReviews() / get_pending_reviews()
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.SqliteReviewStore = exports.MemoryReviewStore = void 0;
50
+ exports.reviewToEventBody = reviewToEventBody;
51
+ exports.overrideToEventBody = overrideToEventBody;
52
+ exports.setReviewStore = setReviewStore;
53
+ exports.recordHumanReview = recordHumanReview;
54
+ exports.recordHumanOverride = recordHumanOverride;
55
+ exports.requireReview = requireReview;
56
+ exports.getPendingReviews = getPendingReviews;
57
+ exports.getTimedOutReviews = getTimedOutReviews;
58
+ exports.checkReviewRequired = checkReviewRequired;
59
+ exports.getOversightSummary = getOversightSummary;
60
+ exports.clearOversightData = clearOversightData;
61
+ const crypto = __importStar(require("crypto"));
62
+ /**
63
+ * Convert HumanReview to event body dictionary.
64
+ */
65
+ function reviewToEventBody(review) {
66
+ const body = {
67
+ reviewed_event_id: review.reviewedEventId,
68
+ reviewer_id: review.reviewerId,
69
+ review_type: review.reviewType,
70
+ decision: review.decision,
71
+ timestamp: review.timestamp,
72
+ };
73
+ if (review.modifications) {
74
+ body.modifications = review.modifications;
75
+ }
76
+ if (review.reviewDurationMs !== undefined) {
77
+ body.review_duration_ms = review.reviewDurationMs;
78
+ }
79
+ if (review.reviewNotes) {
80
+ body.review_notes = review.reviewNotes;
81
+ }
82
+ if (review.complianceFlags.length > 0) {
83
+ body.compliance_flags = review.complianceFlags;
84
+ }
85
+ return body;
86
+ }
87
+ /**
88
+ * Convert HumanOverride to event body dictionary.
89
+ */
90
+ function overrideToEventBody(override) {
91
+ const body = {
92
+ overridden_event_id: override.overriddenEventId,
93
+ override_reason: override.overrideReason,
94
+ timestamp: override.timestamp,
95
+ };
96
+ if (override.originalOutputHash) {
97
+ body.original_output_hash = override.originalOutputHash;
98
+ }
99
+ if (override.correctedOutputHash) {
100
+ body.corrected_output_hash = override.correctedOutputHash;
101
+ }
102
+ if (override.overrideAuthority) {
103
+ body.override_authority = override.overrideAuthority;
104
+ }
105
+ return body;
106
+ }
107
+ const DEFAULT_COMPLETED_REVIEW_TTL_MS = 30 * 24 * 60 * 60 * 1000;
108
+ /**
109
+ * In-memory store for oversight reviews.
110
+ *
111
+ * Note: This is single-instance only. For production/distributed use, supply
112
+ * a persistent store via setReviewStore (e.g., SqliteReviewStore or Redis).
113
+ */
114
+ class MemoryReviewStore {
115
+ constructor(options) {
116
+ this.pendingReviews = new Map();
117
+ this.completedReviews = [];
118
+ this.overrides = [];
119
+ this.completedTtlMs = options?.completedTtlMs ?? DEFAULT_COMPLETED_REVIEW_TTL_MS;
120
+ }
121
+ addPendingReview(review) {
122
+ this.pendingReviews.set(review.eventId, review);
123
+ }
124
+ removePendingReview(eventId) {
125
+ this.pendingReviews.delete(eventId);
126
+ }
127
+ listPendingReviews() {
128
+ return Array.from(this.pendingReviews.values());
129
+ }
130
+ listTimedOutReviews(now) {
131
+ return this.listPendingReviews().filter((review) => review.timeoutAt < now);
132
+ }
133
+ addCompletedReview(review) {
134
+ this.pruneCompletedReviews(Date.now());
135
+ this.completedReviews.push(review);
136
+ }
137
+ listCompletedReviews() {
138
+ this.pruneCompletedReviews(Date.now());
139
+ return [...this.completedReviews];
140
+ }
141
+ addOverride(override) {
142
+ this.overrides.push(override);
143
+ }
144
+ listOverrides() {
145
+ return [...this.overrides];
146
+ }
147
+ clear() {
148
+ this.pendingReviews.clear();
149
+ this.completedReviews = [];
150
+ this.overrides = [];
151
+ }
152
+ pruneCompletedReviews(now) {
153
+ if (this.completedTtlMs <= 0) {
154
+ return;
155
+ }
156
+ const cutoff = now - this.completedTtlMs;
157
+ this.completedReviews = this.completedReviews.filter((review) => {
158
+ const timestampMs = Date.parse(review.timestamp);
159
+ if (Number.isNaN(timestampMs)) {
160
+ return true;
161
+ }
162
+ return timestampMs >= cutoff;
163
+ });
164
+ }
165
+ }
166
+ exports.MemoryReviewStore = MemoryReviewStore;
167
+ /**
168
+ * SQLite-backed persistent review store (requires better-sqlite3).
169
+ */
170
+ class SqliteReviewStore {
171
+ constructor(dbPath, options) {
172
+ let Database;
173
+ try {
174
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
175
+ Database = require('better-sqlite3');
176
+ }
177
+ catch (error) {
178
+ const message = error instanceof Error ? error.message : String(error);
179
+ throw new Error(`SqliteReviewStore requires better-sqlite3. ${message}`);
180
+ }
181
+ this.db = new Database(dbPath);
182
+ this.completedTtlMs = options?.completedTtlMs ?? DEFAULT_COMPLETED_REVIEW_TTL_MS;
183
+ this.db.exec(`
184
+ CREATE TABLE IF NOT EXISTS pending_reviews (
185
+ event_id TEXT PRIMARY KEY,
186
+ trace_id TEXT,
187
+ event_type TEXT,
188
+ model TEXT,
189
+ risk_level TEXT,
190
+ created_at INTEGER,
191
+ timeout_at INTEGER
192
+ );
193
+ CREATE TABLE IF NOT EXISTS completed_reviews (
194
+ reviewed_event_id TEXT,
195
+ reviewer_id TEXT,
196
+ review_type TEXT,
197
+ decision TEXT,
198
+ modifications TEXT,
199
+ review_duration_ms INTEGER,
200
+ review_notes TEXT,
201
+ compliance_flags TEXT,
202
+ timestamp TEXT,
203
+ timestamp_ms INTEGER
204
+ );
205
+ CREATE TABLE IF NOT EXISTS overrides (
206
+ overridden_event_id TEXT,
207
+ override_reason TEXT,
208
+ original_output_hash TEXT,
209
+ corrected_output_hash TEXT,
210
+ override_authority TEXT,
211
+ timestamp TEXT
212
+ );
213
+ `);
214
+ }
215
+ addPendingReview(review) {
216
+ this.db.prepare(`
217
+ INSERT OR REPLACE INTO pending_reviews
218
+ (event_id, trace_id, event_type, model, risk_level, created_at, timeout_at)
219
+ VALUES (?, ?, ?, ?, ?, ?, ?)
220
+ `).run(review.eventId, review.traceId ?? null, review.eventType, review.model ?? null, review.riskLevel ?? null, review.createdAt, review.timeoutAt);
221
+ }
222
+ removePendingReview(eventId) {
223
+ this.db.prepare('DELETE FROM pending_reviews WHERE event_id = ?').run(eventId);
224
+ }
225
+ listPendingReviews() {
226
+ const rows = this.db.prepare('SELECT * FROM pending_reviews').all();
227
+ return rows.map((row) => ({
228
+ eventId: row.event_id,
229
+ traceId: row.trace_id ?? undefined,
230
+ eventType: row.event_type,
231
+ model: row.model ?? undefined,
232
+ riskLevel: row.risk_level ?? undefined,
233
+ createdAt: row.created_at,
234
+ timeoutAt: row.timeout_at,
235
+ }));
236
+ }
237
+ listTimedOutReviews(now) {
238
+ const rows = this.db
239
+ .prepare('SELECT * FROM pending_reviews WHERE timeout_at < ?')
240
+ .all(now);
241
+ return rows.map((row) => ({
242
+ eventId: row.event_id,
243
+ traceId: row.trace_id ?? undefined,
244
+ eventType: row.event_type,
245
+ model: row.model ?? undefined,
246
+ riskLevel: row.risk_level ?? undefined,
247
+ createdAt: row.created_at,
248
+ timeoutAt: row.timeout_at,
249
+ }));
250
+ }
251
+ addCompletedReview(review) {
252
+ const timestampMs = Date.parse(review.timestamp);
253
+ this.db.prepare(`
254
+ INSERT INTO completed_reviews
255
+ (reviewed_event_id, reviewer_id, review_type, decision, modifications, review_duration_ms,
256
+ review_notes, compliance_flags, timestamp, timestamp_ms)
257
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
258
+ `).run(review.reviewedEventId, review.reviewerId, review.reviewType, review.decision, review.modifications ?? null, review.reviewDurationMs ?? null, review.reviewNotes ?? null, JSON.stringify(review.complianceFlags || []), review.timestamp, Number.isNaN(timestampMs) ? null : timestampMs);
259
+ this.pruneCompletedReviews(Date.now());
260
+ }
261
+ listCompletedReviews() {
262
+ this.pruneCompletedReviews(Date.now());
263
+ const rows = this.db.prepare('SELECT * FROM completed_reviews').all();
264
+ return rows.map((row) => ({
265
+ reviewedEventId: row.reviewed_event_id,
266
+ reviewerId: row.reviewer_id,
267
+ reviewType: row.review_type,
268
+ decision: row.decision,
269
+ modifications: row.modifications ?? undefined,
270
+ reviewDurationMs: row.review_duration_ms ?? undefined,
271
+ reviewNotes: row.review_notes ?? undefined,
272
+ complianceFlags: this.parseComplianceFlags(row.compliance_flags),
273
+ timestamp: row.timestamp,
274
+ }));
275
+ }
276
+ addOverride(override) {
277
+ this.db.prepare(`
278
+ INSERT INTO overrides
279
+ (overridden_event_id, override_reason, original_output_hash, corrected_output_hash,
280
+ override_authority, timestamp)
281
+ VALUES (?, ?, ?, ?, ?, ?)
282
+ `).run(override.overriddenEventId, override.overrideReason, override.originalOutputHash ?? null, override.correctedOutputHash ?? null, override.overrideAuthority ?? null, override.timestamp);
283
+ }
284
+ listOverrides() {
285
+ const rows = this.db.prepare('SELECT * FROM overrides').all();
286
+ return rows.map((row) => ({
287
+ overriddenEventId: row.overridden_event_id,
288
+ overrideReason: row.override_reason,
289
+ originalOutputHash: row.original_output_hash ?? undefined,
290
+ correctedOutputHash: row.corrected_output_hash ?? undefined,
291
+ overrideAuthority: row.override_authority ?? undefined,
292
+ timestamp: row.timestamp,
293
+ }));
294
+ }
295
+ clear() {
296
+ this.db.exec('DELETE FROM pending_reviews; DELETE FROM completed_reviews; DELETE FROM overrides;');
297
+ }
298
+ pruneCompletedReviews(now) {
299
+ if (this.completedTtlMs <= 0) {
300
+ return;
301
+ }
302
+ const cutoff = now - this.completedTtlMs;
303
+ this.db.prepare('DELETE FROM completed_reviews WHERE timestamp_ms IS NOT NULL AND timestamp_ms < ?')
304
+ .run(cutoff);
305
+ }
306
+ parseComplianceFlags(value) {
307
+ if (!value) {
308
+ return [];
309
+ }
310
+ try {
311
+ const parsed = JSON.parse(value);
312
+ return Array.isArray(parsed) ? parsed : [];
313
+ }
314
+ catch {
315
+ return [];
316
+ }
317
+ }
318
+ }
319
+ exports.SqliteReviewStore = SqliteReviewStore;
320
+ let reviewStore = new MemoryReviewStore();
321
+ function setReviewStore(store) {
322
+ reviewStore = store;
323
+ }
324
+ /**
325
+ * Record a human review of an AI decision.
326
+ *
327
+ * @param reviewedEventId - ID of the event being reviewed.
328
+ * @param reviewerId - ID of the human reviewer.
329
+ * @param reviewType - Type of review (approval, rejection, modification, escalation).
330
+ * @param decision - Review decision (approved, rejected, modified, escalated).
331
+ * @param options - Additional options.
332
+ * @returns The created HumanReview.
333
+ *
334
+ * @example
335
+ * ```typescript
336
+ * recordHumanReview(
337
+ * 'evt_123',
338
+ * 'usr_456',
339
+ * 'approval',
340
+ * 'approved',
341
+ * { notes: 'Verified output accuracy' }
342
+ * );
343
+ * ```
344
+ */
345
+ function recordHumanReview(reviewedEventId, reviewerId, reviewType, decision, options = {}) {
346
+ const review = {
347
+ reviewedEventId,
348
+ reviewerId,
349
+ reviewType,
350
+ decision,
351
+ modifications: options.modifications,
352
+ reviewDurationMs: options.reviewDurationMs,
353
+ reviewNotes: options.notes,
354
+ complianceFlags: options.complianceFlags || [],
355
+ timestamp: new Date().toISOString(),
356
+ };
357
+ reviewStore.addCompletedReview(review);
358
+ reviewStore.removePendingReview(reviewedEventId);
359
+ return review;
360
+ }
361
+ /**
362
+ * Record a human override of an AI output.
363
+ *
364
+ * @param overriddenEventId - ID of the event being overridden.
365
+ * @param overrideReason - Reason for the override.
366
+ * @param options - Additional options.
367
+ * @returns The created HumanOverride.
368
+ *
369
+ * @example
370
+ * ```typescript
371
+ * recordHumanOverride(
372
+ * 'evt_123',
373
+ * 'Incorrect recommendation',
374
+ * { overrideAuthority: 'supervisor' }
375
+ * );
376
+ * ```
377
+ */
378
+ function recordHumanOverride(overriddenEventId, overrideReason, options = {}) {
379
+ let originalHash;
380
+ let correctedHash;
381
+ if (options.originalOutput) {
382
+ const hash = crypto.createHash('sha256').update(options.originalOutput).digest('hex');
383
+ originalHash = `sha256:${hash}`;
384
+ }
385
+ if (options.correctedOutput) {
386
+ const hash = crypto.createHash('sha256').update(options.correctedOutput).digest('hex');
387
+ correctedHash = `sha256:${hash}`;
388
+ }
389
+ const override = {
390
+ overriddenEventId,
391
+ overrideReason,
392
+ originalOutputHash: originalHash,
393
+ correctedOutputHash: correctedHash,
394
+ overrideAuthority: options.overrideAuthority,
395
+ timestamp: new Date().toISOString(),
396
+ };
397
+ reviewStore.addOverride(override);
398
+ return override;
399
+ }
400
+ /**
401
+ * Mark an event as requiring human review.
402
+ *
403
+ * @param eventId - ID of the event requiring review.
404
+ * @param options - Additional options.
405
+ * @returns The created PendingReview.
406
+ */
407
+ function requireReview(eventId, options = {}) {
408
+ const config = options.config || {};
409
+ const oversightConfig = config.human_oversight || {};
410
+ const timeoutHours = oversightConfig.review_timeout_hours ?? 24;
411
+ const now = Date.now();
412
+ const pending = {
413
+ eventId,
414
+ traceId: options.traceId,
415
+ eventType: options.eventType || 'llm_call',
416
+ model: options.model,
417
+ riskLevel: options.riskLevel,
418
+ createdAt: now,
419
+ timeoutAt: now + timeoutHours * 3600 * 1000,
420
+ };
421
+ reviewStore.addPendingReview(pending);
422
+ return pending;
423
+ }
424
+ /**
425
+ * Get all events pending human review.
426
+ */
427
+ function getPendingReviews() {
428
+ return reviewStore.listPendingReviews();
429
+ }
430
+ /**
431
+ * Get reviews that have timed out.
432
+ */
433
+ function getTimedOutReviews() {
434
+ return reviewStore.listTimedOutReviews(Date.now());
435
+ }
436
+ /**
437
+ * Check if an event requires human review based on config.
438
+ *
439
+ * @param eventType - Type of event.
440
+ * @param model - Model used.
441
+ * @param riskLevel - Risk level of the event.
442
+ * @param config - Optional config.
443
+ * @returns True if review is required.
444
+ */
445
+ function checkReviewRequired(eventType, model, riskLevel, config) {
446
+ const oversightConfig = config?.human_oversight;
447
+ if (!oversightConfig?.enabled) {
448
+ return false;
449
+ }
450
+ const requireFor = oversightConfig.require_review_for || [];
451
+ if (requireFor.length === 0) {
452
+ return false;
453
+ }
454
+ for (const condition of requireFor) {
455
+ if (condition.risk_level && riskLevel !== condition.risk_level) {
456
+ continue;
457
+ }
458
+ if (condition.event_type && eventType !== condition.event_type) {
459
+ continue;
460
+ }
461
+ if (condition.tool_name && model !== condition.tool_name) {
462
+ continue;
463
+ }
464
+ return true;
465
+ }
466
+ return false;
467
+ }
468
+ /**
469
+ * Get summary of human oversight for reporting.
470
+ *
471
+ * @returns Summary dict for compliance reports.
472
+ */
473
+ function getOversightSummary() {
474
+ const completed = reviewStore.listCompletedReviews();
475
+ const pending = reviewStore.listPendingReviews();
476
+ const overrides = reviewStore.listOverrides();
477
+ const timedOut = reviewStore.listTimedOutReviews(Date.now());
478
+ const reviewTimes = completed
479
+ .filter((r) => r.reviewDurationMs !== undefined)
480
+ .map((r) => r.reviewDurationMs);
481
+ const avgReviewTime = reviewTimes.length > 0
482
+ ? reviewTimes.reduce((a, b) => a + b, 0) / reviewTimes.length
483
+ : null;
484
+ return {
485
+ reviews_completed: completed.length,
486
+ reviews_pending: pending.length,
487
+ overrides: overrides.length,
488
+ average_review_time_ms: avgReviewTime,
489
+ timed_out_reviews: timedOut.length,
490
+ };
491
+ }
492
+ /**
493
+ * Clear all oversight tracking data.
494
+ */
495
+ function clearOversightData() {
496
+ reviewStore.clear();
497
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"pdf_report.d.ts","sourceRoot":"","sources":["../src/pdf_report.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,QAAA,IAAI,mBAAmB,SAAQ,CAAC;AAWhC,OAAO,EAAE,mBAAmB,EAAE,CAAC;AAE/B;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAkMD,UAAU,gBAAgB;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,UAAU,CAAC,EAAE,KAAK,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,WAAW,CAAC,EAAE;QACZ,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,uBAAuB,CAAC,EAAE,MAAM,CAAC;KAClC,CAAC;IACF,gBAAgB,CAAC,EAAE;QACjB,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;QAC/B,wBAAwB,CAAC,EAAE,MAAM,EAAE,CAAC;QACpC,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;KAChC,CAAC;CACH;AAED,UAAU,WAAW;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,UAAU,CAAC,EAAE,KAAK,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;KACzB,CAAC,CAAC;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;IAClC,uBAAuB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjD,uBAAuB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClD;AAED,UAAU,SAAS;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AA4XD;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,gBAAgB,EACxB,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,SAAS,GACjB,OAAO,CAAC,MAAM,CAAC,CAiDjB;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAiDjB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIxD"}
1
+ {"version":3,"file":"pdf_report.d.ts","sourceRoot":"","sources":["../src/pdf_report.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,QAAA,IAAI,mBAAmB,SAAQ,CAAC;AAWhC,OAAO,EAAE,mBAAmB,EAAE,CAAC;AAE/B;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAkMD,UAAU,gBAAgB;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,UAAU,CAAC,EAAE,KAAK,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,WAAW,CAAC,EAAE;QACZ,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,uBAAuB,CAAC,EAAE,MAAM,CAAC;KAClC,CAAC;IACF,gBAAgB,CAAC,EAAE;QACjB,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;QAC/B,wBAAwB,CAAC,EAAE,MAAM,EAAE,CAAC;QACpC,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;KAChC,CAAC;CACH;AAED,UAAU,WAAW;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,UAAU,CAAC,EAAE,KAAK,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;KACzB,CAAC,CAAC;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;IAClC,uBAAuB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjD,uBAAuB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClD;AAED,UAAU,SAAS;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAoZD;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,gBAAgB,EACxB,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,SAAS,GACjB,OAAO,CAAC,MAAM,CAAC,CAiDjB;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAiDjB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIxD"}
@@ -275,11 +275,19 @@ function formatDate(isoDate) {
275
275
  return isoDate;
276
276
  }
277
277
  }
278
+ function htmlEscape(value) {
279
+ return value
280
+ .replace(/&/g, '&amp;')
281
+ .replace(/</g, '&lt;')
282
+ .replace(/>/g, '&gt;')
283
+ .replace(/"/g, '&quot;')
284
+ .replace(/'/g, '&#39;');
285
+ }
278
286
  /**
279
287
  * Generate HTML for compliance report.
280
288
  */
281
289
  function generateComplianceHtml(report, config) {
282
- const orgName = config?.organization_name || 'Organization';
290
+ const orgName = htmlEscape(String(config?.organization_name || 'Organization'));
283
291
  const totalEvents = report.total_events || 0;
284
292
  const traces = report.traces || 0;
285
293
  const dateRange = report.date_range || {};
@@ -288,8 +296,8 @@ function generateComplianceHtml(report, config) {
288
296
  const violations = report.violations || [];
289
297
  const tokenUsage = report.token_usage || {};
290
298
  const modelCompliance = report.model_compliance || {};
291
- const startDate = formatDate(dateRange.start);
292
- const endDate = formatDate(dateRange.end);
299
+ const startDate = htmlEscape(formatDate(dateRange.start));
300
+ const endDate = htmlEscape(formatDate(dateRange.end));
293
301
  // Build event type rows
294
302
  let eventTypeRows = '';
295
303
  const sortedEventTypes = Object.entries(byEventType).sort((a, b) => b[1] - a[1]);
@@ -297,7 +305,7 @@ function generateComplianceHtml(report, config) {
297
305
  const pct = totalEvents > 0 ? (count / totalEvents) * 100 : 0;
298
306
  eventTypeRows += `
299
307
  <tr>
300
- <td>${eventType}</td>
308
+ <td>${htmlEscape(String(eventType))}</td>
301
309
  <td>${formatNumber(count)}</td>
302
310
  <td>
303
311
  <div class="metric-bar">
@@ -314,7 +322,7 @@ function generateComplianceHtml(report, config) {
314
322
  for (const [model, count] of sortedModels) {
315
323
  modelRows += `
316
324
  <tr>
317
- <td>${model}</td>
325
+ <td>${htmlEscape(String(model))}</td>
318
326
  <td>${formatNumber(count)}</td>
319
327
  </tr>
320
328
  `;
@@ -324,12 +332,16 @@ function generateComplianceHtml(report, config) {
324
332
  if (violations.length > 0) {
325
333
  violationsHtml = '<div class="violation-list"><h4>Policy Violations</h4>';
326
334
  for (const v of violations.slice(0, 20)) {
335
+ const safeTimestamp = htmlEscape(String(v.timestamp || 'N/A'));
336
+ const safeModel = htmlEscape(String(v.model || 'N/A'));
337
+ const safePolicy = htmlEscape(String(v.policy || 'N/A'));
338
+ const safeMessage = htmlEscape(String(v.message || ''));
327
339
  violationsHtml += `
328
340
  <div class="violation-item">
329
- <strong>${v.timestamp || 'N/A'}</strong> -
330
- Model: <code>${v.model || 'N/A'}</code> |
331
- Policy: ${v.policy || 'N/A'} |
332
- ${v.message || ''}
341
+ <strong>${safeTimestamp}</strong> -
342
+ Model: <code>${safeModel}</code> |
343
+ Policy: ${safePolicy} |
344
+ ${safeMessage}
333
345
  </div>
334
346
  `;
335
347
  }
@@ -344,6 +356,12 @@ function generateComplianceHtml(report, config) {
344
356
  const allowedModels = modelCompliance.allowed_models_used || [];
345
357
  const forbiddenBlocked = modelCompliance.forbidden_models_blocked || [];
346
358
  const unknownModels = modelCompliance.unknown_models_used || [];
359
+ const allowedModelsLabel = allowedModels.length
360
+ ? allowedModels.map((model) => htmlEscape(String(model))).join(', ')
361
+ : 'None';
362
+ const unknownModelsLabel = unknownModels.length
363
+ ? unknownModels.map((model) => htmlEscape(String(model))).join(', ')
364
+ : 'None';
347
365
  const complianceStatus = violations.length === 0 && forbiddenBlocked.length === 0
348
366
  ? 'success'
349
367
  : violations.length < 5
@@ -447,11 +465,11 @@ function generateComplianceHtml(report, config) {
447
465
  <div class="two-column">
448
466
  <div class="card">
449
467
  <h4>Allowed Models Used</h4>
450
- <p>${allowedModels.join(', ') || 'None'}</p>
468
+ <p>${allowedModelsLabel}</p>
451
469
  </div>
452
470
  <div class="card">
453
471
  <h4>Unknown Models</h4>
454
- <p>${unknownModels.join(', ') || 'None'}</p>
472
+ <p>${unknownModelsLabel}</p>
455
473
  </div>
456
474
  </div>
457
475
 
@@ -467,9 +485,9 @@ function generateComplianceHtml(report, config) {
467
485
  * Generate HTML for AI Act transparency report.
468
486
  */
469
487
  function generateAIActHtml(report) {
470
- const orgName = report.organizationName || 'Organization';
471
- const periodStart = report.reportingPeriodStart || 'N/A';
472
- const periodEnd = report.reportingPeriodEnd || 'N/A';
488
+ const orgName = htmlEscape(String(report.organizationName || 'Organization'));
489
+ const periodStart = htmlEscape(String(report.reportingPeriodStart || 'N/A'));
490
+ const periodEnd = htmlEscape(String(report.reportingPeriodEnd || 'N/A'));
473
491
  const totalInteractions = report.totalAiInteractions || 0;
474
492
  const uniqueModels = report.uniqueModelsUsed || 0;
475
493
  const uniqueTraces = report.uniqueTraces || 0;
@@ -494,12 +512,15 @@ function generateAIActHtml(report) {
494
512
  let modelRows = '';
495
513
  for (const model of modelsUsed) {
496
514
  const riskCat = model.riskCategory || 'limited';
497
- const caps = (model.capabilities || []).slice(0, 3).join(', ') || 'N/A';
515
+ const riskLabel = htmlEscape(String(riskCat).toUpperCase());
516
+ const safeModel = htmlEscape(String(model.model || 'N/A'));
517
+ const safeProvider = htmlEscape(String(model.provider || 'N/A'));
518
+ const caps = htmlEscape(String((model.capabilities || []).slice(0, 3).join(', ') || 'N/A'));
498
519
  modelRows += `
499
520
  <tr>
500
- <td>${model.model || 'N/A'}</td>
501
- <td>${model.provider || 'N/A'}</td>
502
- <td><span class="status-badge risk-${riskCat}">${riskCat.toUpperCase()}</span></td>
521
+ <td>${safeModel}</td>
522
+ <td>${safeProvider}</td>
523
+ <td><span class="status-badge risk-${riskCat}">${riskLabel}</span></td>
503
524
  <td>${formatNumber(model.callCount || 0)}</td>
504
525
  <td>${caps}</td>
505
526
  </tr>
@@ -511,7 +532,7 @@ function generateAIActHtml(report) {
511
532
  for (const [classification, count] of sortedClassifications) {
512
533
  classificationRows += `
513
534
  <tr>
514
- <td>${classification}</td>
535
+ <td>${htmlEscape(String(classification))}</td>
515
536
  <td>${formatNumber(count)}</td>
516
537
  </tr>
517
538
  `;
@@ -521,10 +542,10 @@ function generateAIActHtml(report) {
521
542
  if (highRisk.length > 0 || unacceptableRisk.length > 0) {
522
543
  warningsHtml = '<div class="violation-list"><h4>⚠️ Risk Warnings</h4>';
523
544
  if (unacceptableRisk.length > 0) {
524
- warningsHtml += `<p class="status-badge status-error">UNACCEPTABLE RISK: ${unacceptableRisk.join(', ')}</p>`;
545
+ warningsHtml += `<p class="status-badge status-error">UNACCEPTABLE RISK: ${unacceptableRisk.map((model) => htmlEscape(String(model))).join(', ')}</p>`;
525
546
  }
526
547
  if (highRisk.length > 0) {
527
- warningsHtml += `<p class="status-badge status-warning">HIGH RISK: ${highRisk.join(', ')}</p>`;
548
+ warningsHtml += `<p class="status-badge status-warning">HIGH RISK: ${highRisk.map((model) => htmlEscape(String(model))).join(', ')}</p>`;
528
549
  }
529
550
  warningsHtml += '</div>';
530
551
  }