vicary 0.2.8 → 0.2.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70cca39c8c91256c36ed5952eb12710da4125bd605f65dc4c1c71de8e0d77395
4
- data.tar.gz: 879ab66dc00f074accad84a80887ac48a93fc9fe09968bc04f819bee1e0e20a8
3
+ metadata.gz: 2417f5e9aa64949da91c1ca4cf5cc1dac1b8a4bdc70929576bbc7b64d8e629f9
4
+ data.tar.gz: 67cd930a6714621ed7872054fac572b87a210940b1644032a845b16d2b2bbfb3
5
5
  SHA512:
6
- metadata.gz: ecf9bbecf10214d42fc5e495d6b3b161f7d1f1f1a080fb13631c379f06dd609ec182065685060d63f733890c468eacb0b854e371a2fec6b3fdb5bebdd9f45f03
7
- data.tar.gz: 704e8aaf854c08b3128299dc425e543c00cf3789909dd98ccbb13509d9c80020d72d991a8e4c690424f9508ba8dfef6a5c9da1c9ca8f513a642407a33bfdf5b5
6
+ metadata.gz: dfe4bc4a8eb71174d969e41f270eadf8c7170a97e54870fc27b8f2610a8ce2e6b976e39cba2ed87435652d8ca366ca4fb9d90795f773bd8b20db6017a3bce99b
7
+ data.tar.gz: 3bef064a891171f7668d8773e813c6efedea6f25385c5da72a4141279662838b623bcefe4a06de28866efdf7f53b04051fa74a02ad6f9c35acc70b9eba1b0f9a
data/README.md CHANGED
@@ -69,7 +69,7 @@ gzip, which `zlib` reads, so that hazard does not arise on the default path.
69
69
 
70
70
  Two of the last three read the corpus the repository now ships in
71
71
  `conformance/corpora/`, so they measure on a bare checkout with no environment
72
- set: 100% carrier recall and 8.150 over-fired spans per essay against a ≤ 8.15
72
+ set: 100% carrier recall and 7.400 over-fired spans per essay against a ≤ 7.40
73
73
  bar, identical to Python and TypeScript. `VICARY_EVAL_CORPUS_TSV` is an override
74
74
  for a different corpus, not a requirement.
75
75
 
data/assets/MANIFEST.json CHANGED
@@ -20,14 +20,21 @@
20
20
  "title": 38024
21
21
  }
22
22
  },
23
- "stop_words.txt": {
24
- "bytes": 3996,
25
- "entries": 421,
23
+ "stop_words_never_capitalised.txt": {
24
+ "bytes": 8017,
25
+ "entries": 499,
26
26
  "format": 1,
27
- "min_package_version": "0.2.0",
28
- "sha256": "f4dfe89490efe47c7520dac3b9185f39c0eeb397f3ab33f5d1c1211591dfcf30"
27
+ "min_package_version": "0.2.10",
28
+ "sha256": "23798cb88672c126d71a9d0988b9adf0aa044bdc291847012d76deae71fe1d33"
29
+ },
30
+ "stop_words_sometimes_capitalised.txt": {
31
+ "bytes": 6351,
32
+ "entries": 295,
33
+ "format": 1,
34
+ "min_package_version": "0.2.10",
35
+ "sha256": "d6c842a9eeaaa74d739a9daea2439d2f69f8b97e7e87157caafd685fd11a32c8"
29
36
  }
30
37
  },
31
38
  "manifest_version": 1,
32
- "written_by": "vicary 0.2.0"
39
+ "written_by": "vicary 0.2.10"
33
40
  }
@@ -0,0 +1,126 @@
1
+ #!lexicon 1
2
+ #!list stop_words_never_capitalised 499
3
+ # Ordinary words that must never become name candidates, and that English never
4
+ # capitalises in the middle of a sentence.
5
+ #
6
+ # HALF A LIST. The stoplist every front door vetoes candidates against is this
7
+ # file UNION `stop_words_sometimes_capitalised.txt`, and neither half is the
8
+ # stoplist on its own. Loading only one under-vetoes, which makes the redactor
9
+ # more aggressive — the failure that looks privacy-safe and corrupts prose. The
10
+ # `#!list` count on each file is per-file; the union is what code must build.
11
+ #
12
+ # Why there are two files. One consumer asks a second question of these words:
13
+ # `capitalises_ordinary_words` decides a document's capitals are untrustworthy
14
+ # when it finds a mid-sentence capital on a stop word, on the premise that a
15
+ # capital there was never orthographic. That premise is true of the words below
16
+ # and false of the words in the sibling file — "in July" and "my Dad" are
17
+ # correctly capitalised English, and reading them as sloppiness suppressed
18
+ # `Alvarez` in "We stayed with the Alvarez family in July." So the *veto* wants
19
+ # every stop word and the *signal* wants only these, and one list could not
20
+ # serve both.
21
+ #
22
+ # The line between the two files is orthographic, not thematic, because a
23
+ # thematic line is an argument every new word reopens:
24
+ #
25
+ # here function words, verbs, adverbs, interjections and contractions —
26
+ # a capital on one mid-sentence is a mistake, never a construction;
27
+ # sibling nouns, adjectives, numerals, and the proper-noun categories —
28
+ # English capitalises these inside proper names ("Lincoln School",
29
+ # "Great Lakes", "First Baptist"), so a capital is uninformative.
30
+ #
31
+ # Language-neutral on purpose. This list is the only thing standing between
32
+ # candidate generation and "mask every capitalised word", so all three front doors
33
+ # have to work from the same words — a stoplist transliterated by hand into a
34
+ # second language is a second detector wearing the first one's name, and the
35
+ # difference shows up as prose corruption in one language and not the others,
36
+ # which no parity check on masked output would catch.
37
+ #
38
+ # Skewed toward over-inclusion deliberately. A missed name is one span and shows
39
+ # up in the recall number; a wrongly-masked common word corrupts every essay that
40
+ # uses it and shows up nowhere unless somebody reads the prose.
41
+ #
42
+ # Format: `#!` lines are directives, `#` lines are comments, and every other line
43
+ # contributes whitespace-separated words. The count on the `#!list` directive is
44
+ # the number of DISTINCT words after case-folding — authored and generated
45
+ # together, since a reader loads one file — and each front door asserts it
46
+ # against what it parsed. That is not ceremony: a short read here silently makes
47
+ # the redactor MORE aggressive, which looks privacy-safe and passes any check that
48
+ # only asks whether something was masked.
49
+ #
50
+ # A word appearing twice is not an error — the groupings below are thematic and
51
+ # overlap ("else", "may", "us"), and enforcing uniqueness in the source would make
52
+ # the list harder to read for no benefit.
53
+ #
54
+ # Write the SINGULAR only. Plurals are generated into the region at the bottom by
55
+ # `python -m vicary_build lexicon`, which is also what stops one from being added
56
+ # without being priced: the generator drops any form an American bears as a
57
+ # surname, and a hand-written plural is not priced by anything.
58
+ a an the this that these those there here it its it's
59
+ i me my mine myself we us our ours ourselves you your yours
60
+ he him his she her hers they them their theirs who whom whose which what
61
+ and or but so because although though however therefore thus hence yet
62
+ if then else when while until since before after during once whenever
63
+ for from to into onto out off over under above below between among across
64
+ through around about against along beside beyond within without
65
+ at by in on up down near finally
66
+ is am are was were be been being have has had having do does did doing
67
+ can could will would shall should might must let
68
+ not no nor none nothing never always sometimes often usually rarely
69
+ all any both each every few many more most much several some such
70
+ also even just only really very too still again ever else quite rather
71
+ call called come came go went get got give gave take took make made
72
+ see saw look looked think thought know knew say said tell told ask asked
73
+ want wanted need needed try tried help helped work worked feel felt
74
+ find found keep kept leave left put set start started stop stopped
75
+ remember remembered learn learned teach taught write wrote read
76
+ everyone everybody someone somebody anyone anybody nobody everything
77
+ something anything
78
+ yes yeah ok okay maybe perhaps well now today tomorrow yesterday
79
+ im ive ill id dont cant wont didnt isnt aint thats theres whats
80
+ as than instead unless whether either neither plus versus etc
81
+ getting making looking thinking talking playing living walking running
82
+ sitting standing growing learning moving trying using
83
+ back away together alone everywhere somewhere anywhere nowhere
84
+
85
+ # `may`, `march` and `august` are ordinary words too, and they are NOT here:
86
+ # they live in the sibling file, because a mid-sentence `May` is a month far
87
+ # more often than it is a modal. The veto is the union, so candidate generation
88
+ # is unchanged either way; what moves is whether the capital counts as evidence
89
+ # that the writer is sloppy, and on a month it does not.
90
+
91
+ # >>> generated inflections — written by `python -m vicary_build lexicon`
92
+ # Bare plurals of the words above, so a capitalised `Sets` or `Parties` is
93
+ # vetoed by the same lookup its singular is. Hand-written pairs used to do this
94
+ # and only for the 35 somebody thought of.
95
+ #
96
+ # Two subtractions are applied here and neither is optional. A form borne as an
97
+ # American surname is dropped, because a stop word puts every family bearing it
98
+ # beyond the redactor (`may` would claim `Mays`, `will` would claim `Wills`).
99
+ # A form that is a common given name is dropped for a sharper reason: a stop
100
+ # word wins over the given-name tier, so `we` -> `wes` would stop redacting a
101
+ # child called Wes. Plurals still written out by hand above are the ones a
102
+ # subtraction removes from here — they are load-bearing, not leftovers.
103
+ #
104
+ # Regenerate with `python -m vicary_build lexicon`; do not edit below by hand.
105
+ abouts aboves acrosses afters agains againsts aints alones alongs alsos
106
+ althoughs alwayses amongs ands anies ans anybodies anyones anythings
107
+ anywheres arounds ases askeds asks ats aways becauses beens befores beings
108
+ belows bes besides betweens beyonds boths buts calleds calls cames cans
109
+ cants coulds didnts dids doeses doings donts durings eithers elses etcs
110
+ everies everybodies everyones everythings everywheres feels fews finallies
111
+ finds froms gaves gets gives gos gots growings hads hases havings helpeds
112
+ hences hims hises howevers ids ifs ills ins insteads intos ises isnts justs
113
+ keeps kepts knews knows learneds learnings learns leaves lefts lets lookeds
114
+ lookings looks makes maybes mights mosts movings muches musts myselfs
115
+ neededs neithers nobodies nos nothings nots nowheres nows offs oftens okays
116
+ onces onlies ons ontos ourselveses outs perhapses playings pluses puts
117
+ quites rarelies rathers reads reallies remembereds remembers runnings saids
118
+ saws says sets severals shalls shes shoulds sinces sittings somebodies
119
+ someones somethings sometimeses somewheres standings starteds stoppeds
120
+ suches talkings taughts teaches tells thans thems thens therefores thes
121
+ theses thinkings thinks thises thoses thoughs thoughts throughs thuses
122
+ todays togethers tolds tomorrows toos trieds tries tryings unders unlesses
123
+ untils ups uses usings usuallies veries versuses walkings wanteds wants
124
+ wases wents weres whenevers whens whethers whiches whoms whos whoses withins
125
+ withouts wonts workeds woulds writes wrotes yeahs yeses yesterdays yets yous
126
+ # <<< end generated inflections
@@ -0,0 +1,109 @@
1
+ #!lexicon 1
2
+ #!list stop_words_sometimes_capitalised 295
3
+ # Ordinary words that must never become name candidates, but that correct
4
+ # English does capitalise mid-sentence.
5
+ #
6
+ # HALF A LIST. The stoplist every front door vetoes candidates against is this
7
+ # file UNION `stop_words_never_capitalised.txt`, and neither half is the
8
+ # stoplist on its own. Read that file's header for the split and the leak that
9
+ # forced it; in one line: the veto wants every stop word, and the
10
+ # sloppy-capitaliser signal wants only the never-capitalised half, because
11
+ # "in July" and "my Dad" are correct English and told us nothing about the
12
+ # writer.
13
+ #
14
+ # Three kinds of word are here, and the test for all three is the same — is
15
+ # there a correct English sentence with this word capitalised in the middle of
16
+ # it?
17
+ #
18
+ # * **Nouns and adjectives and numerals.** English capitalises them inside
19
+ # proper names: "Lincoln School", "Great Lakes", "First Baptist Church",
20
+ # "New Year's Day". `School` fired the signal on four NWP papers and every
21
+ # one of them was a school's name.
22
+ # * **Months, weekdays and honorifics.** Always capitalised. `July` is what
23
+ # suppressed `Alvarez` in "We stayed with the Alvarez family in July."
24
+ # * **Named categories that identify nobody** — holidays and observances,
25
+ # languages, nationalities and ethnic groups, religions and their scriptures.
26
+ # These are on the stoplist at all because `English` was masked in a 34-word
27
+ # paper by an English-language learner writing about learning English.
28
+ #
29
+ # Two rules govern what may be added to the named-category block, and both are
30
+ # load-bearing.
31
+ #
32
+ # 1. Nothing that `is_common_given_name` already carries. Easter, Valentine,
33
+ # German, Latina, Christian, Jesus and Quran were all proposed and all
34
+ # excluded on that test: a stop word wins over the given-name tier, so
35
+ # listing one of those would stop redacting a real child called Christian.
36
+ # 2. Priced before adding, not after. The 50 category words below claim 138,506
37
+ # American surname bearers between them — 0.072% of the census population,
38
+ # against the 1.25% ceiling the bare-surname exposure gate enforces. French
39
+ # (63,991) and English (46,393) are most of it. Measured 2026-09-10;
40
+ # re-measure before extending, because the cheap-looking additions are the
41
+ # common ones.
42
+ #
43
+ # Scope of that block is deliberately narrow. Public FIGURES, PLACES and WORKS
44
+ # belong in the gazetteer, which already carries `Che Guevara` and `Australia`;
45
+ # settlements are excluded from it on purpose and stay excluded. This list is
46
+ # only for the categories no gazetteer tier models at all.
47
+ #
48
+ # Same format and the same bias as the sibling file: singulars only, plurals
49
+ # generated at the bottom by `python -m vicary_build lexicon`, count asserted on
50
+ # read.
51
+ another other same different new old good bad better best worst
52
+ great big small long short high low young happy sad hard easy
53
+ one two three four five six seven eight nine ten hundred thousand million
54
+ next last first second third
55
+ right wrong true false sure certain important special favorite
56
+ people person thing time day year week month hour minute
57
+ school class teacher student friend family home house mom dad mother father
58
+ parent brother sister grandma grandpa
59
+ life world way place part kind sort lot
60
+ january february march april may june july august september october
61
+ november december monday tuesday wednesday thursday friday saturday sunday
62
+ mr mrs ms dr pm usa u.s tv
63
+
64
+ # Plurals the generated region below cannot carry. Each is also an American
65
+ # surname, so the census veto drops it — and a stop word that quietly stops
66
+ # existing is a recall regression with no diff to read. Written out by hand for
67
+ # that reason, not as a leftover: check
68
+ # `test_the_built_list_still_carries_every_word_it_used_to` before touching one.
69
+ brothers days friends schools times ways weeks years
70
+
71
+ # The named categories. See rules 1 and 2 in the header before adding one.
72
+ christmas halloween thanksgiving hanukkah passover ramadan eid diwali
73
+ kwanzaa valentines
74
+ english spanish french chinese japanese korean italian russian arabic
75
+ hindi portuguese vietnamese greek polish irish scottish
76
+ hispanic latino asian african american european mexican
77
+ canadian indian native jewish muslim catholic protestant buddhist
78
+ hindu nazi
79
+ god bible koran torah allah
80
+
81
+ # >>> generated inflections — written by `python -m vicary_build lexicon`
82
+ # Bare plurals of the words above, so a capitalised `Sets` or `Parties` is
83
+ # vetoed by the same lookup its singular is. Hand-written pairs used to do this
84
+ # and only for the 35 somebody thought of.
85
+ #
86
+ # Two subtractions are applied here and neither is optional. A form borne as an
87
+ # American surname is dropped, because a stop word puts every family bearing it
88
+ # beyond the redactor (`may` would claim `Mays`, `will` would claim `Wills`).
89
+ # A form that is a common given name is dropped for a sharper reason: a stop
90
+ # word wins over the given-name tier, so `we` -> `wes` would stop redacting a
91
+ # child called Wes. Plurals still written out by hand above are the ones a
92
+ # subtraction removes from here — they are load-bearing, not leftovers.
93
+ #
94
+ # Regenerate with `python -m vicary_build lexicon`; do not edit below by hand.
95
+ africans allahs americans anothers aprils arabics asians augusts bads bests
96
+ bigs buddhists canadians catholics certains chineses christmases classes
97
+ dads decembers differents diwalis drs easies eids eights englishes europeans
98
+ falses families fathers favorites februaries firsts fours frenches fridays
99
+ gods grandmas grandpas greats greeks halloweens hanukkahs happies highs
100
+ hindis hindus hispanics hours houses hundreds importants indians irishes
101
+ italians januaries japaneses jewishes julies korans koreans kwanzaas lasts
102
+ latinos lifes lots marches mexicans millions minutes moms mondays months
103
+ mothers mses muslims natives nazis nexts novembers octobers others parents
104
+ parts passovers places pms polishes portugueses protestants ramadans rights
105
+ russians sads saturdays scottishes seconds septembers sevens sisters sixes
106
+ sorts spanishes specials students sundays sures teachers tens thanksgivings
107
+ things thirds thousands threes thursdays torahs trues tuesdays tvs twos usas
108
+ valentineses vietnameses wednesdays worsts wrongs
109
+ # <<< end generated inflections
@@ -119,8 +119,30 @@ module Vicary
119
119
  # load-at-import is that a host may `require "vicary"` to read
120
120
  # {Vicary::VERSION} without a vendored asset, and raising there would fail a
121
121
  # program that never redacts anything.
122
+ #
123
+ # **The union of two files, and every veto must use the union.** The split is
124
+ # not thematic: `stop_words_never_capitalised` holds the words a mid-sentence
125
+ # capital is a *mistake* on, and `stop_words_sometimes_capitalised` the ones
126
+ # correct English capitalises inside a proper name ("Lincoln School"), in a
127
+ # date ("in July") or as a nationality. Only a document-level *signal* needs
128
+ # that distinction; candidate generation needs all 794 and always did.
122
129
  def self.stop_words
123
- @stop_words ||= Lexicon.load("stop_words")
130
+ @stop_words ||= Lexicon.stop_words
131
+ end
132
+
133
+ # The half of the stoplist a mid-sentence capital is never orthographic on.
134
+ # Its own list because reading the whole stoplist for that question counts
135
+ # "in July" as evidence that the writer capitalises sloppily, which it is
136
+ # not — see the file header in `asset/lexicon/`.
137
+ def self.never_capitalised
138
+ @never_capitalised ||= Lexicon.load(Lexicon::NEVER_CAPITALISED)
139
+ end
140
+
141
+ # The other half: stop words English does capitalise mid-sentence — months,
142
+ # weekdays, honorifics, nationalities, and the nouns and adjectives that sit
143
+ # inside proper names.
144
+ def self.sometimes_capitalised
145
+ @sometimes_capitalised ||= Lexicon.load(Lexicon::SOMETIMES_CAPITALISED)
124
146
  end
125
147
 
126
148
  # Contraction and possessive tails. `[A-Z][A-Za-z'’]*` matches "I'm" as one
@@ -383,6 +405,14 @@ module Vicary
383
405
  # document level, and that is what `INCONSISTENT` is for.
384
406
  MARKS_PROPER_NOUNS_MIN = 2
385
407
 
408
+ # How many ordinary words a document must capitalise mid-sentence before its
409
+ # capitals stop counting as testimony. One is enough, and the reason it is
410
+ # not a rate: a word on the never-capitalised half of the stoplist is not a
411
+ # name AND is not a word English capitalises, so a capital on one is neither
412
+ # orthographic nor ambiguous. A floor of one is only defensible while that
413
+ # list stays that clean, which is what the sibling lexicon is for.
414
+ STRAY_CAPITALS_MIN = 1
415
+
386
416
  # A sentence opening on a lower-case letter, which is the writer telling us
387
417
  # directly that they are not keeping standard capitalisation. Matched at the
388
418
  # start of the text as well as after a sentence break.
@@ -645,9 +675,32 @@ module Vicary
645
675
  end
646
676
 
647
677
  # Whether `token` is an ordinary word that must never become a candidate.
678
+ #
679
+ # The one place inflection is folded at runtime, and it folds exactly one
680
+ # thing: a trailing clitic, so `Nazi's` is looked up as `nazi`. Plurals are
681
+ # NOT folded here — they are written into the asset by
682
+ # `python -m vicary_build lexicon`, which drops any form an American bears
683
+ # as a surname before emitting it, so all three front doors inherit the
684
+ # behaviour from the same bytes instead of implementing it three times.
648
685
  def stop?(token)
649
- word = without_clitic(strip(token.downcase, ".,"))
650
- stop_words.include?(strip(word, "'’"))
686
+ stop_words.include?(fold(token))
687
+ end
688
+
689
+ # `token` as the stoplists are keyed: lower-cased, clitic and edge
690
+ # punctuation off. Extracted so the two lookups cannot fold differently.
691
+ def fold(token)
692
+ strip(without_clitic(strip(token.downcase, ".,")), "'’")
693
+ end
694
+
695
+ # Whether a capital on `token` mid-sentence is a mistake rather than a
696
+ # construction — the narrow half of the stoplist.
697
+ #
698
+ # The question {.capitalises_ordinary_words?} needs and {.stop?} does not
699
+ # answer: every word here is a stop word, but a stop word may be a month, a
700
+ # nationality or the noun in "Lincoln School", and English capitalises all
701
+ # three correctly.
702
+ def never_capitalised?(token)
703
+ never_capitalised.include?(fold(token))
651
704
  end
652
705
 
653
706
  # Words split on whitespace, the way Python's bare `str.split()` does.
@@ -1095,6 +1148,82 @@ module Vicary
1095
1148
  !corroborated?(tokens, written_as_a_capital, is_given)
1096
1149
  end
1097
1150
 
1151
+ # Whether this document capitalises words that cannot be names.
1152
+ #
1153
+ # Counts mid-sentence capitals landing on the **never-capitalised** half of
1154
+ # the stoplist, which is a narrower question than {.stop?} answers and the
1155
+ # reason that list is a file of its own.
1156
+ #
1157
+ # It used to read the whole stoplist, and that was wrong in a way no
1158
+ # threshold fixes. The stoplist carries months, weekdays, honorifics,
1159
+ # nationalities, religions and ordinary nouns — `English` is on it because
1160
+ # a 34-word paper by an English-language learner had the word masked — and
1161
+ # every one of those is *correctly* capitalised. A document writing "in
1162
+ # July" tripped this having told us nothing, which suppressed `Alvarez` in
1163
+ # "We stayed with the Alvarez family in July."
1164
+ #
1165
+ # Unlike `marks_proper_nouns?`, which counts every mid-sentence capital and
1166
+ # therefore counts the names too, this cannot be satisfied by a document
1167
+ # that simply names a lot of people. Headings are excluded because title
1168
+ # case capitalises every word in one.
1169
+ def capitalises_ordinary_words?(text, headings = [])
1170
+ count = 0
1171
+ each_match(text, MID_SENTENCE_CAP) do |m|
1172
+ word = m[1]
1173
+ next unless never_capitalised?(word)
1174
+
1175
+ start = m.begin(1)
1176
+ finish = start + word.length
1177
+ next if headings.any? { |h_start, h_end| start < h_end && finish > h_start }
1178
+
1179
+ count += 1
1180
+ end
1181
+ count >= STRAY_CAPITALS_MIN
1182
+ end
1183
+
1184
+ # The mid-sentence guard: drop a lone capital a sloppy capitaliser chose.
1185
+ #
1186
+ # The mirror of {.suppressed_as_an_unevidenced_capital?}, and it exists
1187
+ # because that rule guards the smaller hole. A sentence-initial capital is
1188
+ # orthographically required, so it proves nothing — that is the rule
1189
+ # already there, and on the NWP corpus it is **4 of 66** false-positive
1190
+ # spans. A *mid-sentence* capital was taken as sufficient evidence on its
1191
+ # own, and that is **41 of 66**.
1192
+ #
1193
+ # So a mid-sentence capital stops being self-sufficient, but **only in a
1194
+ # document that has shown its capitals are unreliable** — see
1195
+ # {.capitalises_ordinary_words?}. In every other document nothing changes.
1196
+ #
1197
+ # Two channels can still keep the span, and the document's own
1198
+ # capitalisation is deliberately not one of them: `written_as_a_capital`
1199
+ # *is* the mid-sentence capital, so consulting it here would be the span
1200
+ # vouching for itself. What is left is evidence from outside the document —
1201
+ # the given-name tier, and a first-person relation in the local context
1202
+ # ("my friend Cade"), which is testimony that the token names a person
1203
+ # whatever its case.
1204
+ #
1205
+ # **The measured weakness, which is an equity exposure and not just a
1206
+ # coverage one.** Of the true catches this leaves intact on the NWP corpus,
1207
+ # three — `Amy`, `Barry`, `Whitney` — survive on the given-name tier alone.
1208
+ # Given-name coverage is systematically thinner for less common and
1209
+ # non-Anglo names, so the population this rule is most likely to leak is
1210
+ # not a random sample of children.
1211
+ def suppressed_as_a_stray_mid_sentence_capital?(tokens, start, finish, text, starts,
1212
+ is_given)
1213
+ return false unless tokens.length == 1
1214
+ # A sentence-initial capital belongs to the other rule, which already
1215
+ # consults a channel this one must not.
1216
+ return false if starts.any? { |s| s <= start && start <= s + 2 }
1217
+
1218
+ stripped = strip(tokens[0].downcase, ".,'’")
1219
+ return false if is_given.call(stripped)
1220
+
1221
+ folded = without_clitic(stripped)
1222
+ return false if folded != stripped && is_given.call(folded)
1223
+
1224
+ !names_someone_in_the_writers_life?(text, start, finish)
1225
+ end
1226
+
1098
1227
  # ---------------------------------------------------------------------
1099
1228
  # Titles, and the relations that override them
1100
1229
  # ---------------------------------------------------------------------
@@ -1576,6 +1705,7 @@ module Vicary
1576
1705
  settlement = options[:settlement]
1577
1706
  headings_are_orthographic = options.fetch(:headings_are_orthographic, true)
1578
1707
  title_relation_refusal = options.fetch(:title_relation_refusal, true)
1708
+ mid_sentence_corroboration = options.fetch(:mid_sentence_corroboration, true)
1579
1709
 
1580
1710
  blocked = each_match(text, PROTECTED).map { |m| [m.begin(0), m.begin(0) + m[0].length] }
1581
1711
  starts = sentence_starts(text)
@@ -1610,6 +1740,10 @@ module Vicary
1610
1740
  end
1611
1741
 
1612
1742
  written_as_a_capital = mid_sentence_capitals(text, starts, headings)
1743
+ # A property of the whole document, read once, for the same reason
1744
+ # `habit` is: two call sites computing it separately could disagree.
1745
+ stray_capitals = mid_sentence_corroboration && !given_name.nil? &&
1746
+ capitalises_ordinary_words?(text, headings)
1613
1747
 
1614
1748
  out = []
1615
1749
  each_match(text, CANDIDATE_RE) do |m|
@@ -1638,6 +1772,15 @@ module Vicary
1638
1772
  next
1639
1773
  end
1640
1774
 
1775
+ # ...and the other half of the same question, for the capital the
1776
+ # rule above reads as evidence. Only in a document that has shown its
1777
+ # capitals are worth less than that.
1778
+ if stray_capitals &&
1779
+ suppressed_as_a_stray_mid_sentence_capital?(run, start, start + joined.length,
1780
+ text, starts, given_name)
1781
+ next
1782
+ end
1783
+
1641
1784
  # A *trailing* apostrophe is the closing quote, not part of the name.
1642
1785
  # The candidate pattern treats `'` as a name character so O'Brien
1643
1786
  # survives, which also means "words like 'Terrence'" arrives as
data/lib/vicary/corpus.rb CHANGED
@@ -104,6 +104,10 @@ module Vicary
104
104
 
105
105
  Metrics = Struct.new(
106
106
  :essays, :recall_held_out, :recall_held_out_passed, :recall_held_out_total,
107
+ # Every REDACT span, not only the held-out ones. Held-out recall is blind
108
+ # to a span the detector was SHOWN, and a suppression rule that drops one
109
+ # leaves held-out recall at 100%.
110
+ :recall_all, :recall_all_passed, :recall_all_total,
107
111
  :over_fire_spans_per_essay, :over_fire_spans_total,
108
112
  :asap_rewrites_per_essay, :latency_p50_ms, :latency_p95_ms,
109
113
  :latency_pooled_median_ms,
@@ -485,6 +489,8 @@ module Vicary
485
489
 
486
490
  held_out = outcomes.select { |o| o.held_out && o.verdict != "keep" }
487
491
  passed = held_out.count(&:passed)
492
+ all_redact = outcomes.reject { |o| o.verdict == "keep" }
493
+ all_passed = all_redact.count(&:passed)
488
494
  sorted = latencies.sort
489
495
  at = lambda do |q|
490
496
  next 0.0 if sorted.empty?
@@ -497,6 +503,9 @@ module Vicary
497
503
  recall_held_out: held_out.empty? ? 0.0 : 100.0 * passed / held_out.size,
498
504
  recall_held_out_passed: passed,
499
505
  recall_held_out_total: held_out.size,
506
+ recall_all: all_redact.empty? ? 0.0 : 100.0 * all_passed / all_redact.size,
507
+ recall_all_passed: all_passed,
508
+ recall_all_total: all_redact.size,
500
509
  over_fire_spans_per_essay: cases.empty? ? 0.0 : over_fire.to_f / cases.size,
501
510
  over_fire_spans_total: over_fire,
502
511
  asap_rewrites_per_essay: cases.empty? ? 0.0 : rewrites.to_f / cases.size,
data/lib/vicary/gates.rb CHANGED
@@ -294,7 +294,8 @@ module Vicary
294
294
  # a caller that wants those gates supplies the number, and one that does
295
295
  # not gets NOT MEASURED rather than a load.
296
296
  def measure(spec, gate_spec, asset_entries: nil, bare_surname_exposure: nil,
297
- held_out_recall_carrier: nil, over_fire_per_essay: nil,
297
+ held_out_recall_carrier: nil, all_span_recall_carrier: nil,
298
+ over_fire_per_essay: nil,
298
299
  latency_regression_pct: nil, latency_regression_detail: nil,
299
300
  corpus_id: nil)
300
301
  outcomes = []
@@ -365,6 +366,14 @@ module Vicary
365
366
  "#{round3(held_out_recall_carrier)}% of held-out REDACT spans in carrier essays"
366
367
  end,
367
368
  },
369
+ "all_span_recall_carrier" => {
370
+ value: all_span_recall_carrier,
371
+ detail: if all_span_recall_carrier.nil?
372
+ no_corpus
373
+ else
374
+ "#{round3(all_span_recall_carrier)}% of ALL REDACT spans in carrier essays"
375
+ end,
376
+ },
368
377
  "over_fire_prose" => {
369
378
  value: over_fire_per_essay,
370
379
  detail: if over_fire_per_essay.nil?
@@ -46,6 +46,25 @@ module Vicary
46
46
  # Filename suffix. Named so a second list costs a file rather than a refactor.
47
47
  SUFFIX = ".txt"
48
48
 
49
+ # The two files whose union is the stoplist. Named here rather than at each
50
+ # call site because the *veto* needs both and exactly one consumer needs one
51
+ # — a document-level signal that reads a mid-sentence capital as testimony
52
+ # about the writer, and that must not read "in July" that way. A call site
53
+ # free to load one half is free to narrow the veto by accident, which makes
54
+ # the redactor more aggressive: privacy-safe to look at, prose-corrupting in
55
+ # fact, and invisible to any check that only asks whether something was
56
+ # masked.
57
+ NEVER_CAPITALISED = "stop_words_never_capitalised"
58
+ SOMETIMES_CAPITALISED = "stop_words_sometimes_capitalised"
59
+ STOP_WORD_LISTS = [NEVER_CAPITALISED, SOMETIMES_CAPITALISED].freeze
60
+
61
+ # Every word that must never become a name candidate: both halves, unioned.
62
+ # Word for word the list that shipped as a single `stop_words.txt` through
63
+ # 0.2.9.
64
+ def self.stop_words
65
+ STOP_WORD_LISTS.map { |name| load(name) }.reduce(:|)
66
+ end
67
+
49
68
  # A lexicon is absent, unreadable, or not the shape this reader understands.
50
69
  #
51
70
  # Its own class rather than a bare RuntimeError for the same reason
@@ -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.8"
9
+ VERSION = "0.2.11"
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vicary
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.8
4
+ version: 0.2.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Blake Thomas
@@ -25,7 +25,8 @@ files:
25
25
  - README.md
26
26
  - assets/MANIFEST.json
27
27
  - assets/notability.txt.gz
28
- - assets/stop_words.txt
28
+ - assets/stop_words_never_capitalised.txt
29
+ - assets/stop_words_sometimes_capitalised.txt
29
30
  - lib/vicary.rb
30
31
  - lib/vicary/asset.rb
31
32
  - lib/vicary/candidates.rb
@@ -1,63 +0,0 @@
1
- #!lexicon 1
2
- #!list stop_words 421
3
- # Ordinary words that must never become name candidates.
4
- #
5
- # Language-neutral on purpose. This list is the only thing standing between
6
- # candidate generation and "mask every capitalised word", so all three front doors
7
- # have to work from the same 421 words — a stoplist transliterated by hand into a
8
- # second language is a second detector wearing the first one's name, and the
9
- # difference shows up as prose corruption in one language and not the others,
10
- # which no parity check on masked output would catch.
11
- #
12
- # Skewed toward over-inclusion deliberately. A missed name is one span and shows
13
- # up in the recall number; a wrongly-masked common word corrupts every essay that
14
- # uses it and shows up nowhere unless somebody reads the prose.
15
- #
16
- # Format: `#!` lines are directives, `#` lines are comments, and every other line
17
- # contributes whitespace-separated words. The count on the `#!list` directive is
18
- # the number of DISTINCT words after case-folding, and each front door asserts it
19
- # against what it parsed. That is not ceremony: a short read here silently makes
20
- # the redactor MORE aggressive, which looks privacy-safe and passes any check that
21
- # only asks whether something was masked.
22
- #
23
- # A word appearing twice is not an error — the groupings below are thematic and
24
- # overlap ("else", "may", "us"), and enforcing uniqueness in the source would make
25
- # the list harder to read for no benefit.
26
- a an the this that these those there here it its it's
27
- i me my mine myself we us our ours ourselves you your yours
28
- he him his she her hers they them their theirs who whom whose which what
29
- and or but so because although though however therefore thus hence yet
30
- if then else when while until since before after during once whenever
31
- for from to into onto out off over under above below between among across
32
- through around about against along beside besides beyond within without
33
- at by in on up down near next last first second third finally
34
- is am are was were be been being have has had having do does did doing
35
- can could will would shall should may might must let lets
36
- not no nor none nothing never always sometimes often usually rarely
37
- all any both each every few many more most much several some such
38
- another other others same different new old good bad better best worst
39
- great big small long short high low young happy sad hard easy
40
- one two three four five six seven eight nine ten hundred thousand million
41
- also even just only really very too still again ever else quite rather
42
- call called come came go went get got give gave take took make made
43
- see saw look looked think thought know knew say said tell told ask asked
44
- want wanted need needed try tried help helped work worked feel felt
45
- find found keep kept leave left put set start started stop stopped
46
- remember remembered learn learned teach taught write wrote read
47
- everyone everybody someone somebody anyone anybody nobody everything
48
- something anything people person thing things time times day days
49
- year years week weeks month months hour hours minute minutes
50
- school schools class classes teacher teachers student students friend
51
- friends family families home house mom dad mother father parent parents
52
- brother sister sisters brothers grandma grandpa
53
- life world way ways place places part parts kind sort lot lots
54
- yes yeah ok okay maybe perhaps well now today tomorrow yesterday
55
- january february march april may june july august september october
56
- november december monday tuesday wednesday thursday friday saturday sunday
57
- mr mrs ms dr am pm usa us u.s tv
58
- im ive ill id dont cant wont didnt isnt aint thats theres whats
59
- as than instead unless whether either neither plus versus etc
60
- getting making looking thinking talking playing living walking running
61
- sitting standing growing learning moving trying using
62
- back away together alone everywhere somewhere anywhere nowhere
63
- right wrong true false sure certain important special favorite