@eddacraft/anvil-kindling-integration 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +542 -0
  3. package/dist/adapter.d.ts +49 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +100 -0
  6. package/dist/config.d.ts +89 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +173 -0
  9. package/dist/emitters/action-emitter.d.ts +40 -0
  10. package/dist/emitters/action-emitter.d.ts.map +1 -0
  11. package/dist/emitters/action-emitter.js +52 -0
  12. package/dist/emitters/constraint-emitter.d.ts +32 -0
  13. package/dist/emitters/constraint-emitter.d.ts.map +1 -0
  14. package/dist/emitters/constraint-emitter.js +41 -0
  15. package/dist/emitters/error-emitter.d.ts +33 -0
  16. package/dist/emitters/error-emitter.d.ts.map +1 -0
  17. package/dist/emitters/error-emitter.js +50 -0
  18. package/dist/emitters/gate-emitter.d.ts +37 -0
  19. package/dist/emitters/gate-emitter.d.ts.map +1 -0
  20. package/dist/emitters/gate-emitter.js +53 -0
  21. package/dist/emitters/human-input-emitter.d.ts +30 -0
  22. package/dist/emitters/human-input-emitter.d.ts.map +1 -0
  23. package/dist/emitters/human-input-emitter.js +38 -0
  24. package/dist/emitters/index.d.ts +13 -0
  25. package/dist/emitters/index.d.ts.map +1 -0
  26. package/dist/emitters/index.js +19 -0
  27. package/dist/emitters/plan-emitter.d.ts +75 -0
  28. package/dist/emitters/plan-emitter.d.ts.map +1 -0
  29. package/dist/emitters/plan-emitter.js +116 -0
  30. package/dist/emitters/session-emitter.d.ts +57 -0
  31. package/dist/emitters/session-emitter.d.ts.map +1 -0
  32. package/dist/emitters/session-emitter.js +80 -0
  33. package/dist/index.d.ts +40 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +111 -0
  36. package/dist/kindling-service.d.ts +122 -0
  37. package/dist/kindling-service.d.ts.map +1 -0
  38. package/dist/kindling-service.js +203 -0
  39. package/dist/observation-contract.d.ts +561 -0
  40. package/dist/observation-contract.d.ts.map +1 -0
  41. package/dist/observation-contract.js +391 -0
  42. package/dist/query-contract.d.ts +463 -0
  43. package/dist/query-contract.d.ts.map +1 -0
  44. package/dist/query-contract.js +314 -0
  45. package/dist/query-limits.d.ts +40 -0
  46. package/dist/query-limits.d.ts.map +1 -0
  47. package/dist/query-limits.js +79 -0
  48. package/dist/query-service.d.ts +109 -0
  49. package/dist/query-service.d.ts.map +1 -0
  50. package/dist/query-service.js +140 -0
  51. package/dist/retention.d.ts +79 -0
  52. package/dist/retention.d.ts.map +1 -0
  53. package/dist/retention.js +81 -0
  54. package/dist/sensitive-data-validator.d.ts +47 -0
  55. package/dist/sensitive-data-validator.d.ts.map +1 -0
  56. package/dist/sensitive-data-validator.js +135 -0
  57. package/dist/status.d.ts +104 -0
  58. package/dist/status.d.ts.map +1 -0
  59. package/dist/status.js +136 -0
  60. package/dist/utils/debug.d.ts +9 -0
  61. package/dist/utils/debug.d.ts.map +1 -0
  62. package/dist/utils/debug.js +55 -0
  63. package/package.json +114 -0
  64. package/src/adapter.ts +117 -0
  65. package/src/config.ts +202 -0
  66. package/src/emitters/action-emitter.ts +90 -0
  67. package/src/emitters/constraint-emitter.ts +73 -0
  68. package/src/emitters/error-emitter.ts +86 -0
  69. package/src/emitters/gate-emitter.ts +87 -0
  70. package/src/emitters/human-input-emitter.ts +71 -0
  71. package/src/emitters/index.ts +40 -0
  72. package/src/emitters/plan-emitter.ts +183 -0
  73. package/src/emitters/session-emitter.ts +131 -0
  74. package/src/index.ts +254 -0
  75. package/src/kindling-service.ts +272 -0
  76. package/src/malicious-ai.test.ts +949 -0
  77. package/src/observation-contract.ts +500 -0
  78. package/src/query-contract.ts +389 -0
  79. package/src/query-limits.ts +106 -0
  80. package/src/query-service.ts +217 -0
  81. package/src/retention.ts +153 -0
  82. package/src/sensitive-data-validator.ts +167 -0
  83. package/src/status.ts +221 -0
  84. package/src/utils/debug.ts +65 -0
@@ -0,0 +1,500 @@
1
+ /**
2
+ * Observation Contract (v1)
3
+ *
4
+ * Defines the 9 observation kinds that Anvil must emit to Kindling.
5
+ * This is the write-only contract - what gets recorded, not how it's queried.
6
+ *
7
+ * Based on: "What Kindling is used for in Anvil v1" specification
8
+ *
9
+ * @see query-contract.ts for the read-only query surface
10
+ */
11
+
12
+ import { z } from 'zod';
13
+
14
+ // =============================================================================
15
+ // Schema Version
16
+ // =============================================================================
17
+
18
+ export const OBSERVATION_CONTRACT_VERSION = '1.0.0';
19
+
20
+ // =============================================================================
21
+ // 1. Session Recording (The Spine)
22
+ // =============================================================================
23
+
24
+ /**
25
+ * Session Start: Every Anvil run opens a session capsule
26
+ */
27
+ export const SessionStartObservationSchema = z.object({
28
+ kind: z.literal('session_start'),
29
+ session_id: z.string().uuid().describe('Unique session identifier'),
30
+ timestamp: z.string().datetime().describe('When session started'),
31
+
32
+ // Context at session start
33
+ context: z.object({
34
+ working_directory: z.string().describe('Workspace root'),
35
+ git_ref: z.string().optional().describe('Current git commit/branch'),
36
+ git_dirty: z.boolean().optional().describe('Whether working tree has changes'),
37
+ anvil_version: z.string().describe('Anvil CLI version'),
38
+ command: z.string().describe('CLI command invoked (e.g., "anvil check")'),
39
+ args: z.array(z.string()).describe('Command arguments'),
40
+ environment: z
41
+ .enum(['development', 'ci', 'production', 'unknown'])
42
+ .describe('Execution context'),
43
+ }),
44
+
45
+ // Plan linkage (if this session is executing a plan)
46
+ plan_id: z.string().optional().describe('Plan being executed (if any)'),
47
+ });
48
+
49
+ export type SessionStartObservation = z.infer<typeof SessionStartObservationSchema>;
50
+
51
+ /**
52
+ * Session End: Closes the capsule with outcome
53
+ */
54
+ export const SessionEndObservationSchema = z.object({
55
+ kind: z.literal('session_end'),
56
+ session_id: z.string().uuid().describe('Session being closed'),
57
+ timestamp: z.string().datetime().describe('When session ended'),
58
+
59
+ // Outcome
60
+ outcome: z.enum(['success', 'failure', 'partial', 'cancelled']).describe('Session result'),
61
+ exit_code: z.number().int().describe('Process exit code'),
62
+ duration_ms: z.number().int().nonnegative().describe('Session duration in milliseconds'),
63
+
64
+ // Summary counts (for quick reference)
65
+ summary: z.object({
66
+ gates_evaluated: z.number().int().nonnegative(),
67
+ gates_passed: z.number().int().nonnegative(),
68
+ gates_failed: z.number().int().nonnegative(),
69
+ actions_executed: z.number().int().nonnegative(),
70
+ errors_encountered: z.number().int().nonnegative(),
71
+ }),
72
+ });
73
+
74
+ export type SessionEndObservation = z.infer<typeof SessionEndObservationSchema>;
75
+
76
+ // =============================================================================
77
+ // 2. PlanSpec Lifecycle Tracking
78
+ // =============================================================================
79
+
80
+ /**
81
+ * Plan Created: New plan authored
82
+ */
83
+ export const PlanCreatedObservationSchema = z.object({
84
+ kind: z.literal('plan_created'),
85
+ session_id: z.string().uuid(),
86
+ timestamp: z.string().datetime(),
87
+
88
+ plan_id: z.string().describe('Unique plan identifier'),
89
+ plan_version: z.string().describe('Initial version (e.g., "1.0")'),
90
+ plan_path: z.string().describe('Path to plan file'),
91
+ plan_hash: z.string().describe('Content hash (for version tracking)'),
92
+
93
+ // Context
94
+ created_by: z.enum(['human', 'ai', 'system']).describe('Who created the plan'),
95
+ source: z.string().optional().describe('Source context (e.g., "github-issue-123")'),
96
+ });
97
+
98
+ export type PlanCreatedObservation = z.infer<typeof PlanCreatedObservationSchema>;
99
+
100
+ /**
101
+ * Plan Edited: Plan modified
102
+ */
103
+ export const PlanEditedObservationSchema = z.object({
104
+ kind: z.literal('plan_edited'),
105
+ session_id: z.string().uuid(),
106
+ timestamp: z.string().datetime(),
107
+
108
+ plan_id: z.string(),
109
+ previous_version: z.string(),
110
+ new_version: z.string(),
111
+ previous_hash: z.string(),
112
+ new_hash: z.string(),
113
+
114
+ edited_by: z.enum(['human', 'ai', 'system']),
115
+ change_summary: z.string().optional().describe('Brief description of changes'),
116
+ });
117
+
118
+ export type PlanEditedObservation = z.infer<typeof PlanEditedObservationSchema>;
119
+
120
+ /**
121
+ * Plan Approved: Human approval recorded
122
+ */
123
+ export const PlanApprovedObservationSchema = z.object({
124
+ kind: z.literal('plan_approved'),
125
+ session_id: z.string().uuid(),
126
+ timestamp: z.string().datetime(),
127
+
128
+ plan_id: z.string(),
129
+ plan_version: z.string(),
130
+ approved_by: z.string().describe('Human identifier (e.g., username, email)'),
131
+ approval_method: z.enum(['cli_confirm', 'explicit_flag', 'ci_gate']).describe('How approved'),
132
+ });
133
+
134
+ export type PlanApprovedObservation = z.infer<typeof PlanApprovedObservationSchema>;
135
+
136
+ /**
137
+ * Plan Rejected: Human rejection recorded
138
+ */
139
+ export const PlanRejectedObservationSchema = z.object({
140
+ kind: z.literal('plan_rejected'),
141
+ session_id: z.string().uuid(),
142
+ timestamp: z.string().datetime(),
143
+
144
+ plan_id: z.string(),
145
+ plan_version: z.string(),
146
+ rejected_by: z.string().describe('Human identifier'),
147
+ rejection_reason: z.string().optional().describe('Why rejected (if provided)'),
148
+ });
149
+
150
+ export type PlanRejectedObservation = z.infer<typeof PlanRejectedObservationSchema>;
151
+
152
+ // =============================================================================
153
+ // 3. Action Provenance (What Actually Happened)
154
+ // =============================================================================
155
+
156
+ /**
157
+ * Action Executed: Observable action taken
158
+ */
159
+ export const ActionExecutedObservationSchema = z.object({
160
+ kind: z.literal('action_executed'),
161
+ session_id: z.string().uuid(),
162
+ timestamp: z.string().datetime(),
163
+
164
+ action_id: z.string().describe('Unique action identifier'),
165
+ action_type: z
166
+ .enum(['command', 'tool_invocation', 'file_write', 'file_delete', 'diff_apply'])
167
+ .describe('Type of action'),
168
+
169
+ // What happened (redacted for security)
170
+ details: z.object({
171
+ command: z.string().optional().describe('Command executed (redacted)'),
172
+ tool_name: z.string().optional().describe('Tool invoked'),
173
+ file_paths: z.array(z.string()).optional().describe('Files touched'),
174
+ diff_summary: z
175
+ .object({
176
+ additions: z.number().int().nonnegative(),
177
+ deletions: z.number().int().nonnegative(),
178
+ files_changed: z.number().int().nonnegative(),
179
+ })
180
+ .optional()
181
+ .describe('Summary of changes (NOT full diff)'),
182
+ working_directory: z.string().describe('Where action executed'),
183
+ environment_target: z.string().optional().describe('Environment (e.g., "dev", "staging")'),
184
+ }),
185
+
186
+ // Governance linkage
187
+ governed_by_gate_id: z.string().optional().describe('Gate evaluation that allowed this'),
188
+ governed_by_plan_id: z.string().optional().describe('Plan that authorized this'),
189
+
190
+ // Outcome
191
+ outcome: z.enum(['success', 'failure', 'partial']),
192
+ exit_code: z.number().int().optional(),
193
+ duration_ms: z.number().int().nonnegative(),
194
+ });
195
+
196
+ export type ActionExecutedObservation = z.infer<typeof ActionExecutedObservationSchema>;
197
+
198
+ // =============================================================================
199
+ // 4. Gate Evaluation Records
200
+ // =============================================================================
201
+
202
+ /**
203
+ * Gate Evaluated: Structured gate check result
204
+ */
205
+ export const GateEvaluatedObservationSchema = z.object({
206
+ kind: z.literal('gate_evaluated'),
207
+ session_id: z.string().uuid(),
208
+ timestamp: z.string().datetime(),
209
+
210
+ gate_eval_id: z.string().describe('Unique gate evaluation identifier'),
211
+ gate_id: z.string().describe('Gate identifier (e.g., "architecture", "coverage")'),
212
+ gate_version: z.string().optional().describe('Gate definition version'),
213
+
214
+ // Inputs (sanitised)
215
+ inputs: z
216
+ .object({
217
+ file_count: z.number().int().nonnegative().optional(),
218
+ changed_files: z.array(z.string()).optional().describe('Files evaluated (paths only)'),
219
+ baseline_hash: z.string().optional().describe('Architecture baseline used'),
220
+ })
221
+ .describe('What was evaluated (no sensitive data)'),
222
+
223
+ // Outcome
224
+ outcome: z.enum(['pass', 'fail', 'error', 'skipped']),
225
+
226
+ // Reasons (rule IDs, not prose)
227
+ rules_evaluated: z.array(z.string()).describe('Rule identifiers checked'),
228
+ rules_violated: z.array(z.string()).optional().describe('Rule identifiers that failed'),
229
+
230
+ // Enforcement
231
+ enforcement: z.enum(['blocking', 'warning', 'informational']).describe('Action taken on failure'),
232
+
233
+ // Metrics
234
+ duration_ms: z.number().int().nonnegative(),
235
+ violation_count: z.number().int().nonnegative().optional(),
236
+ warning_count: z.number().int().nonnegative().optional(),
237
+ });
238
+
239
+ export type GateEvaluatedObservation = z.infer<typeof GateEvaluatedObservationSchema>;
240
+
241
+ // =============================================================================
242
+ // 5. Decision Constraints (Why Options Were Removed)
243
+ // =============================================================================
244
+
245
+ /**
246
+ * Constraint Applied: When Anvil prevents an action
247
+ */
248
+ export const ConstraintAppliedObservationSchema = z.object({
249
+ kind: z.literal('constraint_applied'),
250
+ session_id: z.string().uuid(),
251
+ timestamp: z.string().datetime(),
252
+
253
+ constraint_id: z.string().describe('Constraint identifier'),
254
+ constraint_type: z
255
+ .enum(['policy', 'rule', 'scope', 'environment', 'approval_required'])
256
+ .describe('Type of constraint'),
257
+
258
+ // What was prevented
259
+ prevented_action: z.object({
260
+ action_type: z.string().describe('What was attempted'),
261
+ action_target: z.string().optional().describe('Target (e.g., file path, command)'),
262
+ }),
263
+
264
+ // Why prevented
265
+ reason: z.string().describe('Rule ID or policy name that prevented action'),
266
+ scope: z.string().optional().describe('Scope constraint (e.g., "src/ only")'),
267
+ environment: z.string().optional().describe('Environment constraint (e.g., "not in production")'),
268
+
269
+ // What was available vs what was allowed
270
+ options_available: z.array(z.string()).optional().describe('All possible actions'),
271
+ options_allowed: z.array(z.string()).optional().describe('Actions that passed constraints'),
272
+ });
273
+
274
+ export type ConstraintAppliedObservation = z.infer<typeof ConstraintAppliedObservationSchema>;
275
+
276
+ // =============================================================================
277
+ // 6. Human Inputs (First-Class Events)
278
+ // =============================================================================
279
+
280
+ /**
281
+ * Human Input: User action recorded
282
+ */
283
+ export const HumanInputObservationSchema = z.object({
284
+ kind: z.literal('human_input'),
285
+ session_id: z.string().uuid(),
286
+ timestamp: z.string().datetime(),
287
+
288
+ input_type: z
289
+ .enum(['approval', 'override', 'rejection', 'manual_edit', 'confirmation', 'cancellation'])
290
+ .describe('Type of human action'),
291
+
292
+ // Context
293
+ context: z.object({
294
+ prompt: z.string().optional().describe('What user was asked to decide'),
295
+ target: z.string().optional().describe('What the input was about (e.g., plan_id, gate_id)'),
296
+ }),
297
+
298
+ // Decision
299
+ decision: z.string().describe('What user chose'),
300
+ reason: z.string().optional().describe('User-provided reason (if any)'),
301
+
302
+ // Identity (for accountability)
303
+ user_identifier: z.string().describe('User identifier (username, email, etc.)'),
304
+ });
305
+
306
+ export type HumanInputObservation = z.infer<typeof HumanInputObservationSchema>;
307
+
308
+ // =============================================================================
309
+ // 7. Error and Interruption History
310
+ // =============================================================================
311
+
312
+ /**
313
+ * Error: Failure recorded (not noise, data)
314
+ */
315
+ export const ErrorObservationSchema = z.object({
316
+ kind: z.literal('error'),
317
+ session_id: z.string().uuid(),
318
+ timestamp: z.string().datetime(),
319
+
320
+ error_id: z.string().describe('Unique error identifier'),
321
+ error_type: z
322
+ .enum([
323
+ 'command_failure',
324
+ 'tool_error',
325
+ 'aborted_execution',
326
+ 'partial_state',
327
+ 'validation_failure',
328
+ ])
329
+ .describe('Error category'),
330
+
331
+ // What failed
332
+ context: z.object({
333
+ component: z.string().describe('What was running (e.g., "gate:architecture")'),
334
+ action_id: z.string().optional().describe('Action that failed (if applicable)'),
335
+ gate_id: z.string().optional().describe('Gate that errored (if applicable)'),
336
+ }),
337
+
338
+ // Error details (sanitised)
339
+ error_message: z.string().describe('Error message (redacted if sensitive)'),
340
+ error_code: z.string().optional().describe('Error code (e.g., "ENOENT")'),
341
+ exit_code: z.number().int().optional(),
342
+
343
+ // State
344
+ recoverable: z.boolean().describe('Whether error is recoverable'),
345
+ partial_state_description: z
346
+ .string()
347
+ .optional()
348
+ .describe('Description of partial state if interrupted'),
349
+ });
350
+
351
+ export type ErrorObservation = z.infer<typeof ErrorObservationSchema>;
352
+
353
+ // =============================================================================
354
+ // Observation (Union Type)
355
+ // =============================================================================
356
+
357
+ /**
358
+ * All observation kinds (discriminated union by 'kind')
359
+ */
360
+ export const ObservationSchema = z.discriminatedUnion('kind', [
361
+ SessionStartObservationSchema,
362
+ SessionEndObservationSchema,
363
+ PlanCreatedObservationSchema,
364
+ PlanEditedObservationSchema,
365
+ PlanApprovedObservationSchema,
366
+ PlanRejectedObservationSchema,
367
+ ActionExecutedObservationSchema,
368
+ GateEvaluatedObservationSchema,
369
+ ConstraintAppliedObservationSchema,
370
+ HumanInputObservationSchema,
371
+ ErrorObservationSchema,
372
+ ]);
373
+
374
+ export type Observation =
375
+ | SessionStartObservation
376
+ | SessionEndObservation
377
+ | PlanCreatedObservation
378
+ | PlanEditedObservation
379
+ | PlanApprovedObservation
380
+ | PlanRejectedObservation
381
+ | ActionExecutedObservation
382
+ | GateEvaluatedObservation
383
+ | ConstraintAppliedObservation
384
+ | HumanInputObservation
385
+ | ErrorObservation;
386
+
387
+ // =============================================================================
388
+ // Observation Emission Contract
389
+ // =============================================================================
390
+
391
+ /**
392
+ * What Anvil must emit to be "Kindling-complete"
393
+ *
394
+ * Every Anvil execution must:
395
+ * 1. Emit SessionStartObservation when any command starts
396
+ * 2. Emit SessionEndObservation when command completes (success or failure)
397
+ * 3. Emit GateEvaluatedObservation for every gate check
398
+ * 4. Emit ActionExecutedObservation for every observable action
399
+ * 5. Emit ErrorObservation for every failure (even recoverable)
400
+ * 6. Emit HumanInputObservation for every approval/override/rejection
401
+ * 7. Emit ConstraintAppliedObservation when actions are prevented
402
+ * 8. Emit Plan* observations for all plan lifecycle events
403
+ *
404
+ * Observations are:
405
+ * - Immutable (write-once)
406
+ * - Timestamped (ISO8601)
407
+ * - Linked (session_id, plan_id, gate_id, action_id)
408
+ * - Sanitised (no secrets, redacted commands)
409
+ * - Facts only (no interpretation, no inference)
410
+ */
411
+
412
+ // =============================================================================
413
+ // Integration Points (Where to Emit)
414
+ // =============================================================================
415
+
416
+ /**
417
+ * Anvil codebase integration points:
418
+ *
419
+ * SessionStart/End:
420
+ * - cli/src/commands/*.ts (every command entry/exit)
421
+ * - cli/src/commands/watch.ts (each watch cycle)
422
+ *
423
+ * GateEvaluated:
424
+ * - core/src/gate/gate-runner.ts (GateRunner.run completion)
425
+ * - Each check implementation (architecture, coverage, secrets, etc.)
426
+ *
427
+ * ActionExecuted:
428
+ * - Anywhere Anvil executes commands (via child_process)
429
+ * - File write/delete operations
430
+ * - Diff application
431
+ *
432
+ * Plan*:
433
+ * - core/src/aps/ (plan parsing, validation, execution)
434
+ * - cli/src/commands/plan.ts (plan management commands)
435
+ *
436
+ * HumanInput:
437
+ * - cli/src/tui/ (TUI confirmation prompts)
438
+ * - cli/src/commands/ (CLI --approve flags)
439
+ *
440
+ * ConstraintApplied:
441
+ * - core/src/gate/ (when gate blocks action)
442
+ * - Policy evaluation layers
443
+ *
444
+ * Error:
445
+ * - All try/catch blocks that handle failures
446
+ * - Process error handlers
447
+ * - Validation failures
448
+ */
449
+
450
+ // =============================================================================
451
+ // Validation Utilities
452
+ // =============================================================================
453
+
454
+ /**
455
+ * Validate an observation before emission
456
+ */
457
+ export function validateObservation(data: unknown): {
458
+ success: boolean;
459
+ data?: Observation;
460
+ error?: string;
461
+ } {
462
+ const result = ObservationSchema.safeParse(data);
463
+ if (result.success) {
464
+ return { success: true, data: result.data };
465
+ }
466
+ return { success: false, error: result.error.format()._errors.join(', ') };
467
+ }
468
+
469
+ /**
470
+ * Check if observation contains sensitive data (should never pass validation)
471
+ */
472
+ export function containsSensitiveData(obs: Observation): {
473
+ hasSensitiveData: boolean;
474
+ issues: string[];
475
+ } {
476
+ const issues: string[] = [];
477
+
478
+ // Check for common sensitive patterns
479
+ const payloadStr = JSON.stringify(obs);
480
+
481
+ // Passwords, tokens, keys
482
+ if (/password|token|secret|api[_-]?key|private[_-]?key/i.test(payloadStr)) {
483
+ issues.push('Possible password/token/key detected');
484
+ }
485
+
486
+ // AWS credentials
487
+ if (/AKIA[0-9A-Z]{16}|aws_access_key_id|aws_secret_access_key/i.test(payloadStr)) {
488
+ issues.push('Possible AWS credentials detected');
489
+ }
490
+
491
+ // Email addresses (may be sensitive depending on context)
492
+ if (/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g.test(payloadStr)) {
493
+ issues.push('Email addresses detected (may be sensitive)');
494
+ }
495
+
496
+ return {
497
+ hasSensitiveData: issues.length > 0,
498
+ issues,
499
+ };
500
+ }