vicary 0.2.5 → 0.2.6
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 +14 -1
- data/lib/vicary/conformance.rb +26 -0
- data/lib/vicary/spans.rb +129 -0
- data/lib/vicary/version.rb +1 -1
- data/lib/vicary.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 537ad53f4299fd2a3cdfc7bceab94b4678bb3a7da01c2e73f5f0c1883d1dad2e
|
|
4
|
+
data.tar.gz: e51c1775be63158b1724739c94ddecb48b406c20a339d1b4b8a0acba1d753e5d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d406808fe2dfc71599bb85516237fe3272876bd2302e3d794c12fb2d296fb2e4f65ea9906be0a8fe1f7deaaf1afab7cb3e6fa3c15dbe5f77f68c861b5f70ad39
|
|
7
|
+
data.tar.gz: 70e6fbef81ab903425190feda6683d3343604b980da6033f0ad7471816f4c2887eaf8408637dc1092e394e60162b877a1e5de6aa1a91516ba4c5d161bcd0b116
|
data/README.md
CHANGED
|
@@ -23,8 +23,21 @@ Vicary.redact("My cousin Terrence Okonkwo came over that summer.", identity)
|
|
|
23
23
|
|
|
24
24
|
masked, n, restore_map = Vicary.redact_with_report(essay, identity)
|
|
25
25
|
Vicary.restore(masked, restore_map) == essay # => true
|
|
26
|
+
|
|
27
|
+
# Where each replacement came from, for a host that draws on the essay: a
|
|
28
|
+
# placeholder is not the width of the name it replaced, so an offset measured
|
|
29
|
+
# against `masked` is displaced against the composition the student holds.
|
|
30
|
+
spans = Vicary::Spans.derive(masked, restore_map)
|
|
31
|
+
Vicary::Spans.to_original(offset, spans) # masked coordinates -> the student's
|
|
32
|
+
Vicary::Spans.to_redacted(offset, spans) # and back
|
|
33
|
+
Vicary::Spans.original(masked, restore_map) == essay # => true
|
|
26
34
|
```
|
|
27
35
|
|
|
36
|
+
An offset landing inside a placeholder resolves to the start of the span it
|
|
37
|
+
replaced. A map that is absent, or that covers only some of the placeholders in
|
|
38
|
+
the text, yields **no** spans and translates as the identity — a caller can
|
|
39
|
+
handle "no spans" and cannot detect a wrong offset.
|
|
40
|
+
|
|
28
41
|
## Checking it
|
|
29
42
|
|
|
30
43
|
Three layers, because each catches what the one above it cannot.
|
|
@@ -33,7 +46,7 @@ Three layers, because each catches what the one above it cannot.
|
|
|
33
46
|
|---|---|
|
|
34
47
|
| `rake conformance` | the scoreboard against the 54 frames — the final bar, and a coarse first one |
|
|
35
48
|
| `rake gates` | the nine gates, all nine measured from what the repository ships |
|
|
36
|
-
| `rake test` | the unit suites, including `primitives_test.rb
|
|
49
|
+
| `rake test` | the unit suites, including `primitives_test.rb` (forty-odd primitives over the shared corpus, which says *which brick* is crooked) and `spans_test.rb` (the offset arithmetic over the shared `spans.json`) |
|
|
37
50
|
| `rake parity` | gazetteer verdicts, name by name, against the Python reference |
|
|
38
51
|
| `rake redaction_parity` | masked bytes against the Python reference, on prose no fixture contains |
|
|
39
52
|
|
data/lib/vicary/conformance.rb
CHANGED
|
@@ -155,6 +155,32 @@ module Vicary
|
|
|
155
155
|
raw
|
|
156
156
|
end
|
|
157
157
|
|
|
158
|
+
# The offset-translation spec — {Vicary::Spans}' cases and their answers.
|
|
159
|
+
#
|
|
160
|
+
# A third layer, checking a third thing. `frames.json` says what is masked
|
|
161
|
+
# and `primitives.json` says which rule decided it; neither constrains
|
|
162
|
+
# WHERE a replacement came from, and a port can reproduce every byte of
|
|
163
|
+
# both while being unable to tell a host where to draw a highlight on the
|
|
164
|
+
# student's own essay.
|
|
165
|
+
#
|
|
166
|
+
# Absent is an error rather than an empty document, for the same reason
|
|
167
|
+
# `just _conformance-check` refuses to run without a spec: a port that
|
|
168
|
+
# finds no cases and reports success has checked its arithmetic against
|
|
169
|
+
# nothing.
|
|
170
|
+
def load_spans(dir = nil)
|
|
171
|
+
dir = Pathname.new(dir || directory)
|
|
172
|
+
path = dir.join("spans.json")
|
|
173
|
+
unless path.file?
|
|
174
|
+
raise SpecError,
|
|
175
|
+
"no spans.json at #{path}. The ports would check their offset " \
|
|
176
|
+
"translation against nothing."
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
raw = JSON.parse(path.read)
|
|
180
|
+
require_version(raw["document_version"], "spans.json")
|
|
181
|
+
raw
|
|
182
|
+
end
|
|
183
|
+
|
|
158
184
|
# Score an implementation against every frame.
|
|
159
185
|
#
|
|
160
186
|
# The block receives (sentence, identity) — the same input every Python arm
|
data/lib/vicary/spans.rb
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vicary
|
|
4
|
+
# One replacement, located in both the original and the masked text.
|
|
5
|
+
#
|
|
6
|
+
# `orig_start...orig_end` is what was removed; `new_start...new_end` is the
|
|
7
|
+
# placeholder standing in its place. The two widths differ — that difference
|
|
8
|
+
# is the whole reason this type exists.
|
|
9
|
+
RedactionSpan = Struct.new(:orig_start, :orig_end, :new_start, :new_end) do
|
|
10
|
+
# How much this replacement moved everything after it.
|
|
11
|
+
def delta
|
|
12
|
+
(new_end - new_start) - (orig_end - orig_start)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
{ orig_start: orig_start, orig_end: orig_end,
|
|
17
|
+
new_start: new_start, new_end: new_end }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Offset translation between the masked text and the composition the student
|
|
22
|
+
# holds — the Ruby port of `python/src/vicary/redaction.py`'s span layer.
|
|
23
|
+
#
|
|
24
|
+
# Why a host needs it. `redact_with_report` hands back masked bytes and a
|
|
25
|
+
# restore map, and between them they still say nothing about WHERE anything
|
|
26
|
+
# sits. A host rendering a highlight works in the student's coordinates, the
|
|
27
|
+
# detector produced its text in masked coordinates, and `{NAME_1}` is not the
|
|
28
|
+
# width of the name it replaced: every offset after the first replacement is
|
|
29
|
+
# displaced by the cumulative delta. Measured on a 56-paper corpus, redaction
|
|
30
|
+
# fired on 43 essays and not one recorded offset pair on those 43 landed on
|
|
31
|
+
# the original text.
|
|
32
|
+
#
|
|
33
|
+
# **Nothing in the masker records anything for this to work.** That is the
|
|
34
|
+
# point, and it is why this is a pure module rather than instrumentation:
|
|
35
|
+
# {Vicary::Candidates.mask_candidates} is many passes over a string each pass
|
|
36
|
+
# mutates, so a span recorded inside one pass is in that pass's intermediate
|
|
37
|
+
# coordinates and would have to be composed forward through every later one.
|
|
38
|
+
# It is unnecessary — the finished text still *contains* every placeholder, so
|
|
39
|
+
# each replacement's new span is where its placeholder sits, and the restore
|
|
40
|
+
# map gives the original, so its width is a `length`.
|
|
41
|
+
#
|
|
42
|
+
# Checked against `conformance/spans.json`, generated from the Python
|
|
43
|
+
# reference, by `test/spans_test.rb`. The arithmetic is shared with the other
|
|
44
|
+
# two ports and pinned there rather than described here.
|
|
45
|
+
module Spans
|
|
46
|
+
# A placeholder token as it appears in masked text, e.g. `{NAME_1}`.
|
|
47
|
+
PLACEHOLDER = /\{[A-Z_]+(?:_\d+)?\}/.freeze
|
|
48
|
+
|
|
49
|
+
class << self
|
|
50
|
+
# The span map, derived from the masked text and the restore map alone.
|
|
51
|
+
#
|
|
52
|
+
# Replacement preserves order, so one left-to-right walk accumulating the
|
|
53
|
+
# running delta recovers the original offsets exactly.
|
|
54
|
+
#
|
|
55
|
+
# Returns `[]` when `restore_map` is empty or does not cover a placeholder
|
|
56
|
+
# present in the text. A partial map is refused rather than answered for
|
|
57
|
+
# the half it can place, because a translation that is right for some
|
|
58
|
+
# offsets and silently wrong for others is worse than one that declines:
|
|
59
|
+
# the caller can handle "no map" and cannot detect "wrong offset".
|
|
60
|
+
def derive(masked, restore_map)
|
|
61
|
+
return [] if restore_map.nil? || restore_map.empty?
|
|
62
|
+
|
|
63
|
+
spans = []
|
|
64
|
+
delta = 0
|
|
65
|
+
masked.to_s.enum_for(:scan, PLACEHOLDER).each do
|
|
66
|
+
match = Regexp.last_match
|
|
67
|
+
original = restore_map[match[0]]
|
|
68
|
+
return [] if original.nil?
|
|
69
|
+
|
|
70
|
+
orig_start = match.begin(0) - delta
|
|
71
|
+
span = RedactionSpan.new(orig_start, orig_start + original.length,
|
|
72
|
+
match.begin(0), match.end(0))
|
|
73
|
+
spans << span
|
|
74
|
+
delta += span.delta
|
|
75
|
+
end
|
|
76
|
+
spans
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A redacted-text offset in original coordinates.
|
|
80
|
+
#
|
|
81
|
+
# An offset landing INSIDE a placeholder maps to the start of the span it
|
|
82
|
+
# replaced: the placeholder's interior has no counterpart in the original,
|
|
83
|
+
# so any position within it is the same position in original terms.
|
|
84
|
+
def to_original(offset, spans)
|
|
85
|
+
result = offset
|
|
86
|
+
spans.each do |span|
|
|
87
|
+
break if offset < span.new_start
|
|
88
|
+
return span.orig_start if offset < span.new_end
|
|
89
|
+
|
|
90
|
+
result = span.orig_end + (offset - span.new_end)
|
|
91
|
+
end
|
|
92
|
+
result
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# An original-text offset in redacted coordinates.
|
|
96
|
+
#
|
|
97
|
+
# An offset inside a replaced span maps to the start of its placeholder,
|
|
98
|
+
# for the mirror-image reason.
|
|
99
|
+
def to_redacted(offset, spans)
|
|
100
|
+
result = offset
|
|
101
|
+
spans.each do |span|
|
|
102
|
+
break if offset < span.orig_start
|
|
103
|
+
return span.new_start if offset < span.orig_end
|
|
104
|
+
|
|
105
|
+
result = span.new_end + (offset - span.orig_end)
|
|
106
|
+
end
|
|
107
|
+
result
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Reconstruct the pre-redaction text from the masked bytes and the map.
|
|
111
|
+
#
|
|
112
|
+
# Exact where a span map exists, and the masked text unchanged where one
|
|
113
|
+
# does not — which is the honest answer rather than a partial restoration,
|
|
114
|
+
# for the reason {derive} declines.
|
|
115
|
+
def original(masked, restore_map)
|
|
116
|
+
masked = masked.to_s
|
|
117
|
+
out = +""
|
|
118
|
+
prev = 0
|
|
119
|
+
derive(masked, restore_map).each do |span|
|
|
120
|
+
out << masked[prev...span.new_start]
|
|
121
|
+
out << restore_map[masked[span.new_start...span.new_end]]
|
|
122
|
+
prev = span.new_end
|
|
123
|
+
end
|
|
124
|
+
out << masked[prev..].to_s
|
|
125
|
+
out
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
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.6"
|
|
10
10
|
end
|
data/lib/vicary.rb
CHANGED
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.6
|
|
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-
|
|
11
|
+
date: 2026-09-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Finds the names a student writes about — classmates, teachers, relatives — and
|
|
@@ -38,6 +38,7 @@ files:
|
|
|
38
38
|
- lib/vicary/lexicon.rb
|
|
39
39
|
- lib/vicary/minter.rb
|
|
40
40
|
- lib/vicary/redact.rb
|
|
41
|
+
- lib/vicary/spans.rb
|
|
41
42
|
- lib/vicary/structured.rb
|
|
42
43
|
- lib/vicary/version.rb
|
|
43
44
|
homepage: https://github.com/bwthomas/vicary
|