@link-foundation/sensitive-data-sanitizer 0.19.1 → 0.19.3
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 +12 -0
- package/docs/API.md +1 -1
- package/docs/COVERAGE.md +27 -1
- package/package.json +1 -1
- package/src/document-fields.js +18 -1
- package/src/document-person-cues.js +4 -1
- package/src/index.d.ts +1 -0
- package/src/strict-attributes.js +71 -0
- package/src/strict.js +2 -12
- package/src/vocabulary.js +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.19.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6bdaf11: Recognize additional inflected special-category attributes and unfamiliar values after explicit category cues in strict mode. Require personal context for ambiguous English attributes so operational logs and JSON statuses stay intact.
|
|
8
|
+
|
|
9
|
+
## 0.19.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 4afc88d: Read `Mr`, `Ms` and `Dr` as person cues only when capitalized, so lowercase `ms` (milliseconds) and `dr` before a capitalized log word stay. Treat compound JSON keys ending in a personal field, such as `recruiterDisplayName`, `authorFullName` and `resumePdfOriginalFileName`, as person fields.
|
|
14
|
+
|
|
3
15
|
## 0.19.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/docs/API.md
CHANGED
|
@@ -17,7 +17,7 @@ Import the ESM root entry; complete declarations are in `src/index.d.ts`. Inputs
|
|
|
17
17
|
| `compileKnown(...lists)` | Copy, freeze, merge and precompile caller-known personal values |
|
|
18
18
|
| `auditGitHistory(repository, options)` | Read-only audit of local reachable text objects |
|
|
19
19
|
|
|
20
|
-
`redactions` counts merged replacement spans. `findings` can contain several detector hits for one replacement. Findings have `{start, end, type, category, rule, line, column}`; no text, quote, match, entropy score, provider details, or raw diagnostic is copied. Debugging is off by default; `debug(event)` receives value-free counts, rule timings, residual rounds and registry event metadata (see `DebugEvent` in the TypeScript declarations).
|
|
20
|
+
`redactions` counts merged replacement spans. `findings` can contain several detector hits for one replacement. Findings have `{start, end, type, category, rule, line, column}`; no text, quote, match, entropy score, provider details, or raw diagnostic is copied. The optional `strict-attributes` debug event reports only detected and suppressed match counts. Debugging is off by default; `debug(event)` receives value-free counts, rule timings, residual rounds and registry event metadata (see `DebugEvent` in the TypeScript declarations).
|
|
21
21
|
|
|
22
22
|
## Options
|
|
23
23
|
|
package/docs/COVERAGE.md
CHANGED
|
@@ -370,6 +370,11 @@ this explicit vocabulary: `firstName`, `lastName`, `middleName`, `patronymic`,
|
|
|
370
370
|
sensitive in full, including ISO birth timestamps. Thus `first_name` and
|
|
371
371
|
`firstName`, or `birth_date` and `birthDate`, follow the same policy. These are
|
|
372
372
|
schema assumptions: `contact` or `company` can hold operational data too.
|
|
373
|
+
Person fields also match as a word-boundary suffix of a compound key, so
|
|
374
|
+
`recruiterDisplayName`, `author_full_name` and `resumePdfOriginalFileName` are
|
|
375
|
+
person fields. The same suffix rule applies to `vocabulary.personalFields`.
|
|
376
|
+
Other types match only the whole key (`maxAge` and `fileName` stay), and a
|
|
377
|
+
bare `name` is not in the list.
|
|
373
378
|
Existing native labels still apply. Unlisted URL-named fields have ordinary
|
|
374
379
|
content detection; arbitrary names do not imply sensitivity. Declare them with
|
|
375
380
|
[`sensitiveFields`](API.md#caller-declared-sensitive-fields) to replace complete
|
|
@@ -471,7 +476,25 @@ proper-name mode. Romashka Inc keeps its existing legal-form treatment.
|
|
|
471
476
|
`strict: true` independently opts into personal attributes (gender, family,
|
|
472
477
|
ethnicity, religion, health, criminal/political status) and bare dates, ages,
|
|
473
478
|
passport/mobile/postal shapes. It is intended for stored personal prose and can
|
|
474
|
-
remove ordinary uses of
|
|
479
|
+
remove ordinary uses of unambiguous category words. Bounded Russian case/stem
|
|
480
|
+
rules cover disability, ethnicity, religion, diagnoses, intimate life and criminal
|
|
481
|
+
status, including `инвалидом`, `армянин`, `еврейка`, `казах`, `христианин`, `иудей`,
|
|
482
|
+
`эпилепсией`, `астма`, `онкологию`, `туберкулез`, `психиатра`, `депрессия`,
|
|
483
|
+
`гепатит`, `гей` and `отбывал наказание`. English coverage includes `epilepsy`,
|
|
484
|
+
`gay` and `Armenian`. Explicit category cues such as `по национальности X`,
|
|
485
|
+
`исповедует X`, `болеет X`, `на учете у X`, `diagnosed with X` and `ethnicity: X`
|
|
486
|
+
protect one following word (including hyphenated words) up to 64 characters,
|
|
487
|
+
including unfamiliar values outside the lexicon. These cues stay on one line.
|
|
488
|
+
Ambiguous English `disabled`, `single`, `man`, `men`, `russian`
|
|
489
|
+
and `asian` require an adjacent personal cue (pronoun/person role, personal
|
|
490
|
+
label, or following person role). Thus `he is disabled`, `candidate is single`
|
|
491
|
+
and `gender: male` redact, while `the module is disabled`, `single run finished`,
|
|
492
|
+
`single-flight lock acquired`, `man page missing`, `locale russian selected`
|
|
493
|
+
and `asian region endpoint` remain intact. Bare operational JSON statuses
|
|
494
|
+
`disabled`, `single` and `Russian` also remain intact; explicit personal labels
|
|
495
|
+
and caller-declared known personal values still redact. A cue in another clause
|
|
496
|
+
or string value does not make an ambiguous word personal.
|
|
497
|
+
It preserves ISO timestamps, work durations
|
|
475
498
|
(including `Стаж: 5 лет`, `работал там 5 лет`, and duration cues from issue 87),
|
|
476
499
|
and parenthesized durations after known technologies such as `Kubernetes (5 лет)`
|
|
477
500
|
and `Power BI (5 лет)`.
|
|
@@ -542,6 +565,9 @@ corpus, unbounded input, parallel builds or external analysis service.
|
|
|
542
565
|
|
|
543
566
|
Document rules recognize a capitalized Latin name after `candidate`, `recruiter`,
|
|
544
567
|
`applicant`, `Dear`, `Hi`, `Mr`/`Mr.`, `Ms`/`Ms.`, `Dr`/`Dr.`, or `message from`.
|
|
568
|
+
Honorifics count only when capitalized: lowercase `ms` (milliseconds), `mr` and
|
|
569
|
+
`dr` are not cues, and `Ms` right after a number is read as a unit, so
|
|
570
|
+
`took 30 ms Retry scheduled` and `see dr Drive mount` stay unchanged.
|
|
545
571
|
Only the name is replaced; punctuation and the cue remain. These rules work
|
|
546
572
|
without enabling Latin script matching. Finite generic heading nouns such as
|
|
547
573
|
`Stage` and `Status` are preserved: `Candidate Stage changed` and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-foundation/sensitive-data-sanitizer",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.3",
|
|
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/document-fields.js
CHANGED
|
@@ -25,7 +25,24 @@ const types = new Map(
|
|
|
25
25
|
)
|
|
26
26
|
);
|
|
27
27
|
export function documentFieldType(field) {
|
|
28
|
-
|
|
28
|
+
const type = types.get(normalizeFieldName(field));
|
|
29
|
+
if (type) {
|
|
30
|
+
return type;
|
|
31
|
+
}
|
|
32
|
+
// Compound person keys (`recruiterDisplayName`, `author_full_name`) end in
|
|
33
|
+
// a listed person field. Only PERSON entries extend this way, so generic
|
|
34
|
+
// suffixes such as `maxAge` or `homeSchool` keep their exact-match meaning,
|
|
35
|
+
// and a bare `name` is never listed.
|
|
36
|
+
return fieldSuffixes(field).some((suffix) => types.get(suffix) === 'PERSON')
|
|
37
|
+
? 'PERSON'
|
|
38
|
+
: undefined;
|
|
39
|
+
}
|
|
40
|
+
// Word-boundary suffixes of a key, normalized; the whole key comes first.
|
|
41
|
+
export function fieldSuffixes(field) {
|
|
42
|
+
const words = field
|
|
43
|
+
.split(/[\s_.-]+|(?<=[\p{Ll}\p{N}])(?=\p{Lu})|(?<=\p{Lu})(?=\p{Lu}\p{Ll})/u)
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
return words.map((_, i) => words.slice(i).join('').toLowerCase());
|
|
29
46
|
}
|
|
30
47
|
export const normalizeFieldName = (field) =>
|
|
31
48
|
field.replace(/[_-]/g, '').toLowerCase();
|
|
@@ -4,8 +4,11 @@ import { collectMatches } from './detection.js';
|
|
|
4
4
|
// capitalized-word matching for the rest of an English log or document.
|
|
5
5
|
const boundary = '[\\p{L}\\p{M}\\p{N}_]';
|
|
6
6
|
const name = "[A-Z][a-z]{1,47}(?:['’-][A-Z]?[a-z]{1,47})?";
|
|
7
|
+
// Honorifics count only when capitalized: lowercase `ms` is the millisecond
|
|
8
|
+
// unit in logs and lowercase `dr` is usually a word or path fragment. A
|
|
9
|
+
// capitalized `Ms` right after a number is still a unit (`took 30 Ms Retry`).
|
|
7
10
|
const cue =
|
|
8
|
-
'(?:[Cc]andidate|[Rr]ecruiter|[Aa]pplicant|[Dd]ear|[Hh]i|
|
|
11
|
+
'(?:[Cc]andidate|[Rr]ecruiter|[Aa]pplicant|[Dd]ear|[Hh]i|Mr\\.?|(?<!\\d[ \\t]*)Ms\\.?|Dr\\.?|[Mm]essage[ \\t]+from)';
|
|
9
12
|
const pattern = new RegExp(
|
|
10
13
|
`(?<!${boundary})${cue}[ \\t]+(${name})(?!${boundary})`,
|
|
11
14
|
'gu'
|
package/src/index.d.ts
CHANGED
|
@@ -118,6 +118,7 @@ export interface SanitizerOptions {
|
|
|
118
118
|
}
|
|
119
119
|
/** Value-free diagnostics; enabled only when a debug callback is supplied. */
|
|
120
120
|
export type DebugEvent =
|
|
121
|
+
| { event: 'strict-attributes'; detected: number; suppressed: number }
|
|
121
122
|
| { event: 'detected'; count: number }
|
|
122
123
|
| { event: 'document-experience' | 'proper-name-prose'; count: number }
|
|
123
124
|
| { event: 'residual-redaction'; round: number }
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { collectMatches } from './detection.js';
|
|
2
|
+
|
|
3
|
+
const attributes =
|
|
4
|
+
/(?<![\p{L}\p{N}_])(?:женщин[аыуеой]{1,2}|мужчин[аыуеой]{1,2}|замужем|женат(?:а|ы)?|(?:один|одна|двое|трое|четверо|пятеро|\d{1,2})[ \t]+(?:детей|реб[её]нок|реб[её]нка)|русск(?:ая|ий|ого|ой|ие)|татар(?:ин|ка|ы)|православн(?:ый|ая|ого|ой)|мусульман(?:ин|ка)|инвалидност[ьи](?:[ \t]+\d[ \t]+группы)?|беременн(?:а|ая|ость)|судимость(?:[ \t]+погашена)?|член[ \t]+партии|woman|women|male|female|married|divorced|pregnant|pregnancy|disability|christian|muslim|jewish|orthodox|hispanic|criminal[ \t]+record|party[ \t]+member|(?:one|two|three|\d{1,2})[ \t]+children)(?![\p{L}\p{N}_])/giu;
|
|
5
|
+
const additionalAttributes =
|
|
6
|
+
/(?<![\p{L}\p{N}_])(?:диабет(?:ом|а|у|е)?|ВИЧ(?:-положительн(?:ый|ая|ого|ой))?|болен|больна|больны|католик(?:ом|а|и)?|католичк(?:а|ой|и)|буддист(?:ка|кой|ом|а|ы)?|атеист(?:ка|кой|ом|а|ы)?|украин(?:ец|ка|цы|цем|кой)|белорус(?:ка|кой|ом|а|ы)?|несудим(?:а|ы)?|судим(?:а|ы)?|член[ \t]+политической[ \t]+партии|холост(?:а|ы)?|развед[её]н(?:а|ы)?|в[ \t]+разводе|diabetes|diabetic|HIV(?:[ \t-]+positive)?|cancer|buddhist|catholic|atheist|ukrainian|belarusian|previous[ \t]+conviction|member[ \t]+of[ \t]+(?:a[ \t]+)?(?:political[ \t]+)?party|unmarried|widowed)(?![\p{L}\p{N}_])/giu;
|
|
7
|
+
|
|
8
|
+
// Bounded suffixes cover Russian cases, plurals and adjectival forms without
|
|
9
|
+
// turning a stem into an unbounded substring search. Category cues below cover
|
|
10
|
+
// unfamiliar diagnoses, nationalities and religions outside this lexicon.
|
|
11
|
+
const categoryForms =
|
|
12
|
+
/(?<![\p{L}\p{N}_])(?:(?:диабет|католик|католич|буддист|атеист|белорус|православн|беременн|судим)[\p{L}]{0,8}|татар(?:ин(?:а|у|ом|е)?|к[аиуе]|кой|ы|ам|ами|ах)?|мусульман(?:ин(?:а|у|ом|е)?|к[аиуе]|кой|е|ам|ами|ах)?|украин(?:ец|ца|цу|цем|це|цы|цев|цам|цами|цах|к[аиуе]|кой)|инвалид(?:а|у|ом|е|ы|ов|ам|ами|ах|к[аиу]|кой)?|армян(?:ин|ина|ину|ином|ине|е|ам|ами|ах|ка|ки|ке|ку|кой)|евре(?:й|я|ю|ем|и|ев|ям|ями|ях|йк[аиуе]|йкой)|казах(?:а|у|ом|е|и|ов|ам|ами|ах|к[аиуе]|кой)?|христиан(?:ин|ина|ину|ином|ине|е|ам|ами|ах|к[аиуе]|кой)|иуде(?:й|я|ю|ем|и|ев|ям|ями|ях|йк[аиуе]|йкой)|ислам[\p{L}]{0,5}|эпилепси[\p{L}]{1,5}|астм[\p{L}]{1,5}|онкологи[\p{L}]{1,5}|туберкул[её]з[\p{L}]{0,5}|психиатр[\p{L}]{0,6}|депресси[\p{L}]{1,5}|гепатит[\p{L}]{0,5}|гомосексуал[\p{L}]{0,7}|лесбиян[\p{L}]{1,5}|ге(?:й|я|ю|ем|и|ев|ям|ями|ях)|нетрадиционн[\p{L}]{2,3}[ \t]+ориентаци[\p{L}]{1,3}|отбыва(?:л[аи]?|ет|ют|вш[\p{L}]{2,3})[ \t]+наказани[\p{L}]{1,3}|ВИЧ-инфицирован[\p{L}]{0,4}|epilepsy|asthma|tuberculosis|hepatitis|depression|gay|lesbian|homosexual|bisexual|armenian)(?![\p{L}\p{N}_])/giu;
|
|
13
|
+
const categoryCue =
|
|
14
|
+
/(?<![\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;
|
|
15
|
+
const ambiguous =
|
|
16
|
+
/(?<![\p{L}\p{N}_])(?:disabled|single|man|men|russian|asian)(?![\p{L}\p{N}_])/giu;
|
|
17
|
+
|
|
18
|
+
function personalContext(text, match) {
|
|
19
|
+
const prefix = text.slice(Math.max(0, match.index - 128), match.index);
|
|
20
|
+
const suffix = text.slice(
|
|
21
|
+
match.index + match[0].length,
|
|
22
|
+
match.index + match[0].length + 64
|
|
23
|
+
);
|
|
24
|
+
// These continuations remain technical even after a pronoun ("it is a
|
|
25
|
+
// single-flight lock"). No document-wide cue can authorize another clause.
|
|
26
|
+
if (
|
|
27
|
+
/^(?:-flight\b|[ \t]+(?:page|pages|run|worker|thread|lock|region|endpoint|locale|module|feature|flag)\b)/iu.test(
|
|
28
|
+
suffix
|
|
29
|
+
)
|
|
30
|
+
) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return (
|
|
34
|
+
/(?<![\p{L}\p{N}_])(?:i|he|she|we|they|candidate|applicant|patient|person|employee)(?:[ \t]+(?:am|is|are|was|were|identifies[ \t]+as))?(?:[ \t]+(?:a|an))?[ \t]+$/iu.test(
|
|
35
|
+
prefix
|
|
36
|
+
) ||
|
|
37
|
+
/(?<![\p{L}\p{N}_])(?:gender|sex|nationality|ethnicity|marital[ \t_-]*status|disability)(?:["'][ \t]*)?[ \t]*[:=][ \t]*["']?$/iu.test(
|
|
38
|
+
prefix
|
|
39
|
+
) ||
|
|
40
|
+
/^[ \t]+(?:candidate|applicant|patient|person|people|employee)(?![\p{L}\p{N}_])/iu.test(
|
|
41
|
+
suffix
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function detectStrictAttributes(text, emit, options) {
|
|
47
|
+
let detected = 0,
|
|
48
|
+
suppressed = 0;
|
|
49
|
+
const details = {
|
|
50
|
+
type: 'PERSONAL_ATTRIBUTE',
|
|
51
|
+
rule: 'strict-attribute',
|
|
52
|
+
category: 'personal',
|
|
53
|
+
confidence: 0.9,
|
|
54
|
+
};
|
|
55
|
+
const report = (finding) => {
|
|
56
|
+
detected++;
|
|
57
|
+
emit(finding);
|
|
58
|
+
};
|
|
59
|
+
for (const pattern of [attributes, additionalAttributes, categoryForms]) {
|
|
60
|
+
collectMatches(text, pattern, report, details);
|
|
61
|
+
}
|
|
62
|
+
collectMatches(text, categoryCue, report, details, 1);
|
|
63
|
+
collectMatches(text, ambiguous, report, details, 0, (_value, match) => {
|
|
64
|
+
const accepted = personalContext(text, match);
|
|
65
|
+
if (!accepted) {
|
|
66
|
+
suppressed++;
|
|
67
|
+
}
|
|
68
|
+
return accepted;
|
|
69
|
+
});
|
|
70
|
+
options.debug?.({ event: 'strict-attributes', detected, suppressed });
|
|
71
|
+
}
|
package/src/strict.js
CHANGED
|
@@ -3,12 +3,9 @@ import { months } from './personal-context.js';
|
|
|
3
3
|
import { isDocumentTechnology } from './document-experience.js';
|
|
4
4
|
import { placeholderValues } from './placeholders.js';
|
|
5
5
|
import { generatedValues } from './fake.js';
|
|
6
|
+
import { detectStrictAttributes } from './strict-attributes.js';
|
|
6
7
|
|
|
7
8
|
const boundary = '[\\p{L}\\p{N}_]';
|
|
8
|
-
const attributes =
|
|
9
|
-
/(?<![\p{L}\p{N}_])(?:женщин[аыуеой]{1,2}|мужчин[аыуеой]{1,2}|замужем|женат(?:а|ы)?|(?:один|одна|двое|трое|четверо|пятеро|\d{1,2})[ \t]+(?:детей|реб[её]нок|реб[её]нка)|русск(?:ая|ий|ого|ой|ие)|татар(?:ин|ка|ы)|православн(?:ый|ая|ого|ой)|мусульман(?:ин|ка)|инвалидност[ьи](?:[ \t]+\d[ \t]+группы)?|беременн(?:а|ая|ость)|судимость(?:[ \t]+погашена)?|член[ \t]+партии|woman|women|man|men|male|female|married|divorced|pregnant|pregnancy|disabled|disability|christian|muslim|jewish|orthodox|russian|hispanic|asian|criminal[ \t]+record|party[ \t]+member|(?:one|two|three|\d{1,2})[ \t]+children)(?![\p{L}\p{N}_])/giu;
|
|
10
|
-
const additionalAttributes =
|
|
11
|
-
/(?<![\p{L}\p{N}_])(?:диабет(?:ом|а|у|е)?|ВИЧ(?:-положительн(?:ый|ая|ого|ой))?|болен|больна|больны|католик(?:ом|а|и)?|католичк(?:а|ой|и)|буддист(?:ка|кой|ом|а|ы)?|атеист(?:ка|кой|ом|а|ы)?|украин(?:ец|ка|цы|цем|кой)|белорус(?:ка|кой|ом|а|ы)?|несудим(?:а|ы)?|судим(?:а|ы)?|член[ \t]+политической[ \t]+партии|холост(?:а|ы)?|развед[её]н(?:а|ы)?|в[ \t]+разводе|diabetes|diabetic|HIV(?:[ \t-]+positive)?|cancer|buddhist|catholic|atheist|ukrainian|belarusian|previous[ \t]+conviction|member[ \t]+of[ \t]+(?:a[ \t]+)?(?:political[ \t]+)?party|single|unmarried|widowed)(?![\p{L}\p{N}_])/giu;
|
|
12
9
|
|
|
13
10
|
function isDuration(text, match, replacements) {
|
|
14
11
|
// Keep cues on the same clause; an age on the next line is independent.
|
|
@@ -57,14 +54,7 @@ export function detectStrict(text, emit, options) {
|
|
|
57
54
|
if (!options.strict) {
|
|
58
55
|
return;
|
|
59
56
|
}
|
|
60
|
-
|
|
61
|
-
collectMatches(text, pattern, emit, {
|
|
62
|
-
type: 'PERSONAL_ATTRIBUTE',
|
|
63
|
-
rule: 'strict-attribute',
|
|
64
|
-
category: 'personal',
|
|
65
|
-
confidence: 0.9,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
57
|
+
detectStrictAttributes(text, emit, options);
|
|
68
58
|
const date = new RegExp(
|
|
69
59
|
`(?<!${boundary})(?:\\d{4}-\\d{2}-\\d{2}|\\d{1,2}[./]\\d{1,2}[./]\\d{4}|\\d{1,2}[ \\t]+(?:${months})[ \\t]+\\d{4})(?!${boundary}|[Tt]\\d)`,
|
|
70
60
|
'giu'
|
package/src/vocabulary.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { failure, collectMatches } from './detection.js';
|
|
2
2
|
import { wordPattern } from './personal-variants.js';
|
|
3
|
-
import { normalizeFieldName } from './document-fields.js';
|
|
3
|
+
import { fieldSuffixes, normalizeFieldName } from './document-fields.js';
|
|
4
4
|
|
|
5
5
|
export const defaultVocabulary = Object.freeze(
|
|
6
6
|
Object.fromEntries(
|
|
@@ -170,7 +170,8 @@ export function vocabularyField(field, options) {
|
|
|
170
170
|
if (!vocabulary) {
|
|
171
171
|
return undefined;
|
|
172
172
|
}
|
|
173
|
-
return vocabulary.fields.has(normalizeFieldName(field))
|
|
173
|
+
return vocabulary.fields.has(normalizeFieldName(field)) ||
|
|
174
|
+
fieldSuffixes(field).some((suffix) => vocabulary.fields.has(suffix))
|
|
174
175
|
? true
|
|
175
176
|
: options.vocabulary.personalFields?.replace
|
|
176
177
|
? false
|