calmlens 0.18.2 → 0.19.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.
data/sig/calmlens.rbs CHANGED
@@ -109,10 +109,14 @@ module CalmLens
109
109
  RETRY_MAX_DELAY: Integer
110
110
  RETRY_AFTER_MAX: Integer
111
111
  DEFAULT_MAX_RESPONSE_BYTES: Integer
112
+ DEFAULT_PART_SIZE_BYTES: Integer
113
+ MIN_PART_SIZE_BYTES: Integer
114
+ MAX_PART_SIZE_BYTES: Integer
112
115
  ENSURE_LENSES_MAX_PER_REQUEST: Integer
113
116
  attr_reader assets: Assets
114
117
  attr_reader folders: Folders
115
118
  attr_reader hash_lists: HashLists
119
+ attr_reader policies: Policies
116
120
  attr_reader text: Text
117
121
  attr_reader upload_tokens: UploadTokens
118
122
  attr_reader events: Events
@@ -122,7 +126,7 @@ module CalmLens
122
126
  attr_reader cdn_base_url: String
123
127
  def initialize: (api_key: String, project_id: String, ?base_url: String, ?cdn_base_url: String, ?timeout: number,
124
128
  ?open_timeout: number, ?upload_timeout: number?, ?max_retries: Integer, ?retry_base_delay: number,
125
- ?max_response_bytes: Integer, ?transport: Transport::_Transport?) -> void
129
+ ?max_response_bytes: Integer, ?part_size_bytes: Integer, ?transport: Transport::_Transport?) -> void
126
130
  def close: () -> nil
127
131
  def ensure_lenses: (ensure_lenses_options options) -> Array[lens]
128
132
  def ensure_lens: (ensure_lens_options options) -> lens
@@ -200,6 +204,17 @@ module CalmLens
200
204
  def remove_entry: (hash_list_entry_id_options options) -> nil
201
205
  end
202
206
 
207
+ class Policies < Resource
208
+ def list: (?list_policies_options options) -> policy_list
209
+ def retrieve: (policy_id_options options) -> policy
210
+ def create: (create_policy_options options) -> policy
211
+ def update: (update_policy_options options) -> policy
212
+ def apply_preset: (apply_policy_preset_options options) -> policy
213
+ def set_automatic_updates: (set_policy_automatic_updates_options options) -> policy
214
+ def delete: (policy_id_options options) -> nil
215
+ def assign: (assign_policy_options options) -> policy
216
+ end
217
+
203
218
  class UploadTokens < Resource
204
219
  TOKEN_PREFIX: String
205
220
  SIGNING_CONTEXT: String
@@ -338,7 +353,7 @@ module CalmLens
338
353
  def initialize: (?project_id: String, ?api_key: String, ?base_url: String, ?cdn_base_url: String, ?now: _Clock, ?create_id: _IdFactory,
339
354
  ?moderate: _Moderate?, ?timeout: number,
340
355
  ?open_timeout: number, ?upload_timeout: number?, ?max_retries: Integer, ?retry_base_delay: number,
341
- ?max_response_bytes: Integer) -> void
356
+ ?max_response_bytes: Integer, ?part_size_bytes: Integer) -> void
342
357
  # Replaces the mock API key's grants for later requests; the key stays the same.
343
358
  def api_key_grants=: (Array[Hash[Symbol | String, untyped]] grants) -> Array[Hash[Symbol | String, untyped]]
344
359
  # Fails the next request (or the next one `operation` sends) with `transport-error`, `timeout`, an
@@ -398,5 +413,45 @@ module CalmLens
398
413
  module MockClassification
399
414
  CATEGORIES: Array[String]
400
415
  QUALIFIERS: Hash[String, Array[String]]
416
+ # Qualifiers the input type sets, which a TestInputs scenario cannot list.
417
+ INPUT_QUALIFIERS: Array[String]
418
+ end
419
+
420
+ # Signed test inputs: text codes, test images and media phrases signed with a project's secret
421
+ # test input key from the portal.
422
+ module TestInputs
423
+ type action = "allow" | "limit" | "age-restrict" | "block"
424
+ type region = "DE" | "AT" | "FR" | "GB" | "EU" | "AU" | "BR"
425
+ type regional_action = { "region" => region, "action" => "limit" | "age-restrict" | "block" }
426
+ # One qualifying instance; set an action on every instance or on none.
427
+ type test_instance = { "category" => classification_category, ?"score" => number, ?"action" => action, "qualifiers" => Array[String] }
428
+ # A scenario with String keys; Symbol and snake_case keys are accepted as a plain Hash.
429
+ type scenario = { ?"regionalActions" => Array[regional_action], ?"unassessedCategories" => Array[classification_category],
430
+ "instances" => Array[test_instance] }
431
+ type signal = classification_category | "sexual.involving-minors" | "harassment.insult" | "sexual.adult"
432
+ | "hate.regional-symbol" | "violence.graphic"
433
+ type media_signal = classification_category | "violence.graphic"
434
+ type input = scenario | Hash[String | Symbol, untyped] | signal
435
+
436
+ # A painted test image: width by height pixels of 8-bit RGBA in the binary String `rgba`, and
437
+ # the same image as binary PNG bytes in `png`.
438
+ class Image < Struct[untyped]
439
+ attr_accessor width: Integer
440
+ attr_accessor height: Integer
441
+ attr_accessor rgba: String
442
+ attr_accessor png: String
443
+ def initialize: (width: Integer, height: Integer, rgba: String, png: String) -> void
444
+ end
445
+
446
+ REGIONS: Array[region]
447
+ SIGNALS: Array[signal]
448
+ MEDIA_SIGNALS: Array[media_signal]
449
+ # clean, limited, adult_image, adult_text, regional_delivery, germany and mixed (deeply frozen).
450
+ PRESETS: Hash[Symbol, scenario]
451
+
452
+ def self.for_signal: (signal signal, ?("text" | "image") kind) -> scenario
453
+ def self.generate_text_code: (input input, String test_input_key) -> String
454
+ def self.paint_image: (input input, String test_input_key) -> Image
455
+ def self.generate_media_code: (media_signal signal, String test_input_key) -> String
401
456
  end
402
457
  end
data/sig/error_codes.rbs CHANGED
@@ -7,6 +7,8 @@ module CalmLens
7
7
  INVALID_VIEWER_REGION: String
8
8
  VIEWER_REGION_REQUIRED: String
9
9
  REGIONAL_EVIDENCE_REQUIRED: String
10
+ SIGNED_URL_EXPIRED: String
11
+ SIGNED_URL_INVALID: String
10
12
  INVALID_TYPE: String
11
13
  TOO_SMALL: String
12
14
  TOO_BIG: String
@@ -34,9 +36,11 @@ module CalmLens
34
36
  API_KEY_NOT_ACCEPTED: String
35
37
  PERMISSION_DENIED: String
36
38
  FILE_TOO_LARGE: String
37
- MULTIPART_IMAGE_TOO_SMALL: String
38
39
  MULTIPART_IMAGE_TOO_LARGE: String
39
40
  IMAGE_EXCEEDED_MAX_DIMENSIONS: String
41
+ IMAGE_INVALID_INPUT: String
42
+ IMAGE_UNSUPPORTED_FORMAT: String
43
+ IMAGE_INPUT_LIMIT: String
40
44
  UPLOAD_LIMIT_EXCEEDED: String
41
45
  SPEND_LIMIT_REACHED: String
42
46
  INVALID_UPLOAD_MIME_TYPE: String
@@ -77,6 +81,15 @@ module CalmLens
77
81
  HASH_LIST_LIMIT_REACHED: String
78
82
  HASH_LIST_ENTRY_LIMIT_REACHED: String
79
83
  HASH_LIST_ASSET_NOT_HASHED: String
84
+ POLICY_NOT_FOUND: String
85
+ POLICY_REVISION_CONFLICT: String
86
+ POLICY_PROJECTS_CHANGED: String
87
+ POLICY_IN_USE: String
88
+ POLICY_USED_BY_ANOTHER_PROJECT: String
89
+ POLICY_NO_PRESET: String
90
+ POLICY_PRESET_VERSION_CONFLICT: String
91
+ POLICY_PRESET_UPDATE_UNAVAILABLE: String
92
+ POLICY_AUTOMATIC_UPDATES_UNAVAILABLE: String
80
93
  UPLOAD_PRESET_NOT_FOUND: String
81
94
  UPLOAD_PRESET_DISABLED: String
82
95
  UPLOAD_PRESET_TOKEN_REQUIRED: String
data/sig/types.rbs CHANGED
@@ -60,8 +60,8 @@ module CalmLens
60
60
  # Earlier posts to send, oldest first; only the posts CalmLens uses are sent.
61
61
  type moderation_context_input = { items: Array[moderation_context_item_input] }
62
62
 
63
- # ProcessingFeatures.ts: optional analyses to run, keyed by feature. Every feature is off unless
64
- # `enabled` is true.
63
+ # ProcessingFeatures.ts: optional analyses to run, keyed by feature. Link detection runs unless
64
+ # `enabled` is false; every other feature is off unless `enabled` is true.
65
65
  type processing_features_input = {
66
66
  ?ocr: { enabled: bool }?,
67
67
  ?person_detection: { enabled: bool }?,
@@ -139,6 +139,8 @@ module CalmLens
139
139
  ?"personDetection" => person_detection_result,
140
140
  ?"faceDetection" => face_detection_result,
141
141
  ?"linkDetection" => link_detection_result,
142
+ # Preview. Where the asset's links lead; present when the project's policy acts on links.
143
+ ?"linkSafety" => link_safety_result,
142
144
  ?"aiGeneratedDetection" => ai_generated_detection_result,
143
145
  # Where OCR found text, and personal information within it; redaction transforms blur these regions.
144
146
  ?"textRegions" => Array[text_region],
@@ -165,6 +167,9 @@ module CalmLens
165
167
  "flaggedCategories" => Array[String], "regions" => Array[regional_decision],
166
168
  ?"explanationComplete" => bool,
167
169
  ?"categories" => Hash[String, category_policy_score],
170
+ # Preview. What the policy's link-safety section made of the asset's links; absent when the
171
+ # policy doesn't act on links.
172
+ ?"linkSafety" => link_safety_decision,
168
173
  ?"raw" => moderation_raw
169
174
  }
170
175
  # ASSET_CONTENT_SCHEMA. `format` is the MIME type.
@@ -300,6 +305,63 @@ module CalmLens
300
305
  ?"links" => Array[detected_link],
301
306
  ?"reason" => not_evaluated_reason
302
307
  }
308
+ # Link safety (preview, LinkSafety.ts).
309
+ # What a link's destination is: `safe` when every reputation check passed, `adult` or `gambling`
310
+ # from its domain's content categories, `phishing` or `malware` from a threat list or the domain's
311
+ # security risks, and `unresolved` when a short link or redirect chain could not be followed or a
312
+ # check failed. Every address on a link's way is checked, and the most severe verdict is the
313
+ # link's.
314
+ type link_safety_verdict = "safe" | "adult" | "gambling" | "phishing" | "malware" | "unresolved"
315
+ # The verdicts a policy can act on.
316
+ type flagged_link_verdict = "phishing" | "malware" | "adult" | "gambling" | "unresolved"
317
+ # What a link does to its asset, weakest first. Purging content over a link is never offered.
318
+ type link_safety_action = "allow" | "limit" | "age-restrict" | "block"
319
+ # One reputation check of a link, every address it redirects through and its destination: their
320
+ # domain categories, or a threat-list lookup of their URLs. `failed` makes the link `unresolved`;
321
+ # `labels` is what the check reported for any of the addresses.
322
+ type link_safety_evidence = {
323
+ "check" => "domain-categories" | "threat-lists",
324
+ "status" => "checked" | "failed",
325
+ "labels" => Array[String]
326
+ }
327
+ # A link in the asset and what its destination is. `finalUrl` is where it leads after `redirects`
328
+ # redirects, or `null` when a short link or redirect chain could not be
329
+ # followed.
330
+ type checked_link = {
331
+ "url" => String,
332
+ "finalUrl" => String?,
333
+ "redirects" => Integer,
334
+ "source" => "qr-code" | "text",
335
+ "verdict" => link_safety_verdict,
336
+ "evidence" => Array[link_safety_evidence]
337
+ }
338
+ # Preview. `status: "evaluated"` carries `links`; `"not-evaluated"` carries `reason`.
339
+ type link_safety_result = {
340
+ "status" => "evaluated" | "not-evaluated",
341
+ "stage" => processing_feature_stage,
342
+ ?"links" => Array[checked_link],
343
+ ?"reason" => not_evaluated_reason
344
+ }
345
+ # Preview. LINK_SAFETY_SETTINGS_SCHEMA: the action for an asset with a link of each verdict (a
346
+ # verdict left out is allowed), and optionally an action for every link whose destination is
347
+ # outside `allowedDomains` (bare lowercase domains, which also cover their subdomains).
348
+ type link_safety_settings = {
349
+ "verdictActions" => {
350
+ ?"phishing" => link_safety_action,
351
+ ?"malware" => link_safety_action,
352
+ ?"adult" => link_safety_action,
353
+ ?"gambling" => link_safety_action,
354
+ ?"unresolved" => link_safety_action
355
+ },
356
+ ?"offPlatformLinks" => { "action" => "limit" | "age-restrict" | "block", "allowedDomains" => Array[String] }
357
+ }
358
+ # Preview. The strongest action the asset's links take, the indexes into
359
+ # `classification.linkSafety.links` of the links that take it, and the settings in force.
360
+ type link_safety_decision = {
361
+ "action" => link_safety_action,
362
+ "links" => Array[Integer],
363
+ "settings" => link_safety_settings
364
+ }
303
365
  type provenance_signal = { "kind" => "c2pa" | "iptc-digital-source-type" | "generator-metadata", "value" => String }
304
366
  # `status: "evaluated"` carries `provenance` and `signals`; `"not-evaluated"` carries `reason`.
305
367
  type ai_generated_detection_result = {
@@ -317,6 +379,7 @@ module CalmLens
317
379
  ?"personDetection" => person_detection_result,
318
380
  ?"faceDetection" => face_detection_result,
319
381
  ?"linkDetection" => link_detection_result,
382
+ ?"linkSafety" => link_safety_result,
320
383
  ?"aiGeneratedDetection" => ai_generated_detection_result
321
384
  }
322
385
 
@@ -391,7 +454,8 @@ module CalmLens
391
454
  ?blur: number,
392
455
  ?redact: {
393
456
  ?text: bool,
394
- ?personalInformation: bool
457
+ ?personalInformation: bool,
458
+ ?faces: bool
395
459
  },
396
460
  ?border: {
397
461
  ?color: String,
@@ -437,7 +501,8 @@ module CalmLens
437
501
  ?"blur" => number,
438
502
  ?"redact" => {
439
503
  ?"text" => bool,
440
- ?"personalInformation" => bool
504
+ ?"personalInformation" => bool,
505
+ ?"faces" => bool
441
506
  },
442
507
  ?"border" => {
443
508
  ?"color" => String,
@@ -474,12 +539,9 @@ module CalmLens
474
539
  }
475
540
  # </generated: image-transform-options>
476
541
 
477
- # One structured transform step. `imageTransform` is the only transform type.
478
- type transform_step_input = { type: "imageTransform", ?options: image_transform_options_input }
479
- type transform_step = { "type" => "imageTransform", ?"options" => image_transform_options }
480
- # A selector String with one step per path segment, such as `fit=cover,width=1024/blur=12`, or
481
- # structured steps, which the SDK serializes into a selector.
482
- type transform_input = String | transform_step_input | Array[transform_step_input]
542
+ # Transform options applied in one pass: a selector String such as `width=1024,fit=cover`, or
543
+ # structured options, which the SDK serializes into a selector.
544
+ type transform_input = String | image_transform_options_input
483
545
 
484
546
  type lens_type = "image" | "video" | "audio" | "document" | "website"
485
547
  # ENSURE_LENS_BODY_SCHEMA. Changing a saved lens needs a greater `version` (default 1).
@@ -490,7 +552,7 @@ module CalmLens
490
552
  active: bool,
491
553
  ?description: String?,
492
554
  ?version: Integer,
493
- ?transforms: Array[transform_step_input]?
555
+ ?transform: image_transform_options_input?
494
556
  }
495
557
  type lens = {
496
558
  "id" => String,
@@ -501,7 +563,7 @@ module CalmLens
501
563
  ?"description" => String?,
502
564
  "type" => lens_type,
503
565
  "active" => bool,
504
- ?"transforms" => Array[transform_step]?,
566
+ ?"transform" => image_transform_options?,
505
567
  "createdAt" => Integer,
506
568
  ?"updatedAt" => Integer
507
569
  }
@@ -560,6 +622,47 @@ module CalmLens
560
622
  "ordering" => Array[order]
561
623
  }
562
624
 
625
+ # ---- Policies (Policy.ts) ---------------------------------------------------------------------
626
+
627
+ type policy_preset_id = "general-community" | "children-platform" | "education-support" | "wholesome-community"
628
+ # What a policy does with one category: `action` when its policy score reaches 0.5 (default
629
+ # `block`), allowances that let covered content through or act on it more softly, and
630
+ # restrictions, which outweigh allowances.
631
+ type category_policy_settings = {
632
+ ?"action" => flagged_content_action,
633
+ ?"allowances" => Array[policy_rule_snapshot],
634
+ ?"restrictions" => Array[policy_rule_snapshot]
635
+ }
636
+ # Settings by category (`sexual`, `violence`, ...), and optionally link safety.
637
+ type policy_settings = {
638
+ "categorySettings" => Hash[String, category_policy_settings],
639
+ ?"linkSafety" => link_safety_settings
640
+ }
641
+ # Where a policy started from a built-in preset. `version` is the release it is pinned to,
642
+ # `latestVersion` the newest; `projectId` is the project whose preset policy it is, or nil for a copy.
643
+ type policy_preset_origin = {
644
+ "id" => policy_preset_id,
645
+ "version" => Integer,
646
+ "latestVersion" => Integer,
647
+ "projectId" => String?
648
+ }
649
+ # A moderation policy. Policies belong to the project's organisation and its projects share them.
650
+ # Send `revision` with each change; a policy changed since is refused with `policy/revision-conflict`.
651
+ type policy = {
652
+ "id" => String,
653
+ "name" => String,
654
+ "description" => String,
655
+ "settings" => policy_settings,
656
+ "revision" => Integer,
657
+ "automaticUpdates" => bool,
658
+ "preset" => policy_preset_origin?,
659
+ "projectIds" => Array[String],
660
+ "createdAt" => Integer,
661
+ "updatedAt" => Integer
662
+ }
663
+ # Every policy of the project's organisation, oldest first, and the one the project moderates with.
664
+ type policy_list = { "policies" => Array[policy], "projectPolicyId" => String }
665
+
563
666
  # ---- Events and webhooks (Event.ts, EventCatalog.ts, WebhookPayload.ts) ----------------------
564
667
 
565
668
  # EVENT_TYPES in EventCatalog.ts. Ignore types your code does not handle. Webhook, Slack and email
@@ -602,7 +705,14 @@ module CalmLens
602
705
  "createdAt" => Integer
603
706
  }
604
707
  # Unlike other lists, `nextCursor` is never nil: keep it to receive later events.
605
- type events_page = { "items" => Array[event], "nextCursor" => String, "hasMore" => bool }
708
+ # `excludedTypes` are the event types the feed never includes, because the API key lacks the permission
709
+ # to see them.
710
+ type events_page = {
711
+ "items" => Array[event],
712
+ "nextCursor" => String,
713
+ "hasMore" => bool,
714
+ "excludedTypes" => Array[event_type]
715
+ }
606
716
 
607
717
  # ---- Method options -------------------------------------------------------------------------
608
718
 
@@ -661,7 +771,7 @@ module CalmLens
661
771
  # Defaults to `rendered` on the server; `original` takes no lens or transform.
662
772
  ?resource: "original" | "rendered",
663
773
  ?expires_in_seconds: Integer,
664
- # A lens runs before any additional transforms.
774
+ # Transform options replace the lens's options of the same name.
665
775
  ?lens: String,
666
776
  ?transform: transform_input,
667
777
  ?request_options: request_options
@@ -800,6 +910,37 @@ module CalmLens
800
910
  ?request_options: request_options }
801
911
  type hash_list_entry_id_options = { entry_id: String, ?request_options: request_options }
802
912
 
913
+ # Policy settings as options: snake_case Symbol keys, or a policy's `settings` as the API returned it.
914
+ type policy_rule_input = { when: Array[String], action: flagged_content_action, ?name: String, ?match: "all", ?unless: Array[String] }
915
+ type category_policy_settings_input = {
916
+ ?action: flagged_content_action,
917
+ ?allowances: Array[policy_rule_input],
918
+ ?restrictions: Array[policy_rule_input]
919
+ }
920
+ type link_safety_settings_input = {
921
+ verdict_actions: Hash[String | Symbol, link_safety_action],
922
+ ?off_platform_links: { action: "limit" | "age-restrict" | "block", allowed_domains: Array[String] }
923
+ }
924
+ type policy_settings_input = policy_settings | {
925
+ category_settings: Hash[String | Symbol, category_policy_settings_input],
926
+ ?link_safety: link_safety_settings_input
927
+ }
928
+ type list_policies_options = { ?request_options: request_options }
929
+ type policy_id_options = { policy_id: String, ?request_options: request_options }
930
+ # Send `settings` (defining every category) with a `name`, or `preset_id` to copy the latest
931
+ # release of a built-in preset, named after it unless you give a `name`.
932
+ type create_policy_options = { ?name: String, ?description: String, ?settings: policy_settings_input,
933
+ ?preset_id: policy_preset_id, ?request_options: request_options }
934
+ type update_policy_options = { policy_id: String, revision: Integer, ?name: String, ?description: String,
935
+ ?settings: policy_settings_input, ?request_options: request_options }
936
+ # `reset` restores the pinned preset version (send `preset.version`); `update` moves an unchanged
937
+ # copy to the latest release (send `preset.latestVersion`).
938
+ type apply_policy_preset_options = { policy_id: String, operation: "reset" | "update", revision: Integer,
939
+ preset_version: Integer, ?request_options: request_options }
940
+ type set_policy_automatic_updates_options = { policy_id: String, enabled: bool, revision: Integer,
941
+ preset_version: Integer, ?request_options: request_options }
942
+ type assign_policy_options = { policy_id: String, ?request_options: request_options }
943
+
803
944
  # `page_size` is 1 to 100, default 50.
804
945
  type list_events_options = { ?cursor: String, ?page_size: Integer, ?request_options: request_options }
805
946
 
@@ -811,7 +952,7 @@ module CalmLens
811
952
  active: bool,
812
953
  ?description: String?,
813
954
  ?version: Integer,
814
- ?transforms: Array[transform_step_input]?,
955
+ ?transform: image_transform_options_input?,
815
956
  ?request_options: request_options
816
957
  }
817
958
 
@@ -822,7 +963,7 @@ module CalmLens
822
963
  # `rendered` is the default. `original` takes no lens or transform; `placeholder` is a small blurred PNG
823
964
  # that takes no lens or transform.
824
965
  ?resource: "original" | "rendered" | "placeholder",
825
- # A lens runs before any additional transforms.
966
+ # Transform options replace the lens's options of the same name.
826
967
  ?lens: String,
827
968
  ?transform: transform_input,
828
969
  # Appends `?ageConfirmed=true`, so a public `age-restricted` asset is served from this URL. Set it
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: calmlens
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.2
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - CalmLens
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-11-03 00:00:00.000000000 Z
11
+ date: 2026-11-04 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'Server-side client for the CalmLens API: moderation, uploads, events,
14
14
  webhooks and CDN delivery.'
@@ -30,6 +30,8 @@ files:
30
30
  - lib/calmlens/internal.rb
31
31
  - lib/calmlens/lenses.rb
32
32
  - lib/calmlens/moderation_context.rb
33
+ - lib/calmlens/option_keys.rb
34
+ - lib/calmlens/policies.rb
33
35
  - lib/calmlens/resource.rb
34
36
  - lib/calmlens/response.rb
35
37
  - lib/calmlens/server_client.rb
@@ -39,6 +41,9 @@ files:
39
41
  - lib/calmlens/testing/fake_api.rb
40
42
  - lib/calmlens/testing/mock_decision.rb
41
43
  - lib/calmlens/testing/placeholder.rb
44
+ - lib/calmlens/testing/test_image_grid.rb
45
+ - lib/calmlens/testing/test_input_scenario.rb
46
+ - lib/calmlens/testing/test_inputs.rb
42
47
  - lib/calmlens/text.rb
43
48
  - lib/calmlens/transforms.rb
44
49
  - lib/calmlens/transport.rb