@link-foundation/sensitive-data-sanitizer 0.21.0 → 0.21.1
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.
- package/CHANGELOG.md +6 -0
- package/docs/API.md +16 -1
- package/package.json +1 -1
- package/src/placeholder-types.js +11 -1
- package/src/placeholders.js +21 -4
- package/src/prose-words.js +1 -5
- package/src/public-technologies.js +32 -0
- package/src/public.js +3 -0
- package/src/sanitizer.js +7 -1
- package/src/strict-attributes.js +13 -1
- package/src/vocabulary.js +12 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e0e05a4: Cover 34 missing strict personal-attribute terms, preserve their typed placeholders over generic prose names, cache caller vocabulary and cue sets, avoid unnecessary placeholder scans and JSON parsing, and preserve exact public technology names in Cyrillic prose.
|
|
8
|
+
|
|
3
9
|
## 0.21.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/docs/API.md
CHANGED
|
@@ -750,7 +750,10 @@ Cues ignore case and support bounded Russian noun/adjective forms; personal fiel
|
|
|
750
750
|
use normalized field names. Capitalized proper values are required after prose
|
|
751
751
|
cues. Inputs allow at most 256 nonempty strings of 128 code units per list, with
|
|
752
752
|
no line breaks/NUL. Unknown lists and malformed replacement records fail with
|
|
753
|
-
`ERR_CONFIG`. Compilation uses a bounded 64-configuration cache
|
|
753
|
+
`ERR_CONFIG`. Compilation uses a bounded 64-configuration cache and a weak
|
|
754
|
+
object-identity cache. Treat a vocabulary object as immutable after first use;
|
|
755
|
+
pass a new object to change its lists. Repeated lookups reuse compiled patterns
|
|
756
|
+
and a cue-word Set without normalizing or serializing the lists again.
|
|
754
757
|
|
|
755
758
|
`personalAttributes: ['вепс', 'старовер']` adds special-category stems to the
|
|
756
759
|
`strict` rule (it has no effect without `strict: true`). Each word matches as a
|
|
@@ -759,6 +762,18 @@ words of a multi-word entry may be separated by spaces or tabs. Matches are
|
|
|
759
762
|
reported as `PERSONAL_ATTRIBUTE` with the `strict-attribute` rule. The default
|
|
760
763
|
list is empty.
|
|
761
764
|
|
|
765
|
+
Typed special-category attributes take precedence over generic `ProperName`
|
|
766
|
+
guesses, including capitalized caller stems in Cyrillic prose. Strict coverage
|
|
767
|
+
includes the 34 additional nationality, religion, health, criminal-record,
|
|
768
|
+
political and union terms reported in issue 144, even as standalone words.
|
|
769
|
+
|
|
770
|
+
With default `publicKnowledge`, exact public technology spellings such as Python,
|
|
771
|
+
SQL, PostgreSQL, Docker, Kubernetes, React, AWS and Azure remain readable when
|
|
772
|
+
the capitalized prose detector flags them. The offline catalog includes source
|
|
773
|
+
URLs in `src/public-technologies.js`. `publicKnowledge: false` disables this
|
|
774
|
+
exception; known private values and credentials still redact, and an adjacent
|
|
775
|
+
personal name never inherits a technology's exemption.
|
|
776
|
+
|
|
762
777
|
## Callback transformations and stages
|
|
763
778
|
|
|
764
779
|
```js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-foundation/sensitive-data-sanitizer",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"description": "Offline sensitive-text sanitization with credential, multilingual personal-data and scanner union detection",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/placeholder-types.js
CHANGED
|
@@ -129,6 +129,9 @@ export function namespacePattern(format = defaultFormat, extraTypes = []) {
|
|
|
129
129
|
// Tokens of the namespace with the types each spelling can stand for.
|
|
130
130
|
export function placeholderTokens(text, pattern) {
|
|
131
131
|
const tokens = [];
|
|
132
|
+
if (!/[1-9]/u.test(text)) {
|
|
133
|
+
return tokens;
|
|
134
|
+
}
|
|
132
135
|
for (const match of text.matchAll(new RegExp(pattern.source, 'gu'))) {
|
|
133
136
|
const [part, number] = pattern.typedFirst
|
|
134
137
|
? [match[1], match[2]]
|
|
@@ -143,13 +146,20 @@ export function placeholderTokens(text, pattern) {
|
|
|
143
146
|
return tokens;
|
|
144
147
|
}
|
|
145
148
|
export function containsPlaceholder(text, pattern) {
|
|
146
|
-
return new RegExp(pattern.source, 'u').test(text);
|
|
149
|
+
return /[1-9]/u.test(text) && new RegExp(pattern.source, 'u').test(text);
|
|
147
150
|
}
|
|
148
151
|
// Tokens spelled with JSON escapes inside string literals, such as
|
|
149
152
|
// `Person-1`, decoded so that they cannot hide from the namespace check.
|
|
150
153
|
export function escapedPlaceholderTokens(text, pattern) {
|
|
151
154
|
const tokens = [];
|
|
155
|
+
// Without an escape every literal has already been checked in the raw text.
|
|
156
|
+
if (!text.includes('\\')) {
|
|
157
|
+
return tokens;
|
|
158
|
+
}
|
|
152
159
|
for (const literal of jsonStringLiterals(text)) {
|
|
160
|
+
if (!literal.includes('\\')) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
153
163
|
try {
|
|
154
164
|
tokens.push(...placeholderTokens(JSON.parse(literal), pattern));
|
|
155
165
|
} catch {
|
package/src/placeholders.js
CHANGED
|
@@ -349,10 +349,25 @@ function callPattern(findings, options) {
|
|
|
349
349
|
);
|
|
350
350
|
}
|
|
351
351
|
const keepsExisting = (options) => transforms(options)[0]?.existing === 'keep';
|
|
352
|
+
// Default tokens always contain a hyphen followed by a positive number.
|
|
353
|
+
// Escapes may conceal either character, and custom formats need the full scan.
|
|
354
|
+
function mayContainPlaceholder(text, options, escaped = false) {
|
|
355
|
+
const transform = transforms(options)[0];
|
|
356
|
+
const format =
|
|
357
|
+
options[placeholderState]?.map.format ??
|
|
358
|
+
transform?.format ??
|
|
359
|
+
transform?.map?.format ??
|
|
360
|
+
'{Type}-{n}';
|
|
361
|
+
return (
|
|
362
|
+
format !== '{Type}-{n}' ||
|
|
363
|
+
/-[1-9]/u.test(text) ||
|
|
364
|
+
(escaped && text.includes('\\'))
|
|
365
|
+
);
|
|
366
|
+
}
|
|
352
367
|
// With `existing: 'keep'` the tokens of an earlier pass stay as they are, so
|
|
353
368
|
// findings made only of those tokens are not replaced again.
|
|
354
369
|
export function keptPlaceholderSpans(text, findings, options) {
|
|
355
|
-
if (!keepsExisting(options)) {
|
|
370
|
+
if (!keepsExisting(options) || !mayContainPlaceholder(text, options)) {
|
|
356
371
|
return [];
|
|
357
372
|
}
|
|
358
373
|
return placeholderTokens(text, callPattern(findings, options));
|
|
@@ -399,8 +414,10 @@ export function preparePlaceholderText(text, findings, options) {
|
|
|
399
414
|
if (!state || !hasPlaceholders(options)) {
|
|
400
415
|
return;
|
|
401
416
|
}
|
|
402
|
-
const pattern =
|
|
403
|
-
|
|
417
|
+
const pattern = mayContainPlaceholder(text, options, true)
|
|
418
|
+
? callPattern(findings, options)
|
|
419
|
+
: undefined;
|
|
420
|
+
if (pattern && state.existing === 'keep') {
|
|
404
421
|
// New values are numbered after the highest kept number of their type.
|
|
405
422
|
for (const token of [
|
|
406
423
|
...placeholderTokens(text, pattern),
|
|
@@ -413,7 +430,7 @@ export function preparePlaceholderText(text, findings, options) {
|
|
|
413
430
|
);
|
|
414
431
|
}
|
|
415
432
|
}
|
|
416
|
-
} else if (namespaceCollision(text, pattern)) {
|
|
433
|
+
} else if (pattern && namespaceCollision(text, pattern)) {
|
|
417
434
|
throw failure('ERR_PLACEHOLDER_COLLISION');
|
|
418
435
|
}
|
|
419
436
|
state.people = [
|
package/src/prose-words.js
CHANGED
|
@@ -74,10 +74,6 @@ function sentenceStart(text, start) {
|
|
|
74
74
|
function nextWordLowercase(text, end) {
|
|
75
75
|
return /^[ \t]+\p{Ll}/u.test(text.slice(end, end + 4));
|
|
76
76
|
}
|
|
77
|
-
function vocabularyCues(options) {
|
|
78
|
-
const lists = compiledVocabulary(options?.vocabulary)?.lists;
|
|
79
|
-
return lists ? [...lists.roles, ...lists.personalLabels] : [];
|
|
80
|
-
}
|
|
81
77
|
const defaultCues = new Set(
|
|
82
78
|
[...defaultVocabulary.roles, ...defaultVocabulary.personalLabels].map(fold)
|
|
83
79
|
);
|
|
@@ -89,7 +85,7 @@ export function isProseCue(word, options) {
|
|
|
89
85
|
return (
|
|
90
86
|
(cues.has(lower) ||
|
|
91
87
|
defaultCues.has(lower) ||
|
|
92
|
-
|
|
88
|
+
compiledVocabulary(options?.vocabulary)?.cues.has(lower)) &&
|
|
93
89
|
!isNameForm(word)
|
|
94
90
|
);
|
|
95
91
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Exact spellings only: a technology token never exempts a surrounding name,
|
|
2
|
+
// caller-specified private value, credential, URL or identifier.
|
|
3
|
+
export const publicTechnologies = [
|
|
4
|
+
['Python', 'https://www.python.org/'],
|
|
5
|
+
['SQL', 'https://www.iso.org/standard/76583.html'],
|
|
6
|
+
['JavaScript', 'https://developer.mozilla.org/en-US/docs/Web/JavaScript'],
|
|
7
|
+
['TypeScript', 'https://www.typescriptlang.org/'],
|
|
8
|
+
['Java', 'https://dev.java/'],
|
|
9
|
+
['C++', 'https://isocpp.org/'],
|
|
10
|
+
['C#', 'https://learn.microsoft.com/en-us/dotnet/csharp/'],
|
|
11
|
+
['PHP', 'https://www.php.net/'],
|
|
12
|
+
['Kotlin', 'https://kotlinlang.org/'],
|
|
13
|
+
['PostgreSQL', 'https://www.postgresql.org/'],
|
|
14
|
+
['MySQL', 'https://www.mysql.com/'],
|
|
15
|
+
['SQLite', 'https://sqlite.org/'],
|
|
16
|
+
['MongoDB', 'https://www.mongodb.com/'],
|
|
17
|
+
['Redis', 'https://redis.io/'],
|
|
18
|
+
['Elasticsearch', 'https://www.elastic.co/elasticsearch'],
|
|
19
|
+
['Docker', 'https://www.docker.com/'],
|
|
20
|
+
['Kubernetes', 'https://kubernetes.io/'],
|
|
21
|
+
['React', 'https://react.dev/'],
|
|
22
|
+
['Vue', 'https://vuejs.org/'],
|
|
23
|
+
['Angular', 'https://angular.dev/'],
|
|
24
|
+
['Django', 'https://www.djangoproject.com/'],
|
|
25
|
+
['Flask', 'https://flask.palletsprojects.com/'],
|
|
26
|
+
['Node.js', 'https://nodejs.org/'],
|
|
27
|
+
['TensorFlow', 'https://www.tensorflow.org/'],
|
|
28
|
+
['PyTorch', 'https://pytorch.org/'],
|
|
29
|
+
['AWS', 'https://aws.amazon.com/'],
|
|
30
|
+
['Azure', 'https://azure.microsoft.com/'],
|
|
31
|
+
['GCP', 'https://cloud.google.com/'],
|
|
32
|
+
].map(([value, source]) => ({ value, source }));
|
package/src/public.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { isIP } from 'node:net';
|
|
2
|
+
import { publicTechnologies } from './public-technologies.js';
|
|
2
3
|
|
|
3
4
|
// Exact public knowledge and narrowly scoped role-mail heuristics. A role name
|
|
4
5
|
// alone is never an exemption on an unknown/personal domain. Credentials and
|
|
5
6
|
// caller-specified private values always outrank these automatic exceptions.
|
|
6
7
|
export const publicKnowledge = {
|
|
8
|
+
PROPER_NAME: publicTechnologies,
|
|
9
|
+
URL: publicTechnologies.filter((entry) => entry.value === 'Node.js'),
|
|
7
10
|
PERSON: [
|
|
8
11
|
['Albert Einstein', 'Q937'],
|
|
9
12
|
['Marie Curie', 'Q7186'],
|
package/src/sanitizer.js
CHANGED
|
@@ -870,11 +870,17 @@ function preferredType(current, candidate, options) {
|
|
|
870
870
|
if (options.transformations?.[candidate] || candidate === 'PASSPORT_MRZ') {
|
|
871
871
|
return true;
|
|
872
872
|
}
|
|
873
|
+
return preferredSpecificType(current, candidate);
|
|
874
|
+
}
|
|
875
|
+
function preferredSpecificType(current, candidate) {
|
|
873
876
|
const broad = (type) =>
|
|
874
877
|
['ID', 'PASSPORT'].includes(type) ||
|
|
875
878
|
(/PASSPORT/.test(type) &&
|
|
876
879
|
!['PASSPORT_MRZ', 'PASSPORT_NUMBER'].includes(type));
|
|
877
|
-
return
|
|
880
|
+
return (
|
|
881
|
+
(current === 'PROPER_NAME' && candidate === 'PERSONAL_ATTRIBUTE') ||
|
|
882
|
+
(broad(current) && isIdentityType(candidate) && !broad(candidate))
|
|
883
|
+
);
|
|
878
884
|
}
|
|
879
885
|
export function redactResult(
|
|
880
886
|
text,
|
package/src/strict-attributes.js
CHANGED
|
@@ -18,6 +18,11 @@ const softNoun = '(?:а|у|ом|е|и|ей|ам|ами|ах)?';
|
|
|
18
18
|
const ecNoun = '(?:ец|ц(?:а|у|ем|е|ы|ев|ам|ами|ах))';
|
|
19
19
|
const femaleNoun = '(?:а|и|е|у|ой)';
|
|
20
20
|
const nationalities = [
|
|
21
|
+
`кабардин${ecNoun}|кабардинк${femaleNoun}`,
|
|
22
|
+
`казашк${femaleNoun}`,
|
|
23
|
+
`мордвин${hardNoun}|мордовк${femaleNoun}`,
|
|
24
|
+
`удмурт${hardNoun}|удмуртк${femaleNoun}`,
|
|
25
|
+
`молдаванин(?:а|у|ом|е)?|молдаван${hardNoun}|молдаванк${femaleNoun}`,
|
|
21
26
|
`узбек${softNoun}|узбечк${femaleNoun}`,
|
|
22
27
|
`таджик${softNoun}|таджичк${femaleNoun}`,
|
|
23
28
|
`(?:киргиз|кыргыз)${hardNoun}|(?:киргиз|кыргыз)к${femaleNoun}`,
|
|
@@ -41,6 +46,12 @@ const nationalities = [
|
|
|
41
46
|
`уйгур${hardNoun}|уйгурк${femaleNoun}`,
|
|
42
47
|
].join('|');
|
|
43
48
|
const believerNoun = `(?:а|у|ом|е|ы|ов|ам|ами|ах|к${femaleNoun}|кой)?`;
|
|
49
|
+
// These terms are sensitive even without an adjacent personal cue. Keep
|
|
50
|
+
// bounded endings and whole-word boundaries, as for the existing categories.
|
|
51
|
+
const followUpVocabulary = new RegExp(
|
|
52
|
+
`(?<![\\p{L}\\p{N}_])(?:иудаизм${hardNoun}|старовер${believerNoun}|эпилептик${softNoun}|паралич${softNoun}|наркомани[\\p{L}]{1,4}|профсоюз${hardNoun}|коммунист${believerNoun}|uzbek|tajik|chechen|romani|gypsy|latino|latina|baptist|aids|bipolar|adhd|wheelchair|lgbt|convicted|felony|trade[ \\t]+union)(?![\\p{L}\\p{N}_])`,
|
|
53
|
+
'giu'
|
|
54
|
+
);
|
|
44
55
|
const specialVocabulary = new RegExp(
|
|
45
56
|
`(?<![\\p{L}\\p{N}_])(?:${nationalities}|(?:протестант|баптист|адвентист|индуист|кришнаит|иеговист|пятидесятник)${believerNoun}|лютеран(?:ин(?:а|у|ом|е)?|к${femaleNoun}|кой|е|ам|ами|ах)|старообряд${ecNoun}|старообрядк${femaleNoun}|свидетел[\\p{L}]{1,3}[ \\t]+иеговы|инсульт[\\p{L}]{0,3}|инфаркт[\\p{L}]{0,3}|шизофрени[\\p{L}]{1,4}|аутизм[\\p{L}]{0,3}|аутист(?:ка|ки|ке|ку|кой|а|у|ом|ы|ов)?|слабослышащ[\\p{L}]{1,4}|слабовидящ[\\p{L}]{1,4}|незряч[\\p{L}]{1,4}|дцп|колясочни(?:к${hardNoun}|ц${femaleNoun})|(?:передвига[\\p{L}]{1,5}|инвалидн[\\p{L}]{2,4})[ \\t]+(?:на[ \\t]+)?коляск[\\p{L}]{1,3}|болезн[\\p{L}]{1,3}[ \\t]+(?:паркинсона|альцгеймера)|деменци[\\p{L}]{1,3}|алкоголизм[\\p{L}]{0,2}|наркозависим[\\p{L}]{0,4}|биполярн[\\p{L}]{2,4}[ \\t]+расстройств[\\p{L}]{0,3}|(?:би|транс|пан|а)сексуал[\\p{L}]{0,6}|трансгендер[\\p{L}]{0,6}|(?:сидел[аи]?|отсидел[аи]?|осужд[её]н[аы]?|привлекал(?:ся|ась|ись)|отбывал[аи]?[ \\t]+срок)[ \\t]+(?:за|по)[ \\t]+(?:краж|грабе|разбо|убийств|мошенничеств|наркотик|хулиганств|изнасиловани|взятк|стать)[\\p{L}]{0,4}(?:[ \\t]+\\d{1,3}(?:\\.\\d)?)?|(?:сидел[аи]?|отсидел[аи]?)[ \\t]+(?:в[ \\t]+)?(?:тюрьм[\\p{L}]{1,2}|колони[\\p{L}]{1,2}|срок)|бывш[\\p{L}]{2,3}[ \\t]+заключ[её]нн[\\p{L}]{2,3}|член[\\p{L}]{0,3}[ \\t]+профсоюз[\\p{L}]{0,3}|состо(?:ит|ял[аи]?|ю|ят)[ \\t]+в[ \\t]+профсоюзе|профсоюзн(?:ый|ая|ого|ой)[ \\t]+(?:активист|деятел|лидер)[\\p{L}]{0,3}|autism|autistic|schizophreni[ac]|transgender|transsexual|pansexual|asexual|wheelchair[ \\t]+users?|uses[ \\t]+a[ \\t]+wheelchair|in[ \\t]+a[ \\t]+wheelchair|(?:hearing|visually)[ \\t-]+impaired|had[ \\t]+a[ \\t]+(?:stroke|heart[ \\t]+attack)|dementia|alzheimer'?s|parkinson'?s|bipolar[ \\t]+disorder|alcoholism|trade[ \\t]+union[ \\t]+member(?:ship)?|member[ \\t]+of[ \\t]+(?:a|the)[ \\t]+(?:trade|labou?r)[ \\t]+union|convicted[ \\t]+(?:of|for)[ \\t]+[\\p{L}]{3,32}|served[ \\t]+(?:time[ \\t]+(?:in|for)|a[ \\t]+(?:prison[ \\t]+)?sentence)|protestant|lutheran|mormon|hindu|sikh|jehovah'?s[ \\t]+witness)(?![\\p{L}\\p{N}_])`,
|
|
46
57
|
'giu'
|
|
@@ -49,7 +60,7 @@ const categoryCue =
|
|
|
49
60
|
/(?<![\p{L}\p{N}_])(?:по[ \t]+(?:национальности|происхождению)|национальность|этничность|вероисповедание|религия|диагноз|исповеду(?:ет|ют|ю|ешь)|боле(?:ет|ют|ю|ешь)|страда(?:ет|ют|ю|ешь)[ \t]+от|на[ \t]+уч[её]те[ \t]+у|diagnosed[ \t]+with|suffers[ \t]+from|nationality|ethnicity|religion|diagnosis)(?:["']?[ \t]*[:=][ \t]*|[ \t]+)["']?([\p{L}][\p{L}\p{M}-]{0,63})(?![\p{L}\p{M}\p{N}_])/giu;
|
|
50
61
|
// Words that are also languages, scripts or styles need a personal context.
|
|
51
62
|
const ambiguous =
|
|
52
|
-
/(?<![\p{L}\p{N}_])(?:disabled|single|man|men|russian|asian|
|
|
63
|
+
/(?<![\p{L}\p{N}_])(?:disabled|single|man|men|russian|asian|azerbaijani|korean|chinese|georgian|kazakh|kyrgyz|turkmen|uyghur|roma|union[ \t]+member)(?![\p{L}\p{N}_])/giu;
|
|
53
64
|
|
|
54
65
|
function personalContext(text, match) {
|
|
55
66
|
const prefix = text.slice(Math.max(0, match.index - 128), match.index);
|
|
@@ -97,6 +108,7 @@ export function detectStrictAttributes(text, emit, options) {
|
|
|
97
108
|
additionalAttributes,
|
|
98
109
|
categoryForms,
|
|
99
110
|
specialVocabulary,
|
|
111
|
+
followUpVocabulary,
|
|
100
112
|
compiledVocabulary(options.vocabulary)?.attributes,
|
|
101
113
|
]) {
|
|
102
114
|
if (pattern) {
|
package/src/vocabulary.js
CHANGED
|
@@ -55,6 +55,7 @@ export const defaultVocabulary = Object.freeze(
|
|
|
55
55
|
)
|
|
56
56
|
);
|
|
57
57
|
const cache = new Map();
|
|
58
|
+
const identities = new WeakMap();
|
|
58
59
|
const boundary = '[\\p{L}\\p{N}_]';
|
|
59
60
|
const name =
|
|
60
61
|
'[\\p{Lu}][\\p{L}\\p{M}.-]{1,47}(?:[ \\t]+[\\p{Lu}][\\p{L}\\p{M}.-]{1,47}){0,3}';
|
|
@@ -140,10 +141,16 @@ export function compiledVocabulary(vocabulary) {
|
|
|
140
141
|
if (vocabulary === undefined) {
|
|
141
142
|
return undefined;
|
|
142
143
|
}
|
|
144
|
+
const existing = identities.get(vocabulary);
|
|
145
|
+
if (existing) {
|
|
146
|
+
return existing;
|
|
147
|
+
}
|
|
143
148
|
const lists = normalizedLists(vocabulary);
|
|
144
149
|
const key = JSON.stringify(lists);
|
|
145
150
|
if (cache.has(key)) {
|
|
146
|
-
|
|
151
|
+
const result = cache.get(key);
|
|
152
|
+
identities.set(vocabulary, result);
|
|
153
|
+
return result;
|
|
147
154
|
}
|
|
148
155
|
const alternatives = (values) =>
|
|
149
156
|
values.length
|
|
@@ -184,11 +191,15 @@ export function compiledVocabulary(vocabulary) {
|
|
|
184
191
|
'giu'
|
|
185
192
|
);
|
|
186
193
|
result.fields = new Set(lists.personalFields.map(normalizeFieldName));
|
|
194
|
+
result.cues = new Set(
|
|
195
|
+
[...lists.roles, ...lists.personalLabels].map((s) => s.replaceAll('ё', 'е'))
|
|
196
|
+
);
|
|
187
197
|
result.attributes = attributePattern(lists.personalAttributes);
|
|
188
198
|
if (cache.size >= 64) {
|
|
189
199
|
cache.clear();
|
|
190
200
|
}
|
|
191
201
|
cache.set(key, result);
|
|
202
|
+
identities.set(vocabulary, result);
|
|
192
203
|
return result;
|
|
193
204
|
}
|
|
194
205
|
export function vocabularyField(field, options) {
|