@remnic/core 9.62.0 → 9.62.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.
Files changed (158) hide show
  1. package/dist/access-admin-ops-surface.js +11 -11
  2. package/dist/access-authorization-probe.js +12 -12
  3. package/dist/access-boundary.js +11 -11
  4. package/dist/access-cli.js +43 -43
  5. package/dist/access-extraction-force-flush.js +11 -11
  6. package/dist/access-http-query.js +11 -11
  7. package/dist/access-http.js +20 -20
  8. package/dist/access-identity-continuity-surface.js +11 -11
  9. package/dist/access-lcm-surface.js +11 -11
  10. package/dist/access-mcp.js +16 -16
  11. package/dist/access-namespace-preflight.js +11 -11
  12. package/dist/access-observe-write-surface.js +11 -11
  13. package/dist/access-operations-batch.js +12 -12
  14. package/dist/access-operations.js +15 -15
  15. package/dist/access-recall-concurrency.js +11 -11
  16. package/dist/access-recall-response.js +11 -11
  17. package/dist/access-recall-surface.js +11 -11
  18. package/dist/access-service.js +11 -11
  19. package/dist/access-surface-catalog.js +13 -13
  20. package/dist/active-memory-bridge.js +2 -2
  21. package/dist/active-recall.js +3 -3
  22. package/dist/briefing.js +4 -4
  23. package/dist/causal-consolidation.js +5 -5
  24. package/dist/{chunk-KAB7TZN3.js → chunk-3G6YU5YD.js} +2 -2
  25. package/dist/{chunk-XD4VGLDL.js → chunk-3XG4QEA2.js} +35 -31
  26. package/dist/chunk-3XG4QEA2.js.map +1 -0
  27. package/dist/{chunk-HEEEMJ26.js → chunk-4E67XCT2.js} +3 -3
  28. package/dist/{chunk-H32ANIFP.js → chunk-4G45LJJP.js} +2 -2
  29. package/dist/{chunk-4XAVQLBR.js → chunk-4Y2RHVCV.js} +2 -2
  30. package/dist/{chunk-S3B3UIQ3.js → chunk-67MKNKJQ.js} +2 -2
  31. package/dist/{chunk-PA5OPZX2.js → chunk-75U2FR7U.js} +2 -2
  32. package/dist/{chunk-I67HKRBF.js → chunk-7JAXC526.js} +2 -2
  33. package/dist/{chunk-3GPBFDXH.js → chunk-7PVTOZR4.js} +2 -2
  34. package/dist/{chunk-DSA47WBN.js → chunk-BHO3IVAN.js} +2 -2
  35. package/dist/{chunk-MYZ4NA43.js → chunk-DYDACAK2.js} +2 -2
  36. package/dist/{chunk-4JGSG7QN.js → chunk-ETCVSZLG.js} +2 -2
  37. package/dist/{chunk-EIQFIJDM.js → chunk-EU74U4GT.js} +2 -2
  38. package/dist/{chunk-L33N3BCA.js → chunk-GZRDOVQN.js} +2 -2
  39. package/dist/{chunk-AWRJBAIH.js → chunk-HKYNBEL4.js} +6 -6
  40. package/dist/{chunk-MJ4UGUR6.js → chunk-IIRK5V5Z.js} +2 -2
  41. package/dist/{chunk-2RIDW23Y.js → chunk-IXJ5U6ZL.js} +8 -8
  42. package/dist/{chunk-QRKYABS6.js → chunk-JDJOAL4B.js} +2 -2
  43. package/dist/{chunk-GN7HO2LA.js → chunk-JF6YSHJM.js} +7 -7
  44. package/dist/{chunk-D5HDJWNN.js → chunk-JLMH5YMU.js} +3 -3
  45. package/dist/{chunk-FW73HN33.js → chunk-K4WIUTNH.js} +2 -2
  46. package/dist/{chunk-TE4MKFT2.js → chunk-KCFZOSR6.js} +4 -4
  47. package/dist/{chunk-VWDTV6SH.js → chunk-KYI3CM42.js} +2 -2
  48. package/dist/{chunk-CIA6ZUBA.js → chunk-LAM6UDFV.js} +2 -2
  49. package/dist/{chunk-W4Q54HM3.js → chunk-LLLY5IBK.js} +2 -2
  50. package/dist/{chunk-CM4SZZBN.js → chunk-MJFDJTDC.js} +2 -2
  51. package/dist/{chunk-VOELBUS4.js → chunk-NXH34IPL.js} +2 -2
  52. package/dist/{chunk-MU3TPZOV.js → chunk-PBH2Z2NY.js} +2 -2
  53. package/dist/chunk-PJKTFXE2.js +119 -0
  54. package/dist/chunk-PJKTFXE2.js.map +1 -0
  55. package/dist/{chunk-G3YBWFUC.js → chunk-QN7XQEIT.js} +34 -17
  56. package/dist/chunk-QN7XQEIT.js.map +1 -0
  57. package/dist/{chunk-HQBVQICL.js → chunk-QNMNNFKD.js} +2 -2
  58. package/dist/{chunk-EGW4O32Z.js → chunk-QRA3XVJX.js} +7 -4
  59. package/dist/chunk-QRA3XVJX.js.map +1 -0
  60. package/dist/{chunk-L4DLLKPN.js → chunk-SZ4HFZKB.js} +2 -2
  61. package/dist/{chunk-N4OYPGZS.js → chunk-TLUSSS2F.js} +2 -2
  62. package/dist/{chunk-N4EPIKSY.js → chunk-U27G4E5G.js} +3 -3
  63. package/dist/{chunk-W3Y6FYRK.js → chunk-UIRU3MIJ.js} +2 -2
  64. package/dist/{chunk-URN7XWGC.js → chunk-WANOCU3K.js} +2 -2
  65. package/dist/{chunk-XDK6ZFLL.js → chunk-WTMG5GAK.js} +2 -2
  66. package/dist/{chunk-C3R732UY.js → chunk-XIABRKF6.js} +37 -32
  67. package/dist/chunk-XIABRKF6.js.map +1 -0
  68. package/dist/{chunk-D4OCJSLV.js → chunk-YDPAE2RD.js} +2 -2
  69. package/dist/{chunk-FQSDU634.js → chunk-YG56A6XY.js} +2 -2
  70. package/dist/{chunk-XBR5BAKW.js → chunk-YSUSUZR4.js} +3 -3
  71. package/dist/{chunk-YUIBXMZY.js → chunk-ZJKLGS76.js} +2 -2
  72. package/dist/cli.js +32 -32
  73. package/dist/compounding/engine.js +4 -4
  74. package/dist/config.js +3 -3
  75. package/dist/connectors/codex-materialize-runner.js +4 -4
  76. package/dist/connectors/index.js +4 -4
  77. package/dist/convergence-refresh.js +6 -6
  78. package/dist/corpus-watermark.js +6 -6
  79. package/dist/entity-retrieval.js +4 -4
  80. package/dist/external-wiki-access.js +12 -12
  81. package/dist/extraction-source-grounding.js +3 -3
  82. package/dist/extraction.js +7 -7
  83. package/dist/harmonic-retrieval.js +1 -1
  84. package/dist/index.d.ts +1 -0
  85. package/dist/index.js +56 -43
  86. package/dist/maintenance/memory-governance.js +4 -4
  87. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
  88. package/dist/maintenance/rebuild-memory-projection.js +5 -5
  89. package/dist/namespaces/migrate.js +5 -5
  90. package/dist/namespaces/storage.js +4 -4
  91. package/dist/operator-doctor-corpus.js +7 -7
  92. package/dist/operator-toolkit.js +12 -12
  93. package/dist/orchestration/maintenance.js +9 -9
  94. package/dist/orchestrator.js +43 -43
  95. package/dist/provenance.js +2 -2
  96. package/dist/resume-bundles.js +4 -4
  97. package/dist/semantic-consolidation.js +5 -5
  98. package/dist/semantic-rule-promotion.js +4 -4
  99. package/dist/semantic-rule-verifier.js +4 -4
  100. package/dist/source-agent-qualifier.js +6 -6
  101. package/dist/storage.js +3 -3
  102. package/dist/support-passport/index.js +14 -14
  103. package/dist/verified-recall.js +4 -4
  104. package/dist/whitespace.d.ts +26 -1
  105. package/dist/whitespace.js +11 -3
  106. package/package.json +2 -2
  107. package/src/cli.ts +14 -13
  108. package/src/entity-retrieval.ts +4 -7
  109. package/src/harmonic-construction.test.ts +187 -2
  110. package/src/harmonic-retrieval.ts +54 -32
  111. package/src/index.ts +3 -4
  112. package/src/orchestration/recall-section-coordinator.ts +20 -14
  113. package/src/whitespace.test.ts +178 -0
  114. package/src/whitespace.ts +119 -0
  115. package/dist/chunk-6MKAMLQL.js +0 -16
  116. package/dist/chunk-6MKAMLQL.js.map +0 -1
  117. package/dist/chunk-C3R732UY.js.map +0 -1
  118. package/dist/chunk-EGW4O32Z.js.map +0 -1
  119. package/dist/chunk-G3YBWFUC.js.map +0 -1
  120. package/dist/chunk-XD4VGLDL.js.map +0 -1
  121. /package/dist/{chunk-KAB7TZN3.js.map → chunk-3G6YU5YD.js.map} +0 -0
  122. /package/dist/{chunk-HEEEMJ26.js.map → chunk-4E67XCT2.js.map} +0 -0
  123. /package/dist/{chunk-H32ANIFP.js.map → chunk-4G45LJJP.js.map} +0 -0
  124. /package/dist/{chunk-4XAVQLBR.js.map → chunk-4Y2RHVCV.js.map} +0 -0
  125. /package/dist/{chunk-S3B3UIQ3.js.map → chunk-67MKNKJQ.js.map} +0 -0
  126. /package/dist/{chunk-PA5OPZX2.js.map → chunk-75U2FR7U.js.map} +0 -0
  127. /package/dist/{chunk-I67HKRBF.js.map → chunk-7JAXC526.js.map} +0 -0
  128. /package/dist/{chunk-3GPBFDXH.js.map → chunk-7PVTOZR4.js.map} +0 -0
  129. /package/dist/{chunk-DSA47WBN.js.map → chunk-BHO3IVAN.js.map} +0 -0
  130. /package/dist/{chunk-MYZ4NA43.js.map → chunk-DYDACAK2.js.map} +0 -0
  131. /package/dist/{chunk-4JGSG7QN.js.map → chunk-ETCVSZLG.js.map} +0 -0
  132. /package/dist/{chunk-EIQFIJDM.js.map → chunk-EU74U4GT.js.map} +0 -0
  133. /package/dist/{chunk-L33N3BCA.js.map → chunk-GZRDOVQN.js.map} +0 -0
  134. /package/dist/{chunk-AWRJBAIH.js.map → chunk-HKYNBEL4.js.map} +0 -0
  135. /package/dist/{chunk-MJ4UGUR6.js.map → chunk-IIRK5V5Z.js.map} +0 -0
  136. /package/dist/{chunk-2RIDW23Y.js.map → chunk-IXJ5U6ZL.js.map} +0 -0
  137. /package/dist/{chunk-QRKYABS6.js.map → chunk-JDJOAL4B.js.map} +0 -0
  138. /package/dist/{chunk-GN7HO2LA.js.map → chunk-JF6YSHJM.js.map} +0 -0
  139. /package/dist/{chunk-D5HDJWNN.js.map → chunk-JLMH5YMU.js.map} +0 -0
  140. /package/dist/{chunk-FW73HN33.js.map → chunk-K4WIUTNH.js.map} +0 -0
  141. /package/dist/{chunk-TE4MKFT2.js.map → chunk-KCFZOSR6.js.map} +0 -0
  142. /package/dist/{chunk-VWDTV6SH.js.map → chunk-KYI3CM42.js.map} +0 -0
  143. /package/dist/{chunk-CIA6ZUBA.js.map → chunk-LAM6UDFV.js.map} +0 -0
  144. /package/dist/{chunk-W4Q54HM3.js.map → chunk-LLLY5IBK.js.map} +0 -0
  145. /package/dist/{chunk-CM4SZZBN.js.map → chunk-MJFDJTDC.js.map} +0 -0
  146. /package/dist/{chunk-VOELBUS4.js.map → chunk-NXH34IPL.js.map} +0 -0
  147. /package/dist/{chunk-MU3TPZOV.js.map → chunk-PBH2Z2NY.js.map} +0 -0
  148. /package/dist/{chunk-HQBVQICL.js.map → chunk-QNMNNFKD.js.map} +0 -0
  149. /package/dist/{chunk-L4DLLKPN.js.map → chunk-SZ4HFZKB.js.map} +0 -0
  150. /package/dist/{chunk-N4OYPGZS.js.map → chunk-TLUSSS2F.js.map} +0 -0
  151. /package/dist/{chunk-N4EPIKSY.js.map → chunk-U27G4E5G.js.map} +0 -0
  152. /package/dist/{chunk-W3Y6FYRK.js.map → chunk-UIRU3MIJ.js.map} +0 -0
  153. /package/dist/{chunk-URN7XWGC.js.map → chunk-WANOCU3K.js.map} +0 -0
  154. /package/dist/{chunk-XDK6ZFLL.js.map → chunk-WTMG5GAK.js.map} +0 -0
  155. /package/dist/{chunk-D4OCJSLV.js.map → chunk-YDPAE2RD.js.map} +0 -0
  156. /package/dist/{chunk-FQSDU634.js.map → chunk-YG56A6XY.js.map} +0 -0
  157. /package/dist/{chunk-XBR5BAKW.js.map → chunk-YSUSUZR4.js.map} +0 -0
  158. /package/dist/{chunk-YUIBXMZY.js.map → chunk-ZJKLGS76.js.map} +0 -0
@@ -0,0 +1,178 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import {
5
+ collapseWhitespace,
6
+ displayWidth,
7
+ graphemeUnits,
8
+ padEndDisplay,
9
+ truncateCodePointSafe,
10
+ truncateGraphemeSafe,
11
+ } from "./whitespace.js";
12
+
13
+ function hasLoneSurrogate(value: string): boolean {
14
+ for (let index = 0; index < value.length; index += 1) {
15
+ const code = value.charCodeAt(index);
16
+ const isHigh = code >= 0xd800 && code <= 0xdbff;
17
+ const isLow = code >= 0xdc00 && code <= 0xdfff;
18
+ if (isHigh) {
19
+ const next = value.charCodeAt(index + 1);
20
+ if (next < 0xdc00 || next > 0xdfff) return true;
21
+ index += 1;
22
+ } else if (isLow) {
23
+ return true;
24
+ }
25
+ }
26
+ return false;
27
+ }
28
+
29
+ function isGraphemePrefix(prefix: string, value: string): boolean {
30
+ let accumulated = "";
31
+ for (const unit of graphemeUnits(value)) {
32
+ if (accumulated === prefix) return true;
33
+ accumulated += unit;
34
+ }
35
+ return accumulated === prefix;
36
+ }
37
+
38
+ // Deterministic pseudo-random source so failures replay exactly.
39
+ function lcg(seed: number): () => number {
40
+ let state = seed >>> 0;
41
+ return () => {
42
+ state = (state * 1664525 + 1013904223) >>> 0;
43
+ return state / 0x1_0000_0000;
44
+ };
45
+ }
46
+
47
+ // Every entry is one or more whole grapheme clusters covering the cases named
48
+ // in issue #2195: astral emoji, ZWJ sequences, skin-tone modifiers,
49
+ // regional-indicator flags, Hangul (precomposed and jamo), combining marks,
50
+ // and astral kanji.
51
+ const GRAPHEME_PIECES = [
52
+ "a",
53
+ "b",
54
+ " ",
55
+ "7",
56
+ "😀",
57
+ "👨‍👩‍👧‍👦",
58
+ "👍🏽",
59
+ "🇯🇵",
60
+ "🇺🇸",
61
+ "🇰🇷",
62
+ "한",
63
+ "한",
64
+ "e\u0301",
65
+ "k\u0331\u0301",
66
+ "क़",
67
+ "𠮷",
68
+ "日",
69
+ "本",
70
+ "語",
71
+ ] as const;
72
+
73
+ test("truncateGraphemeSafe: property - valid Unicode, grapheme-aligned, within budget", () => {
74
+ const random = lcg(20260816);
75
+ for (let iteration = 0; iteration < 500; iteration += 1) {
76
+ let input = "";
77
+ const pieceCount = 1 + Math.floor(random() * 12);
78
+ for (let piece = 0; piece < pieceCount; piece += 1) {
79
+ input += GRAPHEME_PIECES[Math.floor(random() * GRAPHEME_PIECES.length)]!;
80
+ }
81
+ const maxChars = Math.floor(random() * (input.length + 2));
82
+ const output = truncateGraphemeSafe(input, maxChars);
83
+ assert.ok(
84
+ output.length <= maxChars,
85
+ `budget exceeded for ${JSON.stringify(input)} @ ${maxChars}: ${JSON.stringify(output)}`,
86
+ );
87
+ assert.ok(
88
+ !hasLoneSurrogate(output),
89
+ `lone surrogate for ${JSON.stringify(input)} @ ${maxChars}: ${JSON.stringify(output)}`,
90
+ );
91
+ assert.ok(
92
+ isGraphemePrefix(output, input),
93
+ `split grapheme for ${JSON.stringify(input)} @ ${maxChars}: ${JSON.stringify(output)}`,
94
+ );
95
+ if (input.length <= maxChars) {
96
+ assert.equal(output, input, `fits must pass through @ ${maxChars}`);
97
+ }
98
+ }
99
+ });
100
+
101
+ test("truncateGraphemeSafe: emoji, ZWJ, flags, jamo, and combining marks stay whole", () => {
102
+ assert.equal(truncateGraphemeSafe("hello world", 5), "hello");
103
+ assert.equal(truncateGraphemeSafe("hello", 50), "hello");
104
+ assert.equal(truncateGraphemeSafe("hello", 0), "");
105
+ assert.equal(truncateGraphemeSafe("👍👍", 2), "👍");
106
+ assert.equal(truncateGraphemeSafe("👍", 1), "", "a split emoji is dropped, never half-cut");
107
+ assert.equal(truncateGraphemeSafe("a👍b", 2), "a");
108
+ assert.equal(truncateGraphemeSafe("🇯🇵🇺🇸", 4), "🇯🇵");
109
+ assert.equal(truncateGraphemeSafe("🇯🇵🇺🇸", 3), "", "a split flag is dropped, never half-cut");
110
+ const family = "👨‍👩‍👧‍👦";
111
+ assert.equal(truncateGraphemeSafe(`${family}!`, family.length + 1), `${family}!`);
112
+ assert.equal(truncateGraphemeSafe(`${family}!`, family.length), family);
113
+ assert.equal(truncateGraphemeSafe(family, family.length - 1), "", "ZWJ sequence is atomic");
114
+ assert.equal(truncateGraphemeSafe("한글", 3), "한", "jamo run is one cluster");
115
+ assert.equal(truncateGraphemeSafe("e\u0301x", 2), "e\u0301", "base plus mark is one cluster");
116
+ assert.equal(truncateGraphemeSafe("e\u0301x", 1), "");
117
+ });
118
+
119
+ test("truncateCodePointSafe and collapseWhitespace keep prior behavior", () => {
120
+ assert.equal(collapseWhitespace(" a \n b "), "a b");
121
+ assert.equal(truncateCodePointSafe("abcd", 2), "ab");
122
+ assert.equal(truncateCodePointSafe("ab", 9), "ab");
123
+ assert.equal(truncateCodePointSafe("abc", 0), "");
124
+ });
125
+
126
+ test("displayWidth: CJK wide, fullwidth, halfwidth, marks, and emoji", () => {
127
+ assert.equal(displayWidth(""), 0);
128
+ assert.equal(displayWidth("abc"), 3);
129
+ assert.equal(displayWidth("日本語"), 6);
130
+ assert.equal(displayWidth("ハングル"), 8);
131
+ assert.equal(displayWidth("한국어"), 6);
132
+ assert.equal(displayWidth("ア"), 1, "halfwidth katakana is narrow");
133
+ assert.equal(displayWidth("full"), 8, "fullwidth ASCII is wide");
134
+ assert.equal(displayWidth("e\u0301"), 1, "combining mark is zero width");
135
+ assert.equal(displayWidth("😀"), 2);
136
+ assert.equal(displayWidth("中日 memo"), 9, "2+2+1+4");
137
+ });
138
+
139
+ test("displayWidth: emoji sequences measure as one two-cell grapheme", () => {
140
+ assert.equal(displayWidth("👨‍👩‍👧‍👦"), 2, "ZWJ family emoji is one cluster");
141
+ assert.equal(displayWidth("🇯🇵"), 2, "regional-indicator flag is one cluster");
142
+ assert.equal(displayWidth("👍🏽"), 2, "skin-tone modifier joins the cluster");
143
+ assert.equal(displayWidth("❤️"), 2, "VS16 requests emoji presentation");
144
+ assert.equal(displayWidth("1️⃣"), 2, "keycap sequence is one cluster");
145
+ assert.equal(displayWidth("👨‍👩‍👧‍👦 文"), 5, "clusters compose: 2 + 1 + 2");
146
+ assert.equal(
147
+ padEndDisplay("👨‍👩‍👧‍👦", 6),
148
+ "👨‍👩‍👧‍👦" + " ".repeat(4),
149
+ "emoji rows pad by display width, not code units",
150
+ );
151
+ });
152
+
153
+ test("padEndDisplay: CJK rows align to one computed width", () => {
154
+ assert.equal(padEndDisplay("abc", 6), "abc ");
155
+ assert.equal(padEndDisplay("abc", 2), "abc", "narrow target never truncates");
156
+ assert.equal(padEndDisplay("日本", 10), "日本" + " ".repeat(6));
157
+ const rows = ["機械学習", "nlp", "한국어", "x"];
158
+ const padded = rows.map((row) => padEndDisplay(row, 12));
159
+ for (const row of padded) {
160
+ assert.equal(displayWidth(row), 12);
161
+ }
162
+ assert.deepEqual(padded, [
163
+ "機械学習 ",
164
+ "nlp ",
165
+ "한국어 ",
166
+ "x ",
167
+ ]);
168
+ // Snapshot-style: the CLI topics table shape keeps its score column aligned
169
+ // for CJK rows, which plain padEnd misaligns.
170
+ const table = rows.slice(0, 3).map((term) => ` ${padEndDisplay(term, 12)} 0.123`);
171
+ assert.deepEqual(table, [
172
+ " 機械学習 0.123",
173
+ " nlp 0.123",
174
+ " 한국어 0.123",
175
+ ]);
176
+ const tableWidth = displayWidth(table[0]!);
177
+ assert.ok(table.every((line) => displayWidth(line) === tableWidth));
178
+ });
package/src/whitespace.ts CHANGED
@@ -8,3 +8,122 @@ export function truncateCodePointSafe(value: string, maxChars: number): string {
8
8
  if (glyphs.length <= maxChars) return value;
9
9
  return glyphs.slice(0, Math.max(1, maxChars)).join("").trimEnd();
10
10
  }
11
+
12
+ let graphemeSegmenter: Intl.Segmenter | null | undefined;
13
+
14
+ function getGraphemeSegmenter(): Intl.Segmenter | null {
15
+ if (graphemeSegmenter === undefined) {
16
+ graphemeSegmenter =
17
+ typeof Intl !== "undefined" && typeof Intl.Segmenter === "function"
18
+ ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
19
+ : null;
20
+ }
21
+ return graphemeSegmenter;
22
+ }
23
+
24
+ /**
25
+ * Longest prefix of `value` whose UTF-16 length stays within `maxChars`
26
+ * without splitting a surrogate pair or a grapheme cluster (ZWJ emoji
27
+ * sequences, flags, Hangul jamo runs, combining marks). A cluster wider than
28
+ * the budget is dropped whole rather than split. Falls back to code-point
29
+ * boundaries when Intl.Segmenter is unavailable.
30
+ */
31
+ export function truncateGraphemeSafe(value: string, maxChars: number): string {
32
+ if (maxChars <= 0) return "";
33
+ if (value.length <= maxChars) return value;
34
+ let truncated = "";
35
+ for (const unit of graphemeUnits(value)) {
36
+ if (truncated.length + unit.length > maxChars) break;
37
+ truncated += unit;
38
+ }
39
+ return truncated;
40
+ }
41
+
42
+ /**
43
+ * Grapheme clusters of `value` as separate strings, or its code points when
44
+ * Intl.Segmenter is unavailable.
45
+ */
46
+ export function graphemeUnits(value: string): string[] {
47
+ const segmenter = getGraphemeSegmenter();
48
+ if (segmenter === null) return [...value];
49
+ return Array.from(segmenter.segment(value), (segment) => segment.segment);
50
+ }
51
+
52
+ const COMBINING_MARK = /\p{M}/u;
53
+ const FORMAT_CHAR = /\p{Cf}/u;
54
+ const EMOJI_PRESENTATION = /\p{Emoji_Presentation}/u;
55
+ const EMOJI_PRESENTATION_SELECTOR = "\uFE0F";
56
+
57
+ // ponytail: pragmatic East_Asian_Width Wide/Fullwidth subset - contiguous
58
+ // block ranges plus the emoji presentation planes. Sparse exceptions inside
59
+ // those ranges (narrow unassigned code points, U+303F) do not matter for
60
+ // terminal column alignment; switch to a full EAW table only if drift shows.
61
+ const WIDE_CODE_POINT_RANGES: ReadonlyArray<readonly [number, number]> = [
62
+ [0x1100, 0x115f], // Hangul Jamo
63
+ [0x2e80, 0x303e], // CJK radicals, Kangxi radicals, CJK symbols and punctuation
64
+ [0x3041, 0x33ff], // Hiragana, Katakana, Bopomofo, Hangul compatibility jamo
65
+ [0x3400, 0x4dbf], // CJK unified ideographs extension A
66
+ [0x4e00, 0xa4cf], // CJK unified ideographs, Yi
67
+ [0xa960, 0xa97f], // Hangul Jamo extension A
68
+ [0xac00, 0xd7a3], // Hangul syllables
69
+ [0xf900, 0xfaff], // CJK compatibility ideographs
70
+ [0xfe10, 0xfe19], // Vertical forms
71
+ [0xfe30, 0xfe6b], // CJK compatibility forms
72
+ [0xff00, 0xff60], // Fullwidth ASCII and punctuation
73
+ [0xffe0, 0xffe6], // Fullwidth signs
74
+ [0x1f300, 0x1f64f], // Emoji symbols, pictographs, emoticons
75
+ [0x1f680, 0x1f6ff], // Transport and map
76
+ [0x1f900, 0x1f9ff], // Supplemental symbols
77
+ [0x1fa70, 0x1faff], // Symbols and pictographs extended-A
78
+ [0x20000, 0x2fffd], // CJK extensions B-F
79
+ [0x30000, 0x3fffd], // CJK extension G and later
80
+ ];
81
+
82
+ function isWideGlyph(glyph: string): boolean {
83
+ const codePoint = glyph.codePointAt(0)!;
84
+ let low = 0;
85
+ let high = WIDE_CODE_POINT_RANGES.length - 1;
86
+ while (low <= high) {
87
+ const mid = (low + high) >> 1;
88
+ const [start, end] = WIDE_CODE_POINT_RANGES[mid]!;
89
+ if (codePoint < start) high = mid - 1;
90
+ else if (codePoint > end) low = mid + 1;
91
+ else return true;
92
+ }
93
+ return false;
94
+ }
95
+
96
+ /**
97
+ * Terminal cell width: East_Asian_Width Wide/Fullwidth glyphs count as 2,
98
+ * combining marks and format characters (ZWJ, variation selectors) as 0,
99
+ * everything else as 1. A grapheme cluster with emoji presentation - ZWJ
100
+ * sequences, flags, skin-tone modifiers - occupies two cells.
101
+ */
102
+ export function displayWidth(value: string): number {
103
+ let width = 0;
104
+ for (const cluster of graphemeUnits(value)) {
105
+ width += graphemeClusterWidth(cluster);
106
+ }
107
+ return width;
108
+ }
109
+
110
+ function graphemeClusterWidth(cluster: string): number {
111
+ if (cluster.includes(EMOJI_PRESENTATION_SELECTOR)) return 2;
112
+ let emojiCluster = false;
113
+ let width = 0;
114
+ for (const glyph of cluster) {
115
+ if (COMBINING_MARK.test(glyph) || FORMAT_CHAR.test(glyph)) continue;
116
+ if (EMOJI_PRESENTATION.test(glyph)) emojiCluster = true;
117
+ width += isWideGlyph(glyph) ? 2 : 1;
118
+ }
119
+ return emojiCluster ? 2 : width;
120
+ }
121
+
122
+ /**
123
+ * padEnd analog that pads to a terminal cell width, so rows containing CJK
124
+ * or emoji keep their columns aligned.
125
+ */
126
+ export function padEndDisplay(value: string, targetWidth: number): string {
127
+ const padding = targetWidth - displayWidth(value);
128
+ return padding > 0 ? value + " ".repeat(padding) : value;
129
+ }
@@ -1,16 +0,0 @@
1
- // src/whitespace.ts
2
- function collapseWhitespace(value) {
3
- return value.replace(/\s+/g, " ").trim();
4
- }
5
- function truncateCodePointSafe(value, maxChars) {
6
- const glyphs = Array.from(value);
7
- if (maxChars <= 0) return "";
8
- if (glyphs.length <= maxChars) return value;
9
- return glyphs.slice(0, Math.max(1, maxChars)).join("").trimEnd();
10
- }
11
-
12
- export {
13
- collapseWhitespace,
14
- truncateCodePointSafe
15
- };
16
- //# sourceMappingURL=chunk-6MKAMLQL.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/whitespace.ts"],"sourcesContent":["export function collapseWhitespace(value: string): string {\n return value.replace(/\\s+/g, \" \").trim();\n}\n\nexport function truncateCodePointSafe(value: string, maxChars: number): string {\n const glyphs = Array.from(value);\n if (maxChars <= 0) return \"\";\n if (glyphs.length <= maxChars) return value;\n return glyphs.slice(0, Math.max(1, maxChars)).join(\"\").trimEnd();\n}\n"],"mappings":";AAAO,SAAS,mBAAmB,OAAuB;AACxD,SAAO,MAAM,QAAQ,QAAQ,GAAG,EAAE,KAAK;AACzC;AAEO,SAAS,sBAAsB,OAAe,UAA0B;AAC7E,QAAM,SAAS,MAAM,KAAK,KAAK;AAC/B,MAAI,YAAY,EAAG,QAAO;AAC1B,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,SAAO,OAAO,MAAM,GAAG,KAAK,IAAI,GAAG,QAAQ,CAAC,EAAE,KAAK,EAAE,EAAE,QAAQ;AACjE;","names":[]}