@runhalo/engine 0.5.0 → 0.6.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 (40) hide show
  1. package/dist/context-analyzer.js +38 -31
  2. package/dist/context-analyzer.js.map +1 -1
  3. package/dist/fp-patterns.d.ts +36 -0
  4. package/dist/fp-patterns.js +426 -0
  5. package/dist/fp-patterns.js.map +1 -0
  6. package/dist/frameworks/angular.d.ts +11 -0
  7. package/dist/frameworks/angular.js +41 -0
  8. package/dist/frameworks/angular.js.map +1 -0
  9. package/dist/frameworks/index.js +6 -0
  10. package/dist/frameworks/index.js.map +1 -1
  11. package/dist/frameworks/react.d.ts +13 -0
  12. package/dist/frameworks/react.js +36 -0
  13. package/dist/frameworks/react.js.map +1 -0
  14. package/dist/frameworks/vue.d.ts +9 -0
  15. package/dist/frameworks/vue.js +39 -0
  16. package/dist/frameworks/vue.js.map +1 -0
  17. package/dist/graduation/fp-verdict-logger.d.ts +81 -0
  18. package/dist/graduation/fp-verdict-logger.js +130 -0
  19. package/dist/graduation/fp-verdict-logger.js.map +1 -0
  20. package/dist/graduation/graduation-codifier.d.ts +37 -0
  21. package/dist/graduation/graduation-codifier.js +205 -0
  22. package/dist/graduation/graduation-codifier.js.map +1 -0
  23. package/dist/graduation/graduation-validator.d.ts +73 -0
  24. package/dist/graduation/graduation-validator.js +204 -0
  25. package/dist/graduation/graduation-validator.js.map +1 -0
  26. package/dist/graduation/index.d.ts +71 -0
  27. package/dist/graduation/index.js +105 -0
  28. package/dist/graduation/index.js.map +1 -0
  29. package/dist/graduation/pattern-aggregator.d.ts +77 -0
  30. package/dist/graduation/pattern-aggregator.js +154 -0
  31. package/dist/graduation/pattern-aggregator.js.map +1 -0
  32. package/dist/index.d.ts +75 -0
  33. package/dist/index.js +632 -73
  34. package/dist/index.js.map +1 -1
  35. package/dist/review-board/two-agent-review.d.ts +152 -0
  36. package/dist/review-board/two-agent-review.js +463 -0
  37. package/dist/review-board/two-agent-review.js.map +1 -0
  38. package/package.json +5 -2
  39. package/rules/coppa-tier-1.yaml +17 -10
  40. package/rules/rules.json +408 -40
package/dist/index.js CHANGED
@@ -45,11 +45,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
45
45
  };
46
46
  Object.defineProperty(exports, "__esModule", { value: true });
47
47
  exports.SCAFFOLD_REGISTRY = exports.detectFramework = exports.ScaffoldEngine = exports.ComplianceScoreEngine = exports.transformSetDefault = exports.transformSanitizeInput = exports.transformRemoveDefault = exports.transformUrlUpgrade = exports.FixEngine = exports.REMEDIATION_MAP = exports.HaloEngine = exports.AU_SBD_RULES = exports.AI_AUDIT_RULES = exports.ETHICAL_RULES = exports.COPPA_RULES = exports.treeSitterParser = exports.TreeSitterParser = void 0;
48
+ exports.classifyFile = classifyFile;
48
49
  exports.loadRulesFromYAML = loadRulesFromYAML;
49
50
  exports.loadRulesFromJSON = loadRulesFromJSON;
50
51
  exports.loadRulesFromJSONByPack = loadRulesFromJSONByPack;
51
52
  exports.compileRawRules = compileRawRules;
52
53
  exports.parseHaloignore = parseHaloignore;
54
+ exports.isVendorPath = isVendorPath;
55
+ exports.isDocGeneratorPath = isDocGeneratorPath;
53
56
  exports.shouldIgnoreFile = shouldIgnoreFile;
54
57
  exports.shouldIgnoreViolation = shouldIgnoreViolation;
55
58
  exports.getRemediation = getRemediation;
@@ -78,6 +81,12 @@ function extractCategory(ruleId) {
78
81
  return 'ai-risk';
79
82
  if (ruleId.startsWith('AI-TRANSPARENCY'))
80
83
  return 'ai-transparency';
84
+ if (ruleId.startsWith('AI-GOVERNANCE'))
85
+ return 'ai-governance';
86
+ if (ruleId.startsWith('AI-OVERSIGHT'))
87
+ return 'ai-oversight';
88
+ if (ruleId.startsWith('AI-ACCURACY'))
89
+ return 'ai-accuracy';
81
90
  if (ruleId.startsWith('CAI-'))
82
91
  return 'constitutional-ai';
83
92
  const match = ruleId.match(/^coppa-(\w+)-\d+$/);
@@ -96,6 +105,140 @@ function detectLanguage(filePath) {
96
105
  };
97
106
  return langMap[ext] || 'unknown';
98
107
  }
108
+ /**
109
+ * Sprint 13a: Classify a file using deterministic heuristics.
110
+ * Returns a FileClassification object that the scan loop uses to skip
111
+ * files or suppress specific rules.
112
+ *
113
+ * @param filePath — normalized file path (forward slashes)
114
+ * @param contentPrefix — first 3000 chars of file content (for decorator/annotation detection)
115
+ */
116
+ function classifyFile(filePath, contentPrefix = '') {
117
+ const normalized = filePath.replace(/\\/g, '/');
118
+ const language = detectLanguage(filePath);
119
+ const isVendorResult = isVendorPath(filePath);
120
+ const isDocGeneratorResult = isDocGeneratorPath(filePath);
121
+ // Test/spec/fixture detection (Sprint 8 + 11a, consolidated)
122
+ const isTest = /\.(test|spec)\.(ts|tsx|js|jsx|py|rb|java|go)$/i.test(normalized) ||
123
+ /(^|\/)__tests__\//.test(normalized) ||
124
+ /(^|\/)test\//.test(normalized) ||
125
+ /(^|\/)tests\//.test(normalized) ||
126
+ /(^|\/)spec\//.test(normalized) ||
127
+ /(^|\/)fixtures\//.test(normalized) ||
128
+ /\.(stories|story)\.(ts|tsx|js|jsx)$/i.test(normalized) ||
129
+ /(^|\/)cypress\//.test(normalized) ||
130
+ /(^|\/)e2e\//.test(normalized) ||
131
+ /jest\.config|vitest\.config|playwright\.config/i.test(normalized) ||
132
+ // Sprint 11a: Test environment configs
133
+ /(^|\/)envs\/test[^/]*\.(py|json|ya?ml|toml|cfg|ini)$/i.test(normalized) ||
134
+ /(^|\/)config\/test[^/]*\.(py|json|ya?ml|toml|cfg|ini|js|ts)$/i.test(normalized) ||
135
+ /(^|\/)settings\/test[^/]*\.(py|json|ya?ml|toml)$/i.test(normalized) ||
136
+ /(^|\/)conftest\.py$/i.test(normalized);
137
+ // Consent/privacy implementation files (Sprint 10b)
138
+ const CONSENT_PATH_PATTERNS = /(?:^|\/)(?:consent|cookie[_-]?(?:consent|banner|preferences|notice|policy)|privacy[_-]?(?:policy|notice|banner|settings)|gdpr|ccpa|compliance|data[_-]?(?:deletion|removal|protection))\b/i;
139
+ const isConsent = CONSENT_PATH_PATTERNS.test(normalized);
140
+ // Admin/instructor/staff backend paths (Sprint 11a, updated Sprint 13b)
141
+ // Matches admin directories AND admin.py/admin.rb files (Django/Rails admin registration modules)
142
+ const ADMIN_PATH_PATTERNS = /(?:^|\/)(?:admin|instructor|teacher|staff|management|backoffice|dashboard\/admin|cms|moderator|superuser)(?:\/|\.py|\.rb|\.php|$)/i;
143
+ const isAdmin = ADMIN_PATH_PATTERNS.test(normalized) ||
144
+ /(?:@staff_member_required|@permission_required|@user_passes_test|@login_required.*staff|@admin_required|is_staff|is_superuser)/i.test(contentPrefix);
145
+ // === Sprint 13a: New heuristic patterns ===
146
+ // Django migrations — auto-generated schema changes, no user-facing code
147
+ const isDjangoMigration = /(^|\/)migrations\/\d{4}_[a-zA-Z0-9_]+\.py$/i.test(normalized) ||
148
+ /(^|\/)migrations\/__init__\.py$/i.test(normalized);
149
+ // Rails fixture and seed files — test data, not production behavior
150
+ const isFixtureOrSeed = /(^|\/)fixtures\/[^/]+\.(ya?ml|json|csv)$/i.test(normalized) ||
151
+ /(^|\/)seeds?\//i.test(normalized) ||
152
+ /(^|\/)db\/seeds/i.test(normalized) ||
153
+ /(^|\/)factories?\//i.test(normalized) ||
154
+ /(^|\/)factory\.(ts|js|py|rb)$/i.test(normalized);
155
+ // Mock/factory files — test infrastructure
156
+ const isMockOrFactory = /(?:^|\/)(?:__mocks__|mocks?|fakes?|stubs?)(?:\/|$)/i.test(normalized) ||
157
+ /\.mock\.(ts|tsx|js|jsx|py)$/i.test(normalized) ||
158
+ /\.fake\.(ts|tsx|js|jsx|py)$/i.test(normalized) ||
159
+ /(?:^|\/)(?:mock|fake|stub)[_-]?\w+\.(ts|tsx|js|jsx|py)$/i.test(normalized) ||
160
+ /(?:^|\/)(?:\w+)?[_-](?:mock|fake|stub)\.(ts|tsx|js|jsx|py)$/i.test(normalized);
161
+ // CI/CD configuration files — pipeline definitions, not application code
162
+ const isCIConfig = /(^|\/)\.github\/workflows\//i.test(normalized) ||
163
+ /(^|\/)\.github\/actions\//i.test(normalized) ||
164
+ /(^|\/)\.circleci\//i.test(normalized) ||
165
+ /(^|\/)\.gitlab-ci/i.test(normalized) ||
166
+ /(^|\/)Jenkinsfile$/i.test(normalized) ||
167
+ /(^|\/)\.travis\.yml$/i.test(normalized) ||
168
+ /(^|\/)azure-pipelines/i.test(normalized) ||
169
+ /(^|\/)bitbucket-pipelines/i.test(normalized) ||
170
+ /(^|\/)\.buildkite\//i.test(normalized) ||
171
+ /(^|\/)Dockerfile$/i.test(normalized) ||
172
+ /(^|\/)docker-compose/i.test(normalized);
173
+ // Build output directories — generated code, not source
174
+ const isBuildOutput = /(^|\/)dist\//i.test(normalized) ||
175
+ /(^|\/)build\/(?!src)/i.test(normalized) || // build/ but not build/src/
176
+ /(^|\/)\.next\//i.test(normalized) ||
177
+ /(^|\/)\.nuxt\//i.test(normalized) ||
178
+ /(^|\/)\.svelte-kit\//i.test(normalized) ||
179
+ /(^|\/)out\//i.test(normalized) ||
180
+ /(^|\/)\.output\//i.test(normalized) ||
181
+ /(^|\/)coverage\//i.test(normalized) ||
182
+ /(^|\/)\.cache\//i.test(normalized) ||
183
+ /(^|\/)\.parcel-cache\//i.test(normalized) ||
184
+ /(^|\/)\.turbo\//i.test(normalized);
185
+ // Type definition files — no runtime behavior, only type annotations
186
+ const isTypeDefinition = /\.d\.ts$/i.test(normalized) ||
187
+ /\.pyi$/i.test(normalized) ||
188
+ /(^|\/)@types\//i.test(normalized);
189
+ // Storybook stories — UI component demos, not production code
190
+ const isStorybook = /\.(stories|story)\.(ts|tsx|js|jsx|mdx)$/i.test(normalized) ||
191
+ /(^|\/)\.storybook\//i.test(normalized);
192
+ // Determine if file should be completely skipped
193
+ // (vendor and doc generator are already handled at file-discovery level,
194
+ // but including here for completeness in the classification)
195
+ let shouldSkip = false;
196
+ let skipReason;
197
+ if (isVendorResult) {
198
+ shouldSkip = true;
199
+ skipReason = 'vendor-library';
200
+ }
201
+ else if (isDocGeneratorResult) {
202
+ shouldSkip = true;
203
+ skipReason = 'doc-generator';
204
+ }
205
+ else if (isDjangoMigration) {
206
+ shouldSkip = true;
207
+ skipReason = 'django-migration';
208
+ }
209
+ else if (isBuildOutput) {
210
+ shouldSkip = true;
211
+ skipReason = 'build-output';
212
+ }
213
+ else if (isTypeDefinition) {
214
+ shouldSkip = true;
215
+ skipReason = 'type-definition';
216
+ }
217
+ else if (isCIConfig) {
218
+ shouldSkip = true;
219
+ skipReason = 'ci-config';
220
+ }
221
+ // Note: test, consent, admin, mock, fixture, storybook files are NOT fully skipped
222
+ // They get per-rule suppression instead (some rules ARE valid in these files)
223
+ return {
224
+ method: 'heuristic',
225
+ language,
226
+ isVendor: isVendorResult,
227
+ isTest,
228
+ isConsent,
229
+ isAdmin,
230
+ isDocGenerator: isDocGeneratorResult,
231
+ isDjangoMigration,
232
+ isFixtureOrSeed,
233
+ isMockOrFactory,
234
+ isCIConfig,
235
+ isBuildOutput,
236
+ isTypeDefinition,
237
+ isStorybook,
238
+ shouldSkip,
239
+ skipReason,
240
+ };
241
+ }
99
242
  // YAML Rule Loader - Load rules from coppa-tier-1.yaml
100
243
  function loadRulesFromYAML(yamlPath) {
101
244
  try {
@@ -378,7 +521,7 @@ exports.COPPA_RULES = [
378
521
  /LoginManager\.getInstance\s*\(\s*\)\s*\.logIn/gi
379
522
  ],
380
523
  fixSuggestion: 'Wrap the auth call in a conditional check for user.age >= 13 or use signInWithParentEmail() for children',
381
- penalty: '$51,744 per violation',
524
+ penalty: '$53,088 per violation',
382
525
  languages: ['typescript', 'javascript', 'python', 'go', 'java', 'kotlin', 'swift']
383
526
  },
384
527
  {
@@ -390,11 +533,19 @@ exports.COPPA_RULES = [
390
533
  /(\?|&)(email|first_?name|last_?name|dob|phone|birthdate)=/gi,
391
534
  /axios\.get\s*\(\s*[`'"]https?:\/\/[^\s]*\?[^`'"]*\$\{/gi,
392
535
  /fetch\s*\(\s*[`'"]https?:\/\/[^\s]*\?[^`'"]*\$\{/gi,
393
- /\?[^'"`\s]*\$\{[^}]*(?:\.email|\.firstName|\.lastName|\.dob|\.phone)[^}]*\}/gi
536
+ /\?[^'"`\s]*\$\{[^}]*(?:\.email|\.firstName|\.lastName|\.dob|\.phone)[^}]*\}/gi,
537
+ // Python — requests.get with PII query params
538
+ /requests\.get\s*\([^)]*params\s*=\s*\{[^}]*(?:email|name|phone|dob|birthdate)/gi,
539
+ // Python — Django/Flask redirect with PII in URL
540
+ /(?:redirect|HttpResponseRedirect)\s*\([^)]*\?[^)]*(?:email|name|phone)/gi,
541
+ // PHP — PII in $_GET superglobal
542
+ /\$_GET\s*\[\s*['"](?:email|first_?name|last_?name|dob|phone|birthdate)['"]\s*\]/gi,
543
+ // Ruby — params[] with PII in GET context
544
+ /request\.query_parameters\s*\[\s*:(?:email|name|phone|dob|birthdate)\s*\]/gi
394
545
  ],
395
546
  fixSuggestion: 'Switch to POST method and move PII to request body',
396
- penalty: '$51,744 per violation',
397
- languages: ['typescript', 'javascript', 'python', 'java', 'swift']
547
+ penalty: '$53,088 per violation',
548
+ languages: ['typescript', 'javascript', 'python', 'java', 'swift', 'php', 'ruby']
398
549
  },
399
550
  {
400
551
  id: 'coppa-tracking-003',
@@ -406,11 +557,23 @@ exports.COPPA_RULES = [
406
557
  /ga\s*\(\s*['"]create['"]/gi,
407
558
  /adsbygoogle/gi,
408
559
  /gtag\s*\(\s*['"]config['"]/gi,
409
- /google-analytics\.com\/analytics\.js/gi
560
+ /google-analytics\.com\/analytics\.js/gi,
561
+ // Python — Google Analytics measurement protocol
562
+ /(?:import|from)\s+(?:google\.analytics|pyga|universal_analytics)/gi,
563
+ // Python — Facebook pixel server-side
564
+ /FacebookAdsApi\.init|facebook_business\.adobjects/gi,
565
+ // PHP — Google Analytics server-side
566
+ /(?:TheIconic\\Tracking|Rize\\UriTemplate).*(?:Analytics|Measurement)/gi,
567
+ // PHP — wp_enqueue_script with GA/FB pixel
568
+ /wp_enqueue_script\s*\([^)]*(?:google-analytics|gtag|fbq|facebook-pixel)/gi,
569
+ // Ruby — Google Analytics gems
570
+ /(?:require|gem)\s+['"](?:staccato|google-analytics-rails|gabba)['"]/gi,
571
+ // Java/Kotlin — Firebase Analytics initialization
572
+ /FirebaseAnalytics\.getInstance\s*\(/gi
410
573
  ],
411
574
  fixSuggestion: 'Add "child_directed_treatment": true or "restrictDataProcessing": true to SDK initialization',
412
- penalty: '$51,744 per violation',
413
- languages: ['typescript', 'javascript', 'html']
575
+ penalty: '$53,088 per violation',
576
+ languages: ['typescript', 'javascript', 'html', 'python', 'php', 'ruby', 'java', 'kotlin']
414
577
  },
415
578
  {
416
579
  id: 'coppa-geo-004',
@@ -437,17 +600,24 @@ exports.COPPA_RULES = [
437
600
  // Python — geopy geolocators
438
601
  /(?:Nominatim|GoogleV3|Bing)\s*\([^)]*\)\s*\.(?:geocode|reverse)/gi,
439
602
  // Android manifest — fine location permission
440
- /android\.permission\.ACCESS_FINE_LOCATION/gi
603
+ /android\.permission\.ACCESS_FINE_LOCATION/gi,
604
+ // PHP — geolocation APIs
605
+ /(?:geoip_record_by_name|geoip_country_code_by_name|maxmind)\s*\(/gi,
606
+ // PHP — WordPress geolocation
607
+ /WC_Geolocation::geolocate_ip|wp_geolocate/gi,
608
+ // Ruby — Geocoder gem
609
+ /Geocoder\.search\s*\(|geocode_by\s+:/gi,
610
+ /reverse_geocoded_by\s+:/gi
441
611
  ],
442
612
  fixSuggestion: 'Downgrade accuracy to kCLLocationAccuracyThreeKilometers or require parental consent',
443
- penalty: '$51,744 per violation',
444
- languages: ['typescript', 'javascript', 'swift', 'kotlin', 'java', 'python', 'xml']
613
+ penalty: '$53,088 per violation',
614
+ languages: ['typescript', 'javascript', 'swift', 'kotlin', 'java', 'python', 'xml', 'php', 'ruby']
445
615
  },
446
616
  {
447
617
  id: 'coppa-retention-005',
448
618
  name: 'Missing Data Retention Policy',
449
619
  severity: 'medium',
450
- description: 'User schemas must have deleted_at, expiration_date, or TTL index for data retention',
620
+ description: 'COPPA 2025 explicitly prohibits indefinite retention of children\'s PI. Operators must retain data only as long as reasonably necessary for the purpose collected. Schemas with PII fields must define retention periods, deletion mechanisms, and purpose limitation.',
451
621
  patterns: [
452
622
  // JS/TS — Mongoose schemas
453
623
  /new\s+Schema\s*\(\s*\{[^{}]*\}/gi,
@@ -460,11 +630,17 @@ exports.COPPA_RULES = [
460
630
  // Java/Kotlin — JPA @Entity on user-related classes
461
631
  /@Entity[\s\S]*?class\s+(?:User|Child|Student|Profile|Account|Member)/gi,
462
632
  // Kotlin — data class for user models
463
- /data\s+class\s+(?:User|Child|Student|Profile|Account|Member)\w*\s*\(/gi
633
+ /data\s+class\s+(?:User|Child|Student|Profile|Account|Member)\w*\s*\(/gi,
634
+ // PHP — Laravel/WordPress user models
635
+ /class\s+(?:User|Child|Student|Profile|Account|Member)\w*\s+extends\s+(?:Model|Authenticatable|WP_User)/gi,
636
+ // Ruby — ActiveRecord user models
637
+ /class\s+(?:User|Child|Student|Profile|Account|Member)\w*\s*<\s*(?:ApplicationRecord|ActiveRecord::Base)/gi,
638
+ // Android — SharedPreferences/Editor storing user PII
639
+ /(?:putString|putInt|putBoolean)\s*\(\s*['"](?:user_?(?:name|email|id|phone)|child_?(?:name|email|id|dob)|student_?(?:name|email|id)|email|phone|dob|birthdate)['"]/gi
464
640
  ],
465
- fixSuggestion: 'Add deleted_at column, expiration_date field, or TTL index to database schema',
466
- penalty: 'Regulatory audit failure',
467
- languages: ['typescript', 'javascript', 'python', 'go', 'java', 'kotlin', 'sql']
641
+ fixSuggestion: 'Add explicit retention period (retentionDays, expiresAt, or TTL index), deleted_at column, and document the purpose limitation for data collection per COPPA 2025 § 312.10',
642
+ penalty: '$53,088 per violation (COPPA 2025 indefinite retention prohibition)',
643
+ languages: ['typescript', 'javascript', 'python', 'go', 'java', 'kotlin', 'sql', 'php', 'ruby']
468
644
  },
469
645
  // ========== Rules 6-20 (Sprint 2) ==========
470
646
  // Rule 6: Unencrypted PII Transmission
@@ -478,11 +654,18 @@ exports.COPPA_RULES = [
478
654
  /http:\/\/localhost:[^\s]*(\/api\/)/gi,
479
655
  /axios\.get\s*\(\s*['"]http:\/\//gi,
480
656
  /fetch\s*\(\s*['"]http:\/\//gi,
481
- /http:\/\/[^\s]*email[^\s]*/gi
657
+ /http:\/\/[^\s]*email[^\s]*/gi,
658
+ // Python — requests/urllib with HTTP
659
+ /requests\.(?:get|post)\s*\(\s*['"]http:\/\/(?!localhost)/gi,
660
+ /urllib\.request\.urlopen\s*\(\s*['"]http:\/\/(?!localhost)/gi,
661
+ // PHP — HTTP API calls
662
+ /(?:curl_setopt|file_get_contents|wp_remote_get)\s*\([^)]*['"]http:\/\/(?!localhost)/gi,
663
+ // Ruby — HTTP requests
664
+ /(?:Net::HTTP|HTTParty|Faraday)\.(?:get|post)\s*\([^)]*['"]http:\/\/(?!localhost)/gi
482
665
  ],
483
666
  fixSuggestion: 'Replace http:// with https:// for all API endpoints and resources',
484
667
  penalty: 'Security breach liability + COPPA penalties',
485
- languages: ['typescript', 'javascript', 'python', 'java', 'swift']
668
+ languages: ['typescript', 'javascript', 'python', 'java', 'swift', 'php', 'ruby']
486
669
  },
487
670
  // Rule 7: Passive Audio Recording
488
671
  // Fixed Sprint 4: Skip audio:false, skip AudioContext (playback only), skip import-only
@@ -498,11 +681,18 @@ exports.COPPA_RULES = [
498
681
  /AVAudioSession\s*\.\s*sharedInstance/gi,
499
682
  /AVAudioRecorder\s*\(/gi,
500
683
  /new\s+AudioRecord\s*\(/gi,
501
- /new\s+MediaRecorder\s*\(/gi
684
+ /new\s+MediaRecorder\s*\(/gi,
685
+ // Python — audio recording libraries
686
+ /(?:import|from)\s+(?:pyaudio|sounddevice|speech_recognition)/gi,
687
+ /sounddevice\.rec\s*\(/gi,
688
+ /Recognizer\(\)\.listen/gi,
689
+ // Java/Kotlin — Android AudioRecord
690
+ /AudioRecord\.Builder\s*\(\s*\)/gi,
691
+ /MediaRecorder\s*\(\s*\)\s*\.setAudioSource/gi
502
692
  ],
503
693
  fixSuggestion: 'Wrap audio recording in click handler and add parental consent check',
504
- penalty: '$51,744 per violation',
505
- languages: ['typescript', 'javascript', 'swift', 'kotlin']
694
+ penalty: '$53,088 per violation',
695
+ languages: ['typescript', 'javascript', 'swift', 'kotlin', 'python', 'java']
506
696
  },
507
697
  // Rule 8: Missing Privacy Policy Link
508
698
  // Fixed Sprint 4: Only flag forms with registration-related fields (email, password, name, DOB)
@@ -519,11 +709,17 @@ exports.COPPA_RULES = [
519
709
  // kebab-case / snake_case: sign-up-form, register_form, create-account-form
520
710
  /\b(?:sign[-_]?up|register|registration|create[-_]?account)[-_]form\b/gi,
521
711
  // HTML form elements with registration-related ids/classes
522
- /<form[^>]*(?:id|class|name)\s*=\s*["'][^"']*(?:register|signup|sign[-_]up|create[-_]account)[^"']*["']/gi
712
+ /<form[^>]*(?:id|class|name)\s*=\s*["'][^"']*(?:register|signup|sign[-_]up|create[-_]account)[^"']*["']/gi,
713
+ // Python — Django/Flask registration form classes
714
+ /class\s+(?:SignUp|Register|Registration|CreateAccount)Form\s*\(\s*(?:forms\.Form|ModelForm|FlaskForm)/gi,
715
+ // Ruby — Rails registration routes/controllers
716
+ /def\s+(?:sign_up|register|create_account)\b/gi,
717
+ // PHP — WordPress registration hooks
718
+ /(?:register_new_user|wp_create_user|user_register)\s*\(/gi
523
719
  ],
524
720
  fixSuggestion: 'Add <a href="/privacy">Privacy Policy</a> link to registration form footer',
525
721
  penalty: 'Compliance failure',
526
- languages: ['typescript', 'javascript', 'html', 'tsx', 'jsx', 'php']
722
+ languages: ['typescript', 'javascript', 'html', 'tsx', 'jsx', 'php', 'python', 'ruby']
527
723
  },
528
724
  // Rule 9: Contact Info Collection Without Parent Email
529
725
  {
@@ -533,15 +729,24 @@ exports.COPPA_RULES = [
533
729
  description: 'Forms collecting child email/phone must also require parent email for consent verification',
534
730
  patterns: [
535
731
  /(child_email|student_email)\s*:\s*String/gi,
536
- /(child_email|student_email|kid_email)\s*=/gi
732
+ /(child_email|student_email|kid_email)\s*=/gi,
733
+ // Python — Django model field for child contact
734
+ /(?:child_email|student_email|kid_email)\s*=\s*models\.(?:EmailField|CharField)/gi,
735
+ // PHP — child email in form processing
736
+ /\$(?:child_email|student_email|kid_email)\s*=\s*\$_(?:POST|GET|REQUEST)/gi,
737
+ // Ruby — child contact in params or model
738
+ /(?:child_email|student_email|kid_email)\s*=\s*params\[/gi,
739
+ // Java/Kotlin — child email field
740
+ /(?:private|var|val)\s+\w*\s*(?:childEmail|studentEmail|kidEmail)/gi
537
741
  ],
538
742
  fixSuggestion: 'Make parent_email required when collecting child contact information',
539
- penalty: '$51,744 per violation',
540
- languages: ['typescript', 'javascript', 'python']
743
+ penalty: '$53,088 per violation',
744
+ languages: ['typescript', 'javascript', 'python', 'php', 'ruby', 'java', 'kotlin']
541
745
  },
542
746
  // Rule 10: Insecure Default Passwords
543
747
  {
544
748
  id: 'coppa-sec-010',
749
+ is_active: false, // Sprint 16 W1: 100% FP (0/3 TP) — all hits are test fixture passwords, not production defaults
545
750
  name: 'Weak Default Student Passwords',
546
751
  severity: 'medium',
547
752
  description: 'Default passwords like "password", "123456", or "changeme" create security vulnerabilities',
@@ -570,50 +775,79 @@ exports.COPPA_RULES = [
570
775
  /Freshdesk|FreshChat/gi
571
776
  ],
572
777
  fixSuggestion: 'Disable chat widget for unauthenticated or under-13 users via conditional rendering',
573
- penalty: '$51,744 per violation',
778
+ penalty: '$53,088 per violation',
574
779
  languages: ['typescript', 'javascript', 'html']
575
780
  },
576
781
  // Rule 12: Biometric Data Collection
782
+ // Sprint 15: DISABLED — 0% TP precision (46 entries, ALL false positives).
783
+ // Pattern matches generic terms (FaceID, TouchID, FaceDetector) without
784
+ // distinguishing real biometric capture from SDK type definitions, AWS API
785
+ // schemas, and vendor library code. Rebuild requires AST-level context.
577
786
  {
578
787
  id: 'coppa-bio-012',
579
788
  name: 'Biometric Data Collection',
580
789
  severity: 'critical',
581
- description: 'Face recognition, voice prints, or gait analysis requires explicit parental consent. COPPA 2.0 explicitly classifies biometrics as PI.',
790
+ is_active: false,
791
+ description: 'COPPA 2025 explicitly adds biometric identifiers to the definition of PI. Face recognition, voice prints, gait analysis, behavioral biometrics (keystroke dynamics, mouse movement patterns), iris/pupil scanning, and health biometric APIs all require verifiable parental consent.',
582
792
  patterns: [
583
793
  /(?:import\s+.*from\s+['"]face-api\.js['"]|require\s*\(\s*['"]face-api\.js['"]\s*\))/gi,
584
794
  /LocalAuthentication.*evaluatePolicy/gi,
585
- /FaceID|TouchID/gi,
586
- /biometricAuth|BiometricAuth/g,
587
- /voicePrint|VoicePrint/g,
588
- /livenessCheck|LivenessCheck/g,
589
- /FaceMatcher|FaceDetector|FaceRecognizer/g
795
+ /(?:biometricAuth|BiometricAuth|biometricPrompt|BiometricPrompt)/g,
796
+ /voicePrint|VoicePrint|voiceRecognition|VoiceRecognition|speakerVerification/g,
797
+ /livenessCheck|LivenessCheck|livenessDetection/g,
798
+ /FaceMatcher|FaceDetector|FaceRecognizer|FaceLandmarks/g,
799
+ // Behavioral biometrics (COPPA 2025 expansion)
800
+ /keystrokeDynamic|keystrokePattern|typingBiometric|keyPressAnalysis/g,
801
+ /gaitAnalysis|gaitDetect|gaitRecognition|motionBiometric/g,
802
+ /mouseMovementPattern|cursorTracking|behavioralBiometric/g,
803
+ /irisScann?|pupilDetect|eyeTracking|gazeTracking/gi,
804
+ // Health biometric APIs
805
+ /(?:HKHealthStore|HKQuantityType|HealthKit).*(?:heartRate|stepCount|workout|sleep)/gi,
806
+ /(?:GoogleFit|FitnessOptions|HistoryClient).*(?:heartRate|steps|calories|sleep)/gi,
807
+ // Face detection libraries
808
+ /(?:import|require).*(?:face-api|@mediapipe\/face|@tensorflow\/tfjs-models\/face|deepface|insightface)/gi
590
809
  ],
591
- fixSuggestion: 'Ensure biometric data remains local-only (on-device) or obtain verifiable parental consent',
592
- penalty: '$51,744 per violation',
593
- languages: ['typescript', 'javascript', 'swift', 'kotlin']
810
+ fixSuggestion: 'Ensure biometric data remains local-only (on-device) or obtain verifiable parental consent per COPPA 2025. Do not transmit biometric identifiers to servers without separate parental consent.',
811
+ penalty: '$53,088 per violation',
812
+ languages: ['typescript', 'javascript', 'swift', 'kotlin', 'python', 'java']
594
813
  },
595
814
  // Rule 13: Push Notifications to Children
815
+ // Rebuilt Sprint 18: removed generic Notification constructor & requestPermission (94.4% FP).
816
+ // Now targets push subscription/registration APIs only.
596
817
  {
597
818
  id: 'coppa-notif-013',
598
819
  name: 'Direct Push Notifications Without Consent',
599
- severity: 'medium',
600
- description: 'Push notifications are "Online Contact Info" under COPPA 2.0. Direct notifications to children require parental consent.',
820
+ severity: 'low',
821
+ description: 'FTC declined to codify push notification restrictions in the 2025 final rule but stated it remains concerned about push notifications and engagement techniques. Best practice: gate push subscriptions behind parental consent. Maps to NGL Labs and Sendit enforcement patterns.',
601
822
  patterns: [
602
- /FirebaseMessaging\.subscribeToTopic/gi,
603
- /OneSignal\.promptForPushNotifications/gi,
604
- /sendPushNotification\s*\(/gi,
605
- /fcm\.send\s*\(/gi,
606
- /PushManager\.subscribe\s*\(/gi,
607
- /Notification\.requestPermission/gi,
608
- /new\s+Notification\s*\(/gi
823
+ /FirebaseMessaging\.subscribeToTopic/g,
824
+ /OneSignal\.(?:promptForPushNotifications|init)\s*\(/g,
825
+ /sendPushNotification\s*\(/g,
826
+ /fcm\.send\s*\(/g,
827
+ /PushManager\.subscribe\s*\(/g,
828
+ /pushManager\.subscribe\s*\(/g,
829
+ /messaging\(\)\.getToken\s*\(/g,
830
+ /registerForPushNotifications\s*\(/g,
831
+ /addEventListener\s*\(\s*['"]push['"]/g,
832
+ /expo-notifications/g,
833
+ /react-native-push-notification/g,
834
+ // Python — Django push notification libraries
835
+ /(?:import|from)\s+(?:webpush|pywebpush|push_notifications|django_push_notifications)/gi,
836
+ /webpush\.send\s*\(/gi,
837
+ // PHP — web-push-php library
838
+ /(?:new\s+)?WebPush\s*\(\s*\[/gi,
839
+ /\$webPush->sendOneNotification/gi,
840
+ // Ruby — web-push gem
841
+ /WebPush\.payload_send\s*\(/gi
609
842
  ],
610
843
  fixSuggestion: 'Gate push notification subscription behind parental dashboard setting',
611
- penalty: '$51,744 per violation',
612
- languages: ['typescript', 'javascript', 'swift', 'kotlin']
844
+ penalty: '$53,088 per violation',
845
+ languages: ['typescript', 'javascript', 'swift', 'kotlin', 'python', 'php', 'ruby']
613
846
  },
614
847
  // Rule 14: Unfiltered User Generated Content
615
848
  {
616
849
  id: 'coppa-ugc-014',
850
+ is_active: false, // Sprint 16 W1: 100% FP (0/3 TP) — all hits are API model property assignments, not child UGC
617
851
  name: 'UGC Upload Without PII Filter',
618
852
  severity: 'high',
619
853
  description: 'Text areas for "bio", "about me", or comments must pass through PII scrubbing before database storage',
@@ -626,7 +860,7 @@ exports.COPPA_RULES = [
626
860
  /commentForm.*submit|handleCommentSubmit/gi
627
861
  ],
628
862
  fixSuggestion: 'Add middleware hook for PII scrubbing (regex or AWS Comprehend) before database storage',
629
- penalty: '$51,744 per violation',
863
+ penalty: '$53,088 per violation',
630
864
  languages: ['typescript', 'javascript', 'python']
631
865
  },
632
866
  // Rule 15: XSS Vulnerabilities
@@ -641,11 +875,21 @@ exports.COPPA_RULES = [
641
875
  /\.innerHTML\s*=\s*\$\{/gi,
642
876
  /\.innerHTML\s*=\s*(?!['"]?\s*['"]?\s*;)(?!.*[Ll]ocal(?:ize|ization))(?!.*styleContent)[^;]*\b(?:user|input|query|param|req\.|request\.|body\.|data\.)\w*/gi,
643
877
  /\.html\s*\(\s*(?:user|req\.|request\.|params?\.)/gi,
644
- /v-html\s*=\s*["']?(?!.*sanitize)/gi
878
+ /v-html\s*=\s*["']?(?!.*sanitize)/gi,
879
+ // PHP — echo/print user input without escaping
880
+ /echo\s+\$_(?:GET|POST|REQUEST)\s*\[/gi,
881
+ // PHP — WordPress unescaped output
882
+ /<?php\s+echo\s+\$(?!esc_)/gi,
883
+ // Python — Django mark_safe with user input
884
+ /mark_safe\s*\([^)]*(?:request|user_input|params)/gi,
885
+ // Ruby — Rails raw() with user input
886
+ /raw\s*\(\s*(?:params|@\w*user|@\w*input)/gi,
887
+ // Ruby — html_safe on user input
888
+ /(?:params|request)\[.*\]\.html_safe/gi
645
889
  ],
646
890
  fixSuggestion: 'Use standard JSX rendering or DOMPurify before setting HTML content',
647
891
  penalty: 'Security failure',
648
- languages: ['typescript', 'javascript', 'tsx', 'jsx', 'vue']
892
+ languages: ['typescript', 'javascript', 'tsx', 'jsx', 'vue', 'php', 'python', 'ruby']
649
893
  },
650
894
  // Rule 16: Missing Cookie Consent
651
895
  // Fixed Sprint 4: Only flag tracking/PII cookies, not functional preferences (theme, view mode)
@@ -668,11 +912,17 @@ exports.COPPA_RULES = [
668
912
  // Java/Kotlin — Spring ResponseCookie
669
913
  /ResponseCookie\.from\s*\(/gi,
670
914
  // Generic — any language setting cookies with PII field names
671
- /(?:set_cookie|SetCookie|addCookie|add_cookie)\s*\([^)]*(?:user|email|token|session|track|auth|uid|analytics)/gi
915
+ /(?:set_cookie|SetCookie|addCookie|add_cookie)\s*\([^)]*(?:user|email|token|session|track|auth|uid|analytics)/gi,
916
+ // PHP — setcookie() with PII
917
+ /setcookie\s*\(\s*['"][^'"]*(?:user|email|token|track|auth|uid|analytics)[^'"]*['"]/gi,
918
+ // PHP — WordPress set_transient with PII
919
+ /set_transient\s*\(\s*['"][^'"]*(?:user|email|auth)[^'"]*['"]/gi,
920
+ // Ruby — Rails cookies[] with PII
921
+ /cookies\s*\[\s*:(?:user|email|token|session|track|auth|uid|analytics)\s*\]/gi
672
922
  ],
673
923
  fixSuggestion: 'Add a cookie consent banner component before setting tracking or PII cookies',
674
924
  penalty: 'Compliance warning',
675
- languages: ['typescript', 'javascript', 'python', 'go', 'java', 'kotlin']
925
+ languages: ['typescript', 'javascript', 'python', 'go', 'java', 'kotlin', 'php', 'ruby']
676
926
  },
677
927
  // Rule 17: External Links to Non-Child-Safe Sites
678
928
  // Fixed Sprint 4: Exclude privacy/TOS links, mailto, and common safe targets
@@ -718,13 +968,14 @@ exports.COPPA_RULES = [
718
968
  /(?:setUserId|set_user_id)\s*\([^)]*(?:email|\.name|phone)/gi
719
969
  ],
720
970
  fixSuggestion: 'Hash user ID and omit email/name from analytics payload',
721
- penalty: '$51,744 per violation',
971
+ penalty: '$53,088 per violation',
722
972
  languages: ['typescript', 'javascript', 'python', 'go', 'java', 'kotlin']
723
973
  },
724
974
  // Rule 19: School Official Consent Bypass
725
975
  // Fixed Sprint 4: Tightened patterns to match actual auth/registration flows only
726
976
  {
727
977
  id: 'coppa-edu-019',
978
+ is_active: false,
728
979
  name: 'Missing Teacher/School Verification',
729
980
  severity: 'medium',
730
981
  description: 'Teacher accounts using generic email (@gmail.com) bypass "School Official" consent exception',
@@ -741,6 +992,7 @@ exports.COPPA_RULES = [
741
992
  // Rule 20: Default Privacy Settings Public
742
993
  {
743
994
  id: 'coppa-default-020',
995
+ is_active: false,
744
996
  name: 'Default Public Profile Visibility',
745
997
  severity: 'critical',
746
998
  description: 'Default profile visibility must be private. COPPA 2.0 requires privacy by design.',
@@ -752,8 +1004,41 @@ exports.COPPA_RULES = [
752
1004
  /profileVisibility\s*=\s*['"]?(?:public|Public)['"]?/gi
753
1005
  ],
754
1006
  fixSuggestion: 'Change default visibility to "private" or false',
755
- penalty: '$51,744 per violation',
1007
+ penalty: '$53,088 per violation',
756
1008
  languages: ['typescript', 'javascript', 'python', 'swift']
1009
+ },
1010
+ // Rule 21: Targeted Advertising Without Separate Consent (Sprint 17 — COPPA 2025)
1011
+ {
1012
+ id: 'coppa-ads-021',
1013
+ name: 'Targeted Advertising Without Separate Consent',
1014
+ severity: 'critical',
1015
+ description: 'COPPA 2025 requires separate, specific opt-in consent before collecting children\'s PI for targeted advertising. Marketing consent cannot be bundled with general terms acceptance. Ad SDK initialization without a distinct consent flow is a violation.',
1016
+ patterns: [
1017
+ // Google AdMob
1018
+ /(?:import|require).*(?:google-mobile-ads|@react-native-firebase\/admob|react-native-admob)/gi,
1019
+ /(?:GADMobileAds|GADRequest|GADBannerView|GADInterstitial)\.\w+/gi,
1020
+ /MobileAds\.initialize|AdRequest\.Builder|AdView|InterstitialAd\.load/gi,
1021
+ // Meta Audience Network
1022
+ /(?:FBAudienceNetwork|FBAdView|FBInterstitialAd|FBNativeAd)/gi,
1023
+ /(?:import|require).*(?:react-native-fbads|@react-native-community\/fbads)/gi,
1024
+ // Unity Ads
1025
+ /UnityAds\.(?:initialize|show|load)|import\s+UnityAds/gi,
1026
+ // IronSource
1027
+ /IronSource\.(?:init|showRewardedVideo|loadInterstitial)|import\s+IronSource/gi,
1028
+ // AppLovin
1029
+ /AppLovin\.(?:initialize|showAd)|import.*AppLovinSDK/gi,
1030
+ // Chartboost
1031
+ /Chartboost\.(?:start|showInterstitial|cacheInterstitial)/gi,
1032
+ // AdColony
1033
+ /AdColony\.(?:configure|requestInterstitial)/gi,
1034
+ // Vungle
1035
+ /Vungle\.(?:init|playAd|loadAd)/gi,
1036
+ // MoPub
1037
+ /mopub\.(?:loadBanner|loadInterstitial)|MoPubInterstitial/gi
1038
+ ],
1039
+ fixSuggestion: 'Implement a separate, specific opt-in consent flow for advertising before initializing ad SDKs. Marketing consent must NOT be bundled with general terms acceptance. Use age-gated ad experiences or contextual-only advertising for children under 13.',
1040
+ penalty: '$53,088 per violation (COPPA 2025 separate advertising consent requirement)',
1041
+ languages: ['typescript', 'javascript', 'swift', 'kotlin', 'java', 'python']
757
1042
  }
758
1043
  ];
759
1044
  // Ethical Design Rules (Sprint 5 Preview)
@@ -816,6 +1101,7 @@ exports.ETHICAL_RULES = [
816
1101
  // ETHICAL-004: Manipulative Notifications
817
1102
  {
818
1103
  id: 'ETHICAL-004',
1104
+ is_active: false,
819
1105
  name: 'Manipulative Notification Language',
820
1106
  severity: 'medium',
821
1107
  description: 'Notifications using urgency ("Hurry!", "Missing out") manipulate children\'s fear of social exclusion',
@@ -832,6 +1118,7 @@ exports.ETHICAL_RULES = [
832
1118
  // ETHICAL-005: Artificial Scarcity
833
1119
  {
834
1120
  id: 'ETHICAL-005',
1121
+ is_active: false,
835
1122
  name: 'Artificial Scarcity / Countdowns',
836
1123
  severity: 'medium',
837
1124
  description: 'Fake scarcity ("Only 2 left!") and countdown timers pressure children into impulsive decisions',
@@ -856,6 +1143,7 @@ exports.AI_AUDIT_RULES = [
856
1143
  // AI-AUDIT-001: Placeholder Analytics
857
1144
  {
858
1145
  id: 'AI-AUDIT-001',
1146
+ is_active: false, // Sprint 15: Cut to proposed tier — zero GT
859
1147
  name: 'Placeholder Analytics Script',
860
1148
  severity: 'high',
861
1149
  description: 'AI-generated code frequently includes placeholder analytics (UA-XXXXX, G-XXXXXX, fbq) copied from training data. These may activate real tracking without child_directed_treatment flags.',
@@ -890,6 +1178,7 @@ exports.AI_AUDIT_RULES = [
890
1178
  // AI-AUDIT-003: Hallucinated URLs
891
1179
  {
892
1180
  id: 'AI-AUDIT-003',
1181
+ is_active: false, // Sprint 15: Cut to proposed tier — zero GT
893
1182
  name: 'Hallucinated/Placeholder API URLs',
894
1183
  severity: 'medium',
895
1184
  description: 'AI models often generate fake API endpoints (api.example.com, jsonplaceholder, reqres.in) that may be replaced with real endpoints without proper review.',
@@ -905,6 +1194,7 @@ exports.AI_AUDIT_RULES = [
905
1194
  // AI-AUDIT-004: Copy-Paste Tracking Boilerplate
906
1195
  {
907
1196
  id: 'AI-AUDIT-004',
1197
+ is_active: false, // Sprint 15: Cut to proposed tier — zero GT
908
1198
  name: 'Copy-Paste Tracking Boilerplate',
909
1199
  severity: 'high',
910
1200
  description: 'AI assistants reproduce common analytics setup patterns from training data. These often include user identification, event tracking, and session recording without consent flows.',
@@ -944,6 +1234,7 @@ exports.AI_AUDIT_RULES = [
944
1234
  // AI-AUDIT-006: TODO/FIXME Compliance Gaps
945
1235
  {
946
1236
  id: 'AI-AUDIT-006',
1237
+ is_active: false, // Sprint 15: Cut to proposed tier — zero GT
947
1238
  name: 'Unresolved Compliance TODOs',
948
1239
  severity: 'low',
949
1240
  description: 'AI-generated code often includes TODO/FIXME comments for compliance-related features (consent, age verification, privacy policy) that may ship unimplemented.',
@@ -999,6 +1290,7 @@ exports.AU_SBD_RULES = [
999
1290
  // AU-SBD-003: Unrestricted Direct Messaging
1000
1291
  {
1001
1292
  id: 'AU-SBD-003',
1293
+ is_active: false,
1002
1294
  name: 'Unrestricted Direct Messaging for Minors',
1003
1295
  severity: 'critical',
1004
1296
  description: 'Direct messaging or chat functionality without safety controls (contact restrictions, message filtering, or parental oversight). The AU Online Safety Act requires platforms to take reasonable steps to prevent child exploitation in private communications.',
@@ -1015,6 +1307,7 @@ exports.AU_SBD_RULES = [
1015
1307
  // AU-SBD-004: Algorithmic Feeds Without Safety Guardrails
1016
1308
  {
1017
1309
  id: 'AU-SBD-004',
1310
+ is_active: false, // Sprint 15: Cut to proposed tier — zero GT
1018
1311
  name: 'Recommendation Algorithm Without Safety Guardrails',
1019
1312
  severity: 'high',
1020
1313
  description: 'Content recommendation or feed algorithms detected without safety filtering, content classification, or age-appropriate guardrails. AU SbD requires platforms to assess and mitigate algorithmic harms, particularly for young users.',
@@ -1032,6 +1325,7 @@ exports.AU_SBD_RULES = [
1032
1325
  // AU-SBD-005: Missing Digital Wellbeing / Screen Time Controls
1033
1326
  {
1034
1327
  id: 'AU-SBD-005',
1328
+ is_active: false, // Sprint 16 W1: 18.2% precision (2/11 TP). All 9 FPs are media player autoplay (jellyfin). Pattern too broad — rebuild needed.
1035
1329
  name: 'Engagement Features Without Time Awareness',
1036
1330
  severity: 'medium',
1037
1331
  description: 'High-engagement features (autoplay, continuous scrolling, notifications) detected without corresponding digital wellbeing controls (screen time limits, break reminders, usage dashboards). AU SbD encourages platforms to build in digital wellbeing tools.',
@@ -1049,6 +1343,7 @@ exports.AU_SBD_RULES = [
1049
1343
  // AU-SBD-006: Location Sharing Without Explicit Opt-In
1050
1344
  {
1051
1345
  id: 'AU-SBD-006',
1346
+ is_active: false,
1052
1347
  name: 'Location Data Without Explicit Consent',
1053
1348
  severity: 'critical',
1054
1349
  description: 'Location data collection or sharing enabled without explicit, informed opt-in. AU SbD and the Privacy Act 1988 require data minimization, especially for children\'s geolocation data — location should never be collected by default.',
@@ -1110,6 +1405,48 @@ function parseHaloignore(content) {
1110
1405
  }
1111
1406
  return config;
1112
1407
  }
1408
+ /**
1409
+ * Sprint 10: Check if a file path is in a vendored/third-party library directory.
1410
+ * Vendor files are auto-suppressed to eliminate false positives from code the project doesn't control.
1411
+ */
1412
+ function isVendorPath(filePath) {
1413
+ const normalized = filePath.replace(/\\/g, '/');
1414
+ return /(^|\/)node_modules\//.test(normalized) ||
1415
+ /(^|\/)vendor\//.test(normalized) ||
1416
+ /(^|\/)bower_components\//.test(normalized) ||
1417
+ /(^|\/)third[_-]?party\//.test(normalized) ||
1418
+ /(^|\/)\.bundle\//.test(normalized) ||
1419
+ /(^|\/)Pods\//.test(normalized) ||
1420
+ /(^|\/)external\//.test(normalized) ||
1421
+ /(^|\/)deps\//.test(normalized) ||
1422
+ /(^|\/)\.yarn\//.test(normalized) ||
1423
+ /(^|\/)\.pnpm\//.test(normalized) ||
1424
+ // Minified files are almost always vendored/built
1425
+ /[.\-]min\.(js|css)$/.test(normalized) ||
1426
+ /\.bundle\.js$/.test(normalized) ||
1427
+ // Well-known vendored library directories (catches lib/google2-service/, lib/aws-sdk/, etc.)
1428
+ /(^|\/)lib\/(google[^/]*|aws[^/]*|yui[^/]*|php[^/]*|jquery[^/]*|bootstrap[^/]*|tinymce[^/]*|h5p[^/]*|firebase[^/]*|simplepie[^/]*|tcpdf[^/]*|guzzle[^/]*|psr[^/]*|font-?awesome[^/]*)\//i.test(normalized) ||
1429
+ // H5P vendored libraries (stored under h5p/h5plib/, not lib/)
1430
+ /(^|\/)h5plib\//.test(normalized);
1431
+ }
1432
+ /**
1433
+ * Sprint 11a: Check if a file path is in a documentation generator output directory.
1434
+ * Doc generator templates and output contain external links, code examples, etc. that are
1435
+ * developer-facing, not child-facing content. Flagging these is a false positive.
1436
+ */
1437
+ function isDocGeneratorPath(filePath) {
1438
+ const normalized = filePath.replace(/\\/g, '/');
1439
+ return /(^|\/)(?:jsdoc|typedoc|apidoc|javadoc|doxygen|sphinx|_build|_static)(?:\/|\.)/i.test(normalized) ||
1440
+ // Documentation template files
1441
+ /(?:^|\/)(?:jsdoc|typedoc|apidoc)\.(?:html|hbs|tmpl|ejs)$/i.test(normalized) ||
1442
+ // Generated API docs
1443
+ /(^|\/)(?:docs?\/(?:api|generated|reference|build))\//i.test(normalized) ||
1444
+ // Sphinx build output
1445
+ /(^|\/)_build\/html\//i.test(normalized) ||
1446
+ // Common doc generator config files with template content
1447
+ /(?:^|\/)\.jsdoc\.(?:json|js)$/i.test(normalized) ||
1448
+ /(?:^|\/)typedoc\.json$/i.test(normalized);
1449
+ }
1113
1450
  /**
1114
1451
  * Check if a file should be ignored based on .haloignore config
1115
1452
  */
@@ -1194,6 +1531,35 @@ class HaloEngine {
1194
1531
  this.rules = [...this.rules, ...exports.AU_SBD_RULES];
1195
1532
  }
1196
1533
  }
1534
+ // Sprint 15: Filter out disabled rules.
1535
+ // Static list ensures rules are disabled regardless of source (API, cache, bundled JSON, hardcoded).
1536
+ // is_active flag handles hardcoded rules; DISABLED_RULE_IDS handles all sources.
1537
+ // Sprint 15: Zero-GT rule actions
1538
+ // DISABLE: zero GT entries, cannot validate precision
1539
+ // CUT (to proposed tier): zero GT entries, pattern too broad for production
1540
+ const DISABLED_RULE_IDS = new Set([
1541
+ 'coppa-bio-012', // 0% precision, all FP — rebuild needed
1542
+ // coppa-notif-013 removed — rebuilt Sprint 18 with push-only patterns
1543
+ 'coppa-sec-010', // Sprint 16 W1: 100% FP (0/3 TP) — all hits wrong
1544
+ 'coppa-ugc-014', // Sprint 16 W1: 100% FP (0/3 TP) — all hits wrong
1545
+ 'coppa-edu-019', // Zero GT — teacher registration patterns too narrow to validate
1546
+ 'coppa-default-020', // Zero GT — overlaps with AU-SBD-001 default public profiles
1547
+ 'ETHICAL-004', // Zero GT — manipulative notification language too broad
1548
+ 'ETHICAL-005', // Zero GT — artificial scarcity patterns too broad
1549
+ 'AU-SBD-003', // Zero GT — DM detection patterns too broad
1550
+ 'AU-SBD-005', // Sprint 16 W1: 18.2% precision — autoplay pattern fires on media player APIs
1551
+ 'AU-SBD-006', // Zero GT — location sharing patterns too broad
1552
+ // CUT to proposed tier (zero GT, pattern too broad for production)
1553
+ 'AI-AUDIT-001', // Placeholder analytics — low real-world signal
1554
+ 'AI-AUDIT-003', // Hallucinated URLs — low real-world signal
1555
+ 'AI-AUDIT-004', // Copy-paste tracking boilerplate — too broad
1556
+ 'AI-AUDIT-006', // TODO/FIXME compliance — noise in real codebases
1557
+ 'AU-SBD-004', // Algorithmic feeds — pattern too broad for production
1558
+ // Sprint 17 Day 0: Zero-GT rules validated against 5 repos — patterns don't match real-world code
1559
+ 'ut-sb142-003', // Default DM access — patterns use naming conventions no real app uses (0 hits across Moodle, Discourse, Rocket.Chat, Element, Mastodon)
1560
+ 'ut-sb142-004', // Missing parental tools — 2 hits across 5 repos, both FP. API/bundled pattern mismatch. Needs rebuild
1561
+ ]);
1562
+ this.rules = this.rules.filter(r => r.is_active !== false && !DISABLED_RULE_IDS.has(r.id));
1197
1563
  if (config.severityFilter) {
1198
1564
  this.rules = this.rules.filter(r => config.severityFilter.includes(r.severity));
1199
1565
  }
@@ -1307,11 +1673,9 @@ class HaloEngine {
1307
1673
  v.astConfidence = 0;
1308
1674
  }
1309
1675
  }
1310
- // Sprint 8: Apply framework overrides
1311
- if (this.config.framework) {
1312
- const result = (0, frameworks_1.applyFrameworkOverrides)(violations, this.config.framework);
1313
- violations = result.violations;
1314
- }
1676
+ // Sprint 10: Framework overrides now applied in scanFile() for ALL file types.
1677
+ // No longer needed here — scanFile() already filtered/downgraded regex violations.
1678
+ // AST-added violations (e.g. signInWithPopup → auth-001) are not in any framework profile.
1315
1679
  // Sprint 8: ContextAnalyzer — compute confidence scores
1316
1680
  const violationInputs = violations.map(v => ({
1317
1681
  ruleId: v.ruleId,
@@ -1350,26 +1714,65 @@ class HaloEngine {
1350
1714
  if (ignoreConfig && shouldIgnoreFile(filePath, ignoreConfig)) {
1351
1715
  return [];
1352
1716
  }
1353
- const violations = [];
1717
+ // Sprint 10: Skip vendored/third-party library files entirely
1718
+ // These produce massive false positives (84% FP rate on Moodle — all from lib/, vendor/ paths)
1719
+ if (isVendorPath(filePath)) {
1720
+ return [];
1721
+ }
1722
+ // Sprint 11a: Skip documentation generator output files
1723
+ // JSDoc templates, Sphinx output, TypeDoc pages — developer tools, not child-facing content
1724
+ if (isDocGeneratorPath(filePath)) {
1725
+ return [];
1726
+ }
1727
+ let violations = [];
1354
1728
  const lines = content.split('\n');
1355
- // Sprint 8: Test file detection for regex scanner (coppa-sec-010 FP fix)
1729
+ // Sprint 13a: Consolidated file classification (Pre-filter A+)
1730
+ // All heuristics are now in classifyFile() for consistency and future Option C upgrade
1356
1731
  const normalizedPath = filePath.replace(/\\/g, '/');
1357
- const isTestFile = /\.(test|spec)\.(ts|tsx|js|jsx|py|rb|java|go)$/i.test(normalizedPath) ||
1358
- /(^|\/)__tests__\//.test(normalizedPath) ||
1359
- /(^|\/)test\//.test(normalizedPath) ||
1360
- /(^|\/)tests\//.test(normalizedPath) ||
1361
- /(^|\/)spec\//.test(normalizedPath) ||
1362
- /(^|\/)fixtures\//.test(normalizedPath) ||
1363
- /\.(stories|story)\.(ts|tsx|js|jsx)$/i.test(normalizedPath) ||
1364
- /(^|\/)cypress\//.test(normalizedPath) ||
1365
- /(^|\/)e2e\//.test(normalizedPath) ||
1366
- /jest\.config|vitest\.config|playwright\.config/i.test(normalizedPath);
1367
- // Rules that commonly false-positive in test/fixture files
1732
+ const classification = classifyFile(filePath, content.substring(0, 3000));
1733
+ // Sprint 13a: Skip files that should never be scanned
1734
+ // (Django migrations, build output, type definitions, CI configs)
1735
+ if (classification.shouldSkip) {
1736
+ return [];
1737
+ }
1738
+ // Rules that commonly false-positive in test/fixture/mock files
1368
1739
  const TEST_FP_RULES = new Set([
1369
1740
  'coppa-sec-010', // Weak passwords in test fixtures
1370
1741
  'coppa-tracking-003', // Analytics snippets in test mocks
1371
1742
  'coppa-auth-001', // Auth patterns in test helpers
1372
1743
  'coppa-sec-015', // XSS patterns in security test cases
1744
+ 'coppa-sec-006', // Sprint 11a: HTTP URLs in test config (e.g., http://example-storage.com in envs/test.py)
1745
+ ]);
1746
+ // Rules that should be suppressed in consent/compliance implementation files
1747
+ // These rules flag patterns that are REQUIRED in consent implementations
1748
+ const CONSENT_SUPPRESSED_RULES = new Set([
1749
+ 'coppa-cookies-016', // Cookie consent banners MUST set cookies to track consent state
1750
+ 'coppa-tracking-003', // Consent management may reference tracking to gate it
1751
+ 'coppa-data-002', // Consent flows may reference PII fields to declare collection scope
1752
+ ]);
1753
+ // Rules that FP in admin/instructor code — these patterns exist for managing users, not collecting child data
1754
+ const ADMIN_FP_RULES = new Set([
1755
+ 'coppa-flow-009', // Contact collection: admin reading existing user emails is not child contact flow
1756
+ 'coppa-data-002', // PII in URLs: admin user lookup endpoints are internal tools
1757
+ 'coppa-ui-008', // Registration forms: admin user management is not child registration
1758
+ 'coppa-sec-006', // Sprint 16: HTTP URLs in admin/instructor views are internal tooling, not child-facing
1759
+ ]);
1760
+ // ── Graduated Heuristics (Sprint 13b) ──────────────────────────────────
1761
+ // Auto-promoted from AI Review Board via the graduation pipeline.
1762
+ // Each pattern was dismissed consistently by the AI reviewer and passed
1763
+ // MVP validation criteria (min dismissals, min confidence, zero false confirmations).
1764
+ // These replace AI review calls with deterministic checks: zero cost, instant execution.
1765
+ // Graduated pattern: admin-path
1766
+ // 193 consistent dismissals | avg confidence 9.0/10 | 0 false confirmations
1767
+ // AI reviewer cost per check: ~$0.014 → now $0.00
1768
+ const GRADUATED_ADMIN_RULES = new Set([
1769
+ 'ut-sb142-001', // UT SB-142 age verification: admin panels are not child-facing
1770
+ 'ut-sb142-002', // UT SB-142 parental consent: admin interfaces require staff auth
1771
+ ]);
1772
+ // Graduated pattern: test-file
1773
+ // 27 consistent dismissals | avg confidence 9.0/10 | 0 false confirmations
1774
+ const GRADUATED_TEST_RULES = new Set([
1775
+ 'ut-sb142-001', // UT SB-142 age verification: test utilities are not production child-facing code
1373
1776
  ]);
1374
1777
  // Parse suppression comments
1375
1778
  const suppressions = parseSuppressions(content);
@@ -1381,12 +1784,52 @@ class HaloEngine {
1381
1784
  }
1382
1785
  }
1383
1786
  for (const rule of this.rules) {
1384
- // Sprint 8: Skip rules that commonly FP in test files
1385
- if (isTestFile && TEST_FP_RULES.has(rule.id)) {
1787
+ // Sprint 10: Skip rules that don't target this file's language
1788
+ if (rule.languages && rule.languages.length > 0 && classification.language !== 'unknown') {
1789
+ if (!rule.languages.includes(classification.language)) {
1790
+ continue;
1791
+ }
1792
+ }
1793
+ // Sprint 8+13a: Skip rules that commonly FP in test/fixture/mock/factory files
1794
+ if ((classification.isTest || classification.isMockOrFactory || classification.isFixtureOrSeed) && TEST_FP_RULES.has(rule.id)) {
1795
+ continue;
1796
+ }
1797
+ // Sprint 13a: Skip ALL rules in Storybook stories (UI demos, not production code)
1798
+ if (classification.isStorybook) {
1799
+ continue;
1800
+ }
1801
+ // Sprint 10b: Skip rules that FP in consent/compliance implementation files
1802
+ // Consent forms MUST set cookies, reference tracking, and handle PII — that's the solution, not the problem
1803
+ if (classification.isConsent && CONSENT_SUPPRESSED_RULES.has(rule.id)) {
1804
+ continue;
1805
+ }
1806
+ // Sprint 11a: Skip rules that FP in admin/instructor backend code
1807
+ // Admin functions managing existing user data are not child-facing contact collection flows
1808
+ if (classification.isAdmin && ADMIN_FP_RULES.has(rule.id)) {
1809
+ continue;
1810
+ }
1811
+ // Sprint 13b Graduated: admin-path — admin files are not child-facing
1812
+ // (Promoted from AI Review Board: 193 dismissals, confidence 9.0)
1813
+ if (classification.isAdmin && GRADUATED_ADMIN_RULES.has(rule.id)) {
1814
+ continue;
1815
+ }
1816
+ // Sprint 15: AU-SBD-002 fix — skip in admin/vendor code (85% FP rate from these contexts)
1817
+ if ((classification.isAdmin || classification.isVendor) && rule.id === 'AU-SBD-002') {
1818
+ continue;
1819
+ }
1820
+ // Sprint 13b Graduated: test-file — test/fixture files are not production code
1821
+ // (Promoted from AI Review Board: 27 dismissals, confidence 9.0)
1822
+ if ((classification.isTest || classification.isMockOrFactory || classification.isFixtureOrSeed) && GRADUATED_TEST_RULES.has(rule.id)) {
1386
1823
  continue;
1387
1824
  }
1388
1825
  // Special handling for coppa-retention-005: skip if schema has retention fields
1389
1826
  if (rule.id === 'coppa-retention-005') {
1827
+ // Sprint 11a: Skip Python models annotated with no_pii docstrings
1828
+ // OpenEdX convention: `.. no_pii:` in class docstring means model contains no PII
1829
+ // These models have User FKs but only store non-PII data (e.g., calendar sync preferences)
1830
+ if (classification.language === 'python' && /(?:\.\.\s*no_pii\s*:|#\s*no[_-]?pii\b|no_pii\s*=\s*True)/i.test(content)) {
1831
+ continue;
1832
+ }
1390
1833
  // Check if the content has retention-related fields
1391
1834
  const hasRetention = /deletedAt|deleted_at|expires|TTL|retention|paranoid|expiration/i.test(content);
1392
1835
  if (!hasRetention) {
@@ -1440,6 +1883,82 @@ class HaloEngine {
1440
1883
  if (isOwnDomain)
1441
1884
  continue;
1442
1885
  }
1886
+ // Sprint 11a: For coppa-ext-017: skip IE conditional comments
1887
+ // <!--[if lte IE 9]> ... <![endif]--> are deprecated browser banners, not child-facing links
1888
+ // Seen in: OpenEdX templates with Chrome/Firefox download links for IE users
1889
+ if (rule.id === 'coppa-ext-017') {
1890
+ // Check if we're inside an IE conditional comment block
1891
+ const beforeMatch = content.substring(Math.max(0, match.index - 500), match.index);
1892
+ const afterMatch = content.substring(match.index, Math.min(content.length, match.index + 500));
1893
+ if (/<!--\s*\[if\s+(?:lt|lte|gt|gte|!)?\s*IE/i.test(beforeMatch) && /\[endif\]\s*-->/i.test(afterMatch)) {
1894
+ continue;
1895
+ }
1896
+ // Also skip if the line itself contains the IE conditional pattern
1897
+ if (/<!--\s*\[if\s+(?:lt|lte|gt|gte|!)?\s*IE/i.test(lineContent)) {
1898
+ continue;
1899
+ }
1900
+ }
1901
+ // Sprint 10+16: For coppa-sec-006: skip reserved/example/documentation/standards domains
1902
+ // These are IANA-reserved, standards bodies, or universally used in documentation and are never real endpoints
1903
+ // Require http:// before the domain to avoid matching domains in email addresses etc.
1904
+ if (rule.id === 'coppa-sec-006') {
1905
+ const checkText = (match[0] + ' ' + lineContent).toLowerCase();
1906
+ if (/http:\/\/(www\.)?(example\.(com|org|net)|localhost(:\d|\/|['"\s]|$)|127\.0\.0\.1|0\.0\.0\.0|\[::1\]|httpbin\.org|jsonplaceholder\.typicode\.com|testserver(\.com)?[\/\s'"]|imsglobal\.org|flickr\.com|w3\.org)/.test(checkText)) {
1907
+ continue;
1908
+ }
1909
+ // Sprint 16: Skip devstack/Docker service URLs (development-only, never production)
1910
+ if (/devstack/.test(checkText)) {
1911
+ continue;
1912
+ }
1913
+ // Sprint 16: Skip XML schema/namespace URLs (*.xsd, /xmlns/, /schema/)
1914
+ // These are namespace declarations, not API endpoints
1915
+ if (/\.xsd['"\s,)]|\/xmlns\/|\/schema\//.test(checkText)) {
1916
+ continue;
1917
+ }
1918
+ }
1919
+ // Sprint 10: For coppa-sec-015 (XSS): skip innerHTML assignments that are already sanitized
1920
+ // Y.Escape.html(), DOMPurify.sanitize(), etc. show the developer IS handling XSS
1921
+ if (rule.id === 'coppa-sec-015') {
1922
+ if (/(?:escape\.html|dompurify|sanitize|purify)\s*\(/i.test(lineContent)) {
1923
+ continue;
1924
+ }
1925
+ }
1926
+ // Sprint 10: For coppa-ui-008: skip admin tool registration (LTI cartridge, Brickfield, etc.)
1927
+ // These are admin/developer-facing forms, not child-facing registration
1928
+ if (rule.id === 'coppa-ui-008') {
1929
+ if (/cartridge[_-]?registration|brickfield|registersetting|tool_configure/i.test(lineContent) ||
1930
+ /cartridge[_-]?registration|brickfield|registersetting/i.test(normalizedPath)) {
1931
+ continue;
1932
+ }
1933
+ }
1934
+ // Sprint 10b: For coppa-cookies-016: skip consent implementations
1935
+ // Files/code implementing cookie consent are the solution, not the problem
1936
+ if (rule.id === 'coppa-cookies-016') {
1937
+ // File path patterns: cookie-consent.js, gdpr-banner.js, etc.
1938
+ if (/cookie[_-]?(consent|law|notice|banner|policy|popup|gdpr|preferences)/i.test(normalizedPath) ||
1939
+ /(?:consent|gdpr|ccpa|privacy)[_-]?(?:banner|popup|modal|notice|manager)/i.test(normalizedPath)) {
1940
+ continue;
1941
+ }
1942
+ // Line-level: function/variable names showing consent management intent
1943
+ if (/(?:handleConsent|acceptCookies|declineCookies|cookieBanner|consentManager|cookiePreferences|saveCookiePreferences|showCookieNotice|getCookieConsent|setCookieConsent)\s*[=(]/i.test(lineContent) ||
1944
+ /(?:accept|decline|preferences|banner|consent)\s*[=:]/i.test(lineContent) && /cookie/i.test(lineContent)) {
1945
+ continue;
1946
+ }
1947
+ // Import-level: known consent management libraries
1948
+ if (/(?:require|import).*(?:cookieconsent|react-cookie-consent|onetrust|cookiebot|osano|cookie-notice|gdpr-cookie)/i.test(content.substring(0, 2000))) {
1949
+ continue;
1950
+ }
1951
+ // Sprint 11a: Skip cookie DELETION patterns — setting expires to past or max-age=0/-1 is cleanup, not tracking
1952
+ // Seen in: Moodle submit.js — code that removes cookies flagged as if setting them
1953
+ if (/max[_-]?age\s*[=:]\s*['"]?\s*(-\d+|0)\b/i.test(lineContent) ||
1954
+ /expires\s*[=:]\s*['"]?\s*(?:Thu,\s*01\s+Jan\s+1970|new\s+Date\s*\(\s*0\s*\))/i.test(lineContent) ||
1955
+ /new\s+Date\s*\(\s*0\s*\)/.test(lineContent) && /expires/i.test(lineContent) ||
1956
+ /=\s*['"]?\s*deleted\b/i.test(lineContent) ||
1957
+ /(?:delete|remove|clear|expire|destroy)[_-]?cookie/i.test(lineContent) ||
1958
+ /\.cookie\s*=\s*['"][^'"]*;\s*expires\s*=\s*['"]?\s*Thu,\s*01/i.test(lineContent)) {
1959
+ continue;
1960
+ }
1961
+ }
1443
1962
  // Check if this violation already exists (avoid duplicates)
1444
1963
  const exists = violations.some(v => v.ruleId === rule.id &&
1445
1964
  v.line === lineNumber &&
@@ -1453,6 +1972,14 @@ class HaloEngine {
1453
1972
  if (suppressed) {
1454
1973
  suppressionComment = suppressions.get(lineNumber);
1455
1974
  }
1975
+ // Sprint 11b: Extract surrounding code context (5 lines before + 5 after)
1976
+ const contextStart = Math.max(0, lineNumber - 6); // lineNumber is 1-indexed
1977
+ const contextEnd = Math.min(lines.length, lineNumber + 5);
1978
+ const surroundingLines = lines.slice(contextStart, contextEnd).map((l, i) => {
1979
+ const ln = contextStart + i + 1;
1980
+ const marker = ln === lineNumber ? '>>>' : ' ';
1981
+ return `${marker} ${ln}: ${l}`;
1982
+ });
1456
1983
  violations.push({
1457
1984
  ruleId: rule.id,
1458
1985
  ruleName: rule.name,
@@ -1471,11 +1998,43 @@ class HaloEngine {
1471
1998
  matchType: 'regex',
1472
1999
  fixability: getRemediation(rule.id).fixability,
1473
2000
  remediation: getRemediation(rule.id),
2001
+ // Sprint 11b: Enriched context for AI Review Board
2002
+ surroundingCode: surroundingLines.join('\n'),
2003
+ fileMetadata: {
2004
+ language: classification.language,
2005
+ isVendor: classification.isVendor,
2006
+ isTest: classification.isTest,
2007
+ isAdmin: classification.isAdmin,
2008
+ isConsent: classification.isConsent,
2009
+ isDocGenerator: classification.isDocGenerator,
2010
+ detectedFramework: this.config.framework,
2011
+ // Sprint 13a: Extended classification data
2012
+ isMock: classification.isMockOrFactory,
2013
+ isFixture: classification.isFixtureOrSeed,
2014
+ isCIConfig: classification.isCIConfig,
2015
+ isBuildOutput: classification.isBuildOutput,
2016
+ isTypeDefinition: classification.isTypeDefinition,
2017
+ isStorybook: classification.isStorybook,
2018
+ },
1474
2019
  });
1475
2020
  }
1476
2021
  }
1477
2022
  }
1478
2023
  }
2024
+ // Sprint 10: Apply framework overrides to ALL file types (Python, PHP, HTML, etc.)
2025
+ // Previously this only ran inside scanFileWithAST() for JS/TS files.
2026
+ if (this.config.framework) {
2027
+ const result = (0, frameworks_1.applyFrameworkOverrides)(violations, this.config.framework);
2028
+ violations = result.violations;
2029
+ }
2030
+ // Sprint 12b: Dedup AI-GOVERNANCE-002 / AI-RISK-003 overlap
2031
+ // If both fire on same file+line, suppress AI-RISK-003 (AI-GOVERNANCE-002 subsumes it)
2032
+ const govViolations = new Set(violations
2033
+ .filter(v => v.ruleId === 'AI-GOVERNANCE-002')
2034
+ .map(v => `${v.filePath}:${v.line}`));
2035
+ if (govViolations.size > 0) {
2036
+ violations = violations.filter(v => !(v.ruleId === 'AI-RISK-003' && govViolations.has(`${v.filePath}:${v.line}`)));
2037
+ }
1479
2038
  // Filter suppressed if configured
1480
2039
  if (this.config.suppressions?.enabled !== false && !this.config.includeSuppressed) {
1481
2040
  const unsuppressed = violations.filter(v => !v.suppressed);