vicary 0.2.7 → 0.2.8
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.
- checksums.yaml +4 -4
- data/README.md +7 -0
- data/lib/vicary/redact.rb +93 -0
- data/lib/vicary/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 70cca39c8c91256c36ed5952eb12710da4125bd605f65dc4c1c71de8e0d77395
|
|
4
|
+
data.tar.gz: 879ab66dc00f074accad84a80887ac48a93fc9fe09968bc04f819bee1e0e20a8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ecf9bbecf10214d42fc5e495d6b3b161f7d1f1f1a080fb13631c379f06dd609ec182065685060d63f733890c468eacb0b854e371a2fec6b3fdb5bebdd9f45f03
|
|
7
|
+
data.tar.gz: 704e8aaf854c08b3128299dc425e543c00cf3789909dd98ccbb13509d9c80020d72d991a8e4c690424f9508ba8dfef6a5c9da1c9ca8f513a642407a33bfdf5b5
|
data/README.md
CHANGED
|
@@ -31,6 +31,13 @@ spans = Vicary::Spans.derive(masked, restore_map)
|
|
|
31
31
|
Vicary::Spans.to_original(offset, spans) # masked coordinates -> the student's
|
|
32
32
|
Vicary::Spans.to_redacted(offset, spans) # and back
|
|
33
33
|
Vicary::Spans.original(masked, restore_map) == essay # => true
|
|
34
|
+
|
|
35
|
+
# Several fields, one pass, and one restore map per field. Numbering is the
|
|
36
|
+
# JOINED document's, so one person keeps one placeholder across every field —
|
|
37
|
+
# masking field by field renumbers and hands `{NAME_1}` to two people.
|
|
38
|
+
masked, n, maps, batched = Vicary.redact_batch_with_report(fields, identity)
|
|
39
|
+
# `batched == false` means the join did not round-trip and each field was
|
|
40
|
+
# passed separately, so cross-field placeholder identity does NOT hold.
|
|
34
41
|
```
|
|
35
42
|
|
|
36
43
|
An offset landing inside a placeholder resolves to the start of the span it
|
data/lib/vicary/redact.rb
CHANGED
|
@@ -32,6 +32,15 @@ module Vicary
|
|
|
32
32
|
|
|
33
33
|
NAME_DETECTION_ENV_VAR = "VICARY_NAME_DETECTION"
|
|
34
34
|
|
|
35
|
+
# Separator for a batched pass, byte-identical to the Python reference's
|
|
36
|
+
# `BATCH_SEPARATOR`. Chosen to be something a writing-coach model will not
|
|
37
|
+
# emit and a PII policy will not touch: no words, no digits, no name-shaped or
|
|
38
|
+
# address-shaped substrings for an entity detector to bite on, and distinctive
|
|
39
|
+
# enough that a stray single character cannot fake it. A round-trip check runs
|
|
40
|
+
# anyway — see {redact_batch_with_report} — because a separator that "should"
|
|
41
|
+
# survive is exactly the kind of assumption that silently mis-aligns fields.
|
|
42
|
+
BATCH_SEPARATOR = "\n\u241E\u241E\u241E\n"
|
|
43
|
+
|
|
35
44
|
IDENTITY_ALIASES = Set.new(%w[identity off none 0 false no]).freeze
|
|
36
45
|
GAZETTEER_ALIASES = Set.new(%w[gazetteer on 1 true yes names]).freeze
|
|
37
46
|
LOWERCASE_ALIASES = Set.new(%w[gazetteer-lowercase gazetteer_lowercase lowercase full max]).freeze
|
|
@@ -156,6 +165,90 @@ module Vicary
|
|
|
156
165
|
[masked, n, minter.assigned]
|
|
157
166
|
end
|
|
158
167
|
|
|
168
|
+
# Split a joined document's restore map into one map per part.
|
|
169
|
+
#
|
|
170
|
+
# By OCCURRENCE, not by re-deriving: the placeholders are unique strings
|
|
171
|
+
# over the joined document, so a part's map is exactly the entries whose
|
|
172
|
+
# placeholder survived into that part. Re-running detection per part would
|
|
173
|
+
# renumber, and renumbering is the one thing this must not do.
|
|
174
|
+
def split_joined_restore_map(restore_map, parts)
|
|
175
|
+
parts.map do |part|
|
|
176
|
+
restore_map.select { |placeholder, _original| part.include?(placeholder) }
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Redact several fields in ONE pass, and return the way back for each.
|
|
181
|
+
#
|
|
182
|
+
# Numbering is the JOINED document's, not each field's, so the same entity
|
|
183
|
+
# carries the same placeholder across fields — which is the property that
|
|
184
|
+
# lets a reader match a name in one field to the same name in another. A
|
|
185
|
+
# caller must therefore not assume a field's map starts at `{NAME_1}`.
|
|
186
|
+
#
|
|
187
|
+
# **Why a host wants this.** Masking field by field is irreversible in the
|
|
188
|
+
# way that matters: a caller holding only the masked strings cannot tell
|
|
189
|
+
# which placeholder belongs to which field, nor what any of them stood for,
|
|
190
|
+
# so it cannot put back the words its reader is already entitled to see.
|
|
191
|
+
# It also renumbers, so one entity gets a different token per field.
|
|
192
|
+
#
|
|
193
|
+
# **Where it can go wrong, and what happens then.** The join/split round
|
|
194
|
+
# trip is the risk: masking changes lengths, so offsets cannot be trusted,
|
|
195
|
+
# and the split relies on {BATCH_SEPARATOR} surviving the pass intact. If
|
|
196
|
+
# the masked document does not split back into exactly as many parts as
|
|
197
|
+
# went in, this **falls back to per-field passes and says so**
|
|
198
|
+
# (`batched == false`) rather than returning a mis-aligned list — one
|
|
199
|
+
# field's suggestion pasted into another's is a worse outcome than a slower
|
|
200
|
+
# call. Cross-field placeholder identity does not survive that fallback,
|
|
201
|
+
# which is stated rather than papered over: a caller correlating names
|
|
202
|
+
# across fields would otherwise be silently wrong on exactly those inputs.
|
|
203
|
+
#
|
|
204
|
+
# Returns `[masked_texts, n_masked, restore_maps, batched]`. `restore_maps`
|
|
205
|
+
# is positionally aligned with `texts` — one map per field, empty for a
|
|
206
|
+
# field that was empty or that nothing was masked in.
|
|
207
|
+
def redact_batch_with_report(texts, identity, options = {})
|
|
208
|
+
texts = texts.to_a
|
|
209
|
+
return [[], 0, [], true] if texts.empty?
|
|
210
|
+
|
|
211
|
+
empty_maps = texts.map { {} }
|
|
212
|
+
nonempty = texts.each_index.reject { |i| texts[i].nil? || texts[i].empty? }
|
|
213
|
+
return [texts.dup, 0, empty_maps, true] if nonempty.empty?
|
|
214
|
+
|
|
215
|
+
if nonempty.length == 1
|
|
216
|
+
only = nonempty.first
|
|
217
|
+
masked, n, map = redact_with_report(texts[only], identity, options)
|
|
218
|
+
out = texts.dup
|
|
219
|
+
out[only] = masked
|
|
220
|
+
maps = empty_maps.dup
|
|
221
|
+
maps[only] = map
|
|
222
|
+
return [out, n, maps, true]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
joined = nonempty.map { |i| texts[i] }.join(BATCH_SEPARATOR)
|
|
226
|
+
masked, n, map = redact_with_report(joined, identity, options)
|
|
227
|
+
parts = masked.split(BATCH_SEPARATOR, -1)
|
|
228
|
+
|
|
229
|
+
if parts.length != nonempty.length
|
|
230
|
+
out = texts.dup
|
|
231
|
+
maps = empty_maps.dup
|
|
232
|
+
total = 0
|
|
233
|
+
nonempty.each do |i|
|
|
234
|
+
field_masked, field_n, field_map = redact_with_report(texts[i], identity, options)
|
|
235
|
+
out[i] = field_masked
|
|
236
|
+
maps[i] = field_map
|
|
237
|
+
total += field_n
|
|
238
|
+
end
|
|
239
|
+
return [out, total, maps, false]
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
out = texts.dup
|
|
243
|
+
maps = empty_maps.dup
|
|
244
|
+
split = split_joined_restore_map(map, parts)
|
|
245
|
+
nonempty.each_with_index do |i, k|
|
|
246
|
+
out[i] = parts[k]
|
|
247
|
+
maps[i] = split[k]
|
|
248
|
+
end
|
|
249
|
+
[out, n, maps, true]
|
|
250
|
+
end
|
|
251
|
+
|
|
159
252
|
# Redact personal names and structured PII from `text`.
|
|
160
253
|
#
|
|
161
254
|
# @param text [String] the composition to redact.
|
data/lib/vicary/version.rb
CHANGED
|
@@ -6,5 +6,5 @@ module Vicary
|
|
|
6
6
|
# Shared across all three front doors on purpose: one detector, one number. A
|
|
7
7
|
# gem 0.3.0 that corresponds to nothing on PyPI cannot be reasoned about, and
|
|
8
8
|
# the parity claim is between *versions*, not between package names.
|
|
9
|
-
VERSION = "0.2.
|
|
9
|
+
VERSION = "0.2.8"
|
|
10
10
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vicary
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Blake Thomas
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Finds the names a student writes about — classmates, teachers, relatives — and
|