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 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 buildSubstitutionStep(from, to, fromIndex, toIndex, map) {
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 buildSubstitutionStep(from, to, fromIndex, toIndex, map) {
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 };