namespace-guard 0.15.0 → 0.16.0
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/README.md +1 -0
- package/dist/cli.js +25 -3
- package/dist/cli.mjs +25 -3
- package/dist/confusable-weights.d.mts +19 -0
- package/dist/confusable-weights.d.ts +19 -0
- package/dist/confusable-weights.js +2198 -0
- package/dist/confusable-weights.mjs +2173 -0
- package/dist/index.d.mts +38 -2
- package/dist/index.d.ts +38 -2
- package/dist/index.js +25 -3
- package/dist/index.mjs +25 -3
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -67,6 +67,7 @@ We started by auditing how major Unicode-confusable implementations compose norm
|
|
|
67
67
|
- Export the vectors as JSON (`docs/data/composability-vectors.json`) and wire them into CLI drift baselines.
|
|
68
68
|
- Publish a labeled benchmark corpus (`docs/data/confusable-bench.v1.json`) for cross-tool evaluation and CI regressions.
|
|
69
69
|
- Submitted the findings for Unicode public review (PRI #540): https://www.unicode.org/review/pri540/
|
|
70
|
+
- Validated the 31 divergence vectors empirically by rendering each character across 12 fonts and measuring SSIM similarity: TR39 is visually correct for letter-shape confusables, NFKC for digit confusables, and 61% are ties where both targets are near-identical ([confusable-vision](https://github.com/paultendo/confusable-vision)).
|
|
70
71
|
|
|
71
72
|
Details:
|
|
72
73
|
- Technical reference: [docs/reference.md#how-the-anti-spoofing-pipeline-works](docs/reference.md#how-the-anti-spoofing-pipeline-works)
|
package/dist/cli.js
CHANGED
|
@@ -2872,10 +2872,19 @@ function isNfkcDivergentMapping(ch, mapped) {
|
|
|
2872
2872
|
const nfkc = ch.normalize("NFKC").toLowerCase();
|
|
2873
2873
|
return /^[a-z0-9]$/.test(nfkc) && nfkc !== mapped;
|
|
2874
2874
|
}
|
|
2875
|
-
function
|
|
2875
|
+
function lookupWeight(from, to, weights, context) {
|
|
2876
|
+
if (!weights) return void 0;
|
|
2877
|
+
const w = weights[from]?.[to] ?? weights[to]?.[from];
|
|
2878
|
+
if (!w) return void 0;
|
|
2879
|
+
if (context === "identifier" && !w.xidContinue) return void 0;
|
|
2880
|
+
if (context === "domain" && !w.idnaPvalid) return void 0;
|
|
2881
|
+
return w;
|
|
2882
|
+
}
|
|
2883
|
+
function buildSubstitutionStep(from, to, fromIndex, toIndex, map, weights, context) {
|
|
2876
2884
|
if (from === to) {
|
|
2877
2885
|
return { op: "match", from, to, fromIndex, toIndex, cost: 0 };
|
|
2878
2886
|
}
|
|
2887
|
+
const weight = lookupWeight(from, to, weights, context ?? "all");
|
|
2879
2888
|
const fromPrototype = map[from] ?? from;
|
|
2880
2889
|
const toPrototype = map[to] ?? to;
|
|
2881
2890
|
if (fromPrototype === toPrototype) {
|
|
@@ -2883,7 +2892,7 @@ function buildSubstitutionStep(from, to, fromIndex, toIndex, map) {
|
|
|
2883
2892
|
const toScript = getScriptTag(to);
|
|
2884
2893
|
const crossScript = fromScript !== "non-letter" && toScript !== "non-letter" && fromScript !== toScript;
|
|
2885
2894
|
const divergence = isNfkcDivergentMapping(from, fromPrototype) || isNfkcDivergentMapping(to, toPrototype);
|
|
2886
|
-
let cost = 0.35;
|
|
2895
|
+
let cost = weight?.glyphReuse ? 0 : weight?.cost ?? 0.35;
|
|
2887
2896
|
let reason;
|
|
2888
2897
|
if (crossScript) {
|
|
2889
2898
|
cost += 0.2;
|
|
@@ -2919,6 +2928,17 @@ function buildSubstitutionStep(from, to, fromIndex, toIndex, map) {
|
|
|
2919
2928
|
reason: "nfkc-equivalent"
|
|
2920
2929
|
};
|
|
2921
2930
|
}
|
|
2931
|
+
if (weight) {
|
|
2932
|
+
return {
|
|
2933
|
+
op: "substitution",
|
|
2934
|
+
from,
|
|
2935
|
+
to,
|
|
2936
|
+
fromIndex,
|
|
2937
|
+
toIndex,
|
|
2938
|
+
cost: weight.cost,
|
|
2939
|
+
reason: "visual-weight"
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2922
2942
|
return { op: "substitution", from, to, fromIndex, toIndex, cost: 1 };
|
|
2923
2943
|
}
|
|
2924
2944
|
function buildDeletionStep(ch, fromIndex, toIndex) {
|
|
@@ -2960,6 +2980,8 @@ function skeleton(input, options) {
|
|
|
2960
2980
|
}
|
|
2961
2981
|
function confusableDistance(a, b, options) {
|
|
2962
2982
|
const map = options?.map ?? CONFUSABLE_MAP_FULL;
|
|
2983
|
+
const weights = options?.weights;
|
|
2984
|
+
const context = options?.context ?? "all";
|
|
2963
2985
|
const left = toCodePoints(a.normalize("NFD").toLowerCase());
|
|
2964
2986
|
const right = toCodePoints(b.normalize("NFD").toLowerCase());
|
|
2965
2987
|
const m = left.length;
|
|
@@ -2985,7 +3007,7 @@ function confusableDistance(a, b, options) {
|
|
|
2985
3007
|
}
|
|
2986
3008
|
for (let i2 = 1; i2 <= m; i2++) {
|
|
2987
3009
|
for (let j2 = 1; j2 <= n; j2++) {
|
|
2988
|
-
const substitution = buildSubstitutionStep(left[i2 - 1], right[j2 - 1], i2 - 1, j2 - 1, map);
|
|
3010
|
+
const substitution = buildSubstitutionStep(left[i2 - 1], right[j2 - 1], i2 - 1, j2 - 1, map, weights, context);
|
|
2989
3011
|
const deletion = buildDeletionStep(left[i2 - 1], i2 - 1, j2);
|
|
2990
3012
|
const insertion = buildInsertionStep(right[j2 - 1], i2, j2 - 1);
|
|
2991
3013
|
const candidates = [
|
package/dist/cli.mjs
CHANGED
|
@@ -2849,10 +2849,19 @@ function isNfkcDivergentMapping(ch, mapped) {
|
|
|
2849
2849
|
const nfkc = ch.normalize("NFKC").toLowerCase();
|
|
2850
2850
|
return /^[a-z0-9]$/.test(nfkc) && nfkc !== mapped;
|
|
2851
2851
|
}
|
|
2852
|
-
function
|
|
2852
|
+
function lookupWeight(from, to, weights, context) {
|
|
2853
|
+
if (!weights) return void 0;
|
|
2854
|
+
const w = weights[from]?.[to] ?? weights[to]?.[from];
|
|
2855
|
+
if (!w) return void 0;
|
|
2856
|
+
if (context === "identifier" && !w.xidContinue) return void 0;
|
|
2857
|
+
if (context === "domain" && !w.idnaPvalid) return void 0;
|
|
2858
|
+
return w;
|
|
2859
|
+
}
|
|
2860
|
+
function buildSubstitutionStep(from, to, fromIndex, toIndex, map, weights, context) {
|
|
2853
2861
|
if (from === to) {
|
|
2854
2862
|
return { op: "match", from, to, fromIndex, toIndex, cost: 0 };
|
|
2855
2863
|
}
|
|
2864
|
+
const weight = lookupWeight(from, to, weights, context ?? "all");
|
|
2856
2865
|
const fromPrototype = map[from] ?? from;
|
|
2857
2866
|
const toPrototype = map[to] ?? to;
|
|
2858
2867
|
if (fromPrototype === toPrototype) {
|
|
@@ -2860,7 +2869,7 @@ function buildSubstitutionStep(from, to, fromIndex, toIndex, map) {
|
|
|
2860
2869
|
const toScript = getScriptTag(to);
|
|
2861
2870
|
const crossScript = fromScript !== "non-letter" && toScript !== "non-letter" && fromScript !== toScript;
|
|
2862
2871
|
const divergence = isNfkcDivergentMapping(from, fromPrototype) || isNfkcDivergentMapping(to, toPrototype);
|
|
2863
|
-
let cost = 0.35;
|
|
2872
|
+
let cost = weight?.glyphReuse ? 0 : weight?.cost ?? 0.35;
|
|
2864
2873
|
let reason;
|
|
2865
2874
|
if (crossScript) {
|
|
2866
2875
|
cost += 0.2;
|
|
@@ -2896,6 +2905,17 @@ function buildSubstitutionStep(from, to, fromIndex, toIndex, map) {
|
|
|
2896
2905
|
reason: "nfkc-equivalent"
|
|
2897
2906
|
};
|
|
2898
2907
|
}
|
|
2908
|
+
if (weight) {
|
|
2909
|
+
return {
|
|
2910
|
+
op: "substitution",
|
|
2911
|
+
from,
|
|
2912
|
+
to,
|
|
2913
|
+
fromIndex,
|
|
2914
|
+
toIndex,
|
|
2915
|
+
cost: weight.cost,
|
|
2916
|
+
reason: "visual-weight"
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2899
2919
|
return { op: "substitution", from, to, fromIndex, toIndex, cost: 1 };
|
|
2900
2920
|
}
|
|
2901
2921
|
function buildDeletionStep(ch, fromIndex, toIndex) {
|
|
@@ -2937,6 +2957,8 @@ function skeleton(input, options) {
|
|
|
2937
2957
|
}
|
|
2938
2958
|
function confusableDistance(a, b, options) {
|
|
2939
2959
|
const map = options?.map ?? CONFUSABLE_MAP_FULL;
|
|
2960
|
+
const weights = options?.weights;
|
|
2961
|
+
const context = options?.context ?? "all";
|
|
2940
2962
|
const left = toCodePoints(a.normalize("NFD").toLowerCase());
|
|
2941
2963
|
const right = toCodePoints(b.normalize("NFD").toLowerCase());
|
|
2942
2964
|
const m = left.length;
|
|
@@ -2962,7 +2984,7 @@ function confusableDistance(a, b, options) {
|
|
|
2962
2984
|
}
|
|
2963
2985
|
for (let i2 = 1; i2 <= m; i2++) {
|
|
2964
2986
|
for (let j2 = 1; j2 <= n; j2++) {
|
|
2965
|
-
const substitution = buildSubstitutionStep(left[i2 - 1], right[j2 - 1], i2 - 1, j2 - 1, map);
|
|
2987
|
+
const substitution = buildSubstitutionStep(left[i2 - 1], right[j2 - 1], i2 - 1, j2 - 1, map, weights, context);
|
|
2966
2988
|
const deletion = buildDeletionStep(left[i2 - 1], i2 - 1, j2);
|
|
2967
2989
|
const insertion = buildInsertionStep(right[j2 - 1], i2, j2 - 1);
|
|
2968
2990
|
const candidates = [
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ConfusableWeights } from './index.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Measured visual similarity weights for 903 confusable pairs
|
|
5
|
+
* (110 TR39 + 793 novel discoveries).
|
|
6
|
+
*
|
|
7
|
+
* Scored across macos-m1-system-230fonts using SSIM.
|
|
8
|
+
* Each weight includes danger (max SSIM), stableDanger (p95 SSIM), cost (1 - stableDanger),
|
|
9
|
+
* and identifier property flags (xidContinue, idnaPvalid, tr39Allowed).
|
|
10
|
+
*
|
|
11
|
+
* Import from "namespace-guard/confusable-weights" to use with confusableDistance().
|
|
12
|
+
*
|
|
13
|
+
* @licence CC-BY-4.0 (data); MIT (code)
|
|
14
|
+
* @attribution Paul Wood FRSA, confusable-vision
|
|
15
|
+
* @see https://github.com/paultendo/confusable-vision
|
|
16
|
+
*/
|
|
17
|
+
declare const CONFUSABLE_WEIGHTS: ConfusableWeights;
|
|
18
|
+
|
|
19
|
+
export { CONFUSABLE_WEIGHTS };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ConfusableWeights } from './index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Measured visual similarity weights for 903 confusable pairs
|
|
5
|
+
* (110 TR39 + 793 novel discoveries).
|
|
6
|
+
*
|
|
7
|
+
* Scored across macos-m1-system-230fonts using SSIM.
|
|
8
|
+
* Each weight includes danger (max SSIM), stableDanger (p95 SSIM), cost (1 - stableDanger),
|
|
9
|
+
* and identifier property flags (xidContinue, idnaPvalid, tr39Allowed).
|
|
10
|
+
*
|
|
11
|
+
* Import from "namespace-guard/confusable-weights" to use with confusableDistance().
|
|
12
|
+
*
|
|
13
|
+
* @licence CC-BY-4.0 (data); MIT (code)
|
|
14
|
+
* @attribution Paul Wood FRSA, confusable-vision
|
|
15
|
+
* @see https://github.com/paultendo/confusable-vision
|
|
16
|
+
*/
|
|
17
|
+
declare const CONFUSABLE_WEIGHTS: ConfusableWeights;
|
|
18
|
+
|
|
19
|
+
export { CONFUSABLE_WEIGHTS };
|